first commit

This commit is contained in:
2025-03-03 20:59:42 +03:00
commit 273e68557a
1099 changed files with 17880 additions and 0 deletions

183
lib/app.dart Normal file
View File

@@ -0,0 +1,183 @@
import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_displaymode/flutter_displaymode.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:fps_widget/fps_widget.dart';
import 'package:provider/provider.dart';
import 'package:remever/common/events/common_events.dart';
import 'package:remever/common/events/events.dart';
import 'package:remever/common/functions.dart';
import 'package:remever/common/getters.dart';
import 'package:remever/common/mixin/subscriptionable.dart';
import 'package:remever/common/storage.dart';
import 'package:remever/components/notifiers/app_settings.dart';
import 'package:remever/components/listeners/theme_listener.dart';
import 'package:remever/i18n/strings.g.dart';
import 'package:remever/inject.dart';
import 'package:remever/router.dart';
import 'package:remever/theme/custom_theme.dart';
final Completer<GlobalKey<NavigatorState>> navKeyCompleter =
Completer<GlobalKey<NavigatorState>>();
class MyApp extends StatefulWidget {
///
/// Основной класс приложения
///
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
///
/// Состояние приложения
///
class _MyAppState extends State<MyApp>
with Subscriptionable<MyApp>, WidgetsBindingObserver {
///
/// Установка максимально фреймрейта
///
Future<void> setOptimalDisplayMode() async {
if (Platform.isAndroid) {
try {
await FlutterDisplayMode.setHighRefreshRate();
} on PlatformException catch (e) {
debugPrint('Не удалось установить частоту кадров экрана ${e.code}');
}
}
}
@override
void initState() {
unawaited(setOptimalDisplayMode());
WidgetsBinding.instance.addObserver(this);
super.initState();
}
@override
List<StreamSubscription<dynamic>> get subscribe {
return <StreamSubscription<dynamic>>[
/// Слушатель событий оповещений
eventBus.on<NotificationEvent>().listen((NotificationEvent event) {
log('app -> ${event.text}');
}),
];
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.resumed:
break;
case AppLifecycleState.inactive:
case AppLifecycleState.paused:
case AppLifecycleState.detached:
case AppLifecycleState.hidden:
///
/// При сворачивании/закрывании приложения
/// Компонуем все [Hive] хранилища
///
hiveLang.compact();
hiveTheme.compact();
break;
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
Widget build(BuildContext context) {
SystemChrome.setPreferredOrientations(<DeviceOrientation>[
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
return Builder(
builder: (BuildContext context) {
return ScreenUtilInit(
designSize: const Size(380, 812),
minTextAdapt: true,
splitScreenMode: true,
useInheritedMediaQuery: true,
builder: (_, __) => _buildListeners(),
);
},
);
}
///
/// Построение слушателей
///
Widget _buildListeners() {
return ThemeModeListener(
builder: (BuildContext context, ThemeMode themeMode) {
return _buildMaterialApp(themeMode);
},
);
}
///
/// Основной построитель приложения
///
Widget _buildMaterialApp(ThemeMode themeMode) {
return MaterialApp.router(
title: 'Who Will Win',
theme: CustomTheme.lightTheme,
darkTheme: CustomTheme.darkTheme,
themeMode: themeMode,
showPerformanceOverlay: false,
locale: TranslationProvider.of(context).flutterLocale,
supportedLocales: AppLocaleUtils.supportedLocales,
localizationsDelegates: GlobalMaterialLocalizations.delegates,
routerConfig: globalRouter.config(),
builder: (BuildContext context, Widget? child) {
return ChangeNotifierProvider<AppSettingsNotifier>(
create: (_) => settingsNotifier,
builder: (BuildContext context, Widget? nchild) {
if (nchild != null) return nchild;
return Consumer<AppSettingsNotifier>(
// TIP: должно убрать мерцание
key: const Key('consumer AppSettingsNotifier'),
child: child,
builder: (
BuildContext context,
AppSettingsNotifier value,
Widget? nchild,
) {
final Widget result = nchild ?? const SizedBox();
if (value.showFps) {
return Material(
child: FPSWidget(
alignment: Alignment.centerLeft,
child: result,
),
);
}
return result;
},
);
},
);
},
);
}
}

View File

@@ -0,0 +1,40 @@
// Flutter imports:
import 'package:flutter/material.dart';
import 'package:remever/common/resources.dart';
///
/// Событие изменения темы
///
class ThemeChangedEvent {}
enum NotificationEventType { NOTIFY, ERROR, WARNING }
extension NotificationEventTypeExtension on NotificationEventType {
Color get color {
switch (this) {
case NotificationEventType.NOTIFY:
return AppColors.gray;
case NotificationEventType.ERROR:
return AppColors.red;
case NotificationEventType.WARNING:
return AppColors.yellowL;
}
}
}
///
/// Событие оповещения
///
class NotificationEvent {
NotificationEvent({
required this.text,
this.type = NotificationEventType.NOTIFY,
this.action,
});
final String text;
final NotificationEventType type;
final SnackBarAction? action;
}

View File

@@ -0,0 +1,7 @@
// Package imports:
import 'package:event_bus/event_bus.dart';
export '../../common/events/common_events.dart';
/// Глобальная шина данных [EventBus]
final EventBus eventBus = EventBus();

64
lib/common/functions.dart Normal file
View File

@@ -0,0 +1,64 @@
// Flutter imports:
import 'package:flutter/material.dart';
// Package imports:
import 'package:get_it/get_it.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import 'package:remever/common/resources.dart';
import 'package:remever/components/extensions/context.dart';
import 'package:remever/router.dart';
import 'events/events.dart';
///
/// Глобальный навигатор
///
AppRouter get globalRouter {
return GetIt.I.get<AppRouter>();
}
///
/// Глобальный показ ошибки
///
void showErrorNotification(String text, [SnackBarAction? action]) {
eventBus.fire(
NotificationEvent(
text: text,
type: NotificationEventType.ERROR,
action: action,
),
);
}
///
/// Глобальный показ уведомления
///
void showInfoNoitification(String text, [SnackBarAction? action]) {
eventBus.fire(
NotificationEvent(
text: text,
type: NotificationEventType.NOTIFY,
action: action,
),
);
}
///
/// Показ диалога как cupertino
///
Future<T?> showCuperModalBottomSheet<T>({
required BuildContext context,
required WidgetBuilder builder,
Color? backgroundColor,
double? height,
}) {
return showCupertinoModalBottomSheet(
topRadius: const Radius.circular(24).r,
backgroundColor: backgroundColor ?? AppColors.white,
context: context,
builder:
(BuildContext _) => SizedBox(
height: height ?? MediaQuery.of(context).size.height / 2,
child: Builder(builder: builder),
),
);
}

11
lib/common/getters.dart Normal file
View File

@@ -0,0 +1,11 @@
// Flutter imports:
import 'package:flutter/material.dart';
import 'package:remever/components/notifiers/app_settings.dart';
import 'package:remever/inject.dart';
import 'package:remever/theme/custom_theme.dart';
final CustomTheme currentTheme = CustomTheme(ThemeMode.light);
AppSettingsNotifier get settingsNotifier {
return getIt<AppSettingsNotifier>();
}

View File

@@ -0,0 +1,84 @@
// Dart imports:
import 'dart:async';
// Flutter imports:
import 'package:flutter/widgets.dart';
///
/// Миксин для виджетов которые имеют подписки
///
/// Автоматически отписывается в методе [dispose]
///
mixin Subscriptionable<T extends StatefulWidget> on State<T> {
/// Массив подписок
final List<StreamSubscription<dynamic>> subs =
<StreamSubscription<dynamic>>[];
///
/// Метод получения списка подписок
///
List<StreamSubscription<dynamic>> get subscribe {
return <StreamSubscription<dynamic>>[];
}
///
/// Обновление состояния экрана если он [mounted]
///
@protected
void setState_(VoidCallback? callback) {
callback?.call();
if (mounted) {
// ignore: no-empty-block
setState(() {});
}
}
@override
void initState() {
subs.addAll(subscribe);
super.initState();
}
@override
void dispose() {
for (StreamSubscription<dynamic> sub in subs) {
sub.cancel();
}
super.dispose();
}
}
///
/// Миксин для подписки любых классов
///
mixin WithSubscription on Object {
/// Массив подписок
final List<StreamSubscription<dynamic>> subs =
<StreamSubscription<dynamic>>[];
///
/// Метод получения списка подписок
///
List<StreamSubscription<dynamic>> get subscribe {
return <StreamSubscription<dynamic>>[];
}
///
/// Добавить все подписки из subscribe
///
void subscribeAll() {
subs.addAll(subscribe);
}
///
/// Отписаться от всех подписок
///
void unsubscribe() {
for (StreamSubscription<dynamic> sub in subs) {
sub.cancel();
}
}
}

131
lib/common/resources.dart Normal file
View File

@@ -0,0 +1,131 @@
// Flutter imports:
import 'package:flutter/material.dart';
// Package imports:
import 'package:intl/intl.dart';
///
/// Константы
///
abstract class Const {}
abstract class Storage {
///
/// Хранилище авторизации
///
static const String storageAuth = 'auth';
///
/// Хранилище языка
///
static const String hiveLang = 'lang';
///
/// Ключ для хранилища [ThemeMode]
///
static const String hiveThemeMode = 'hive_theme_mode';
}
///
/// Высчитываемые константы
///
abstract class Compute {
///
/// Денежный форматтер
///
static final NumberFormat currency = NumberFormat.currency(
locale: 'ru_RU',
symbol: 'руб.',
decimalDigits: 0,
);
}
///
/// Имена ключей хранилищ
///
abstract class StorageKeys {
///
/// Ключ хранения токена авторизации
///
static const String accessToken = 'access.token';
///
/// Ключ хранения токена обновления
///
static const String refreshToken = 'refresh.token';
///
/// Ключ хранения кода языка
///
static const String langCode = 'lang.code';
///
/// Ключ хранения темы приложения
///
static const String themeKey = 'theme_key';
///
/// Ключ хранения селфа
///
static const String profile = 'profile';
}
///
/// Описание некоторых значений по-умолчанию
///
abstract class Defaults {
///
/// Длительность анимаций
///
static const Duration animationDur = Duration(milliseconds: 300);
}
///
/// Цвета приложения
///
abstract class AppColors {
static const Color white = Color(0xFFFFFFFF);
static const Color bg = Color(0xfFEFEFF4);
static const Color primary = Color(0xFF4633BF);
static const Color disabled = Color(0xFF8B8B8B);
static const Color gray = Color(0xFFE0E0E0);
static const Color body_text = Color(0xFF080514);
static const Color secondary = Color(0xFF1F1F1F);
static const Color navigationL = Color(0xFF5C5C5C);
static const Color navigationD = Color(0xFFE6E6E6);
static const Gradient primaryGradient = LinearGradient(
stops: <double>[0.52, 1.0],
colors: <Color>[Color(0xFFFFD12D), Color(0xFFFF922D)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
static const Color black = Color(0xFF282828);
static const Color secondaryL = Color(0xFF282828);
static const Color green = Color(0xFF3EBF81);
static const Color yellowText = Color(0xFFCEBC13);
static const Color purple = Color(0xFF923EFF);
static const Color red = Color(0xFFDA4E4E);
static const Color blueL = Color(0xFFE1F1FD);
static const Color blueD = Color(0xFF22333F);
static const Color greenD = Color(0xFF24372E);
static const Color purpleD = Color(0xFF2B2235);
static const Color redD = Color(0xFF3B2626);
static const Color yellowD = Color(0xFF413A21);
static const Color greenL = Color(0xFFE2F5EC);
static const Color purpleL = Color(0xFFF4ECFF);
static const Color redL = Color(0xFFF9E4E4);
static const Color yellowL = Color(0xFFFFF8E0);
static const Color backgroundLD1 = Color(0xFFF2F2F2);
static const Color backgroundL = Color(0xFFF8F8F8);
static const Color secondaryLL1 = Color(0xFF303030);
static const Color backgroundLL1 = Color(0xFFFBFBFB);
static const Color app_blue = Color(0xFF0b84f6);
static const Color app_grey = Color(0xFFF7F7F9);
static const Color app_dark_grey = Color(0xFF7C7C7C);
static const Color app_border = Color(0xFFE5E5E8);
static const Color app_dark_blue = Color(0xFF888B98);
static const Color app_err = Color(0xFFFF4D49);
static const Color app_overlay = Color(0xFFF3F3FF);
}

View File

@@ -0,0 +1,84 @@
// Package imports:
import 'package:curl_logger_dio_interceptor/curl_logger_dio_interceptor.dart';
import 'package:dio/dio.dart';
import 'package:dio_smart_retry/dio_smart_retry.dart';
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
// Project imports:
import '../../components/env.dart';
///
/// Обработчик на события для авторизации
///
InterceptorsWrapper get _auth {
return InterceptorsWrapper(
onRequest: (
RequestOptions options,
RequestInterceptorHandler handler,
) async {
// try {
// String? token = await authSecStorage.read(key: StorageKeys.authToken);
// if (token != null) {
// options.headers['Authorization'] = 'Bearer $token';
// }
// } catch (e) {
// getIt<LogService>().log(
// entity: LogEntity.error(message: 'Error to load access token $e'),
// );
// }
return handler.next(options);
},
);
}
InterceptorsWrapper get _error {
return InterceptorsWrapper(
onError: (DioException error, ErrorInterceptorHandler handler) async {
final int? statusCode = error.response?.statusCode;
if (statusCode == 401) {
// String? token = await getIt<AuthService>().refresh();
}
handler.next(error);
},
);
}
///
/// API клиент для работы с бекендом
///
Dio get apiClient {
final Dio client = Dio(
BaseOptions(
baseUrl: Env.get.url.toString(),
contentType: 'application/json',
),
);
client.interceptors
..add(_auth)
..add(_error)
..add(
PrettyDioLogger(
request: true,
requestBody: true,
requestHeader: true,
responseBody: true,
error: true,
),
)
..add(CurlLoggerDioInterceptor())
..add(
RetryInterceptor(
dio: client,
logPrint: print,
retries: 1,
retryDelays: <Duration>[const Duration(seconds: 1)],
),
);
return client;
}

25
lib/common/storage.dart Normal file
View File

@@ -0,0 +1,25 @@
// Flutter imports:
import 'package:flutter/material.dart' show ThemeMode;
// Package imports:
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hive_ce/hive.dart';
import 'package:remever/common/resources.dart';
import 'package:remever/i18n/strings.g.dart';
// Project imports:
///
/// Защищенное хранилище для авторизации
///
const FlutterSecureStorage authSecStorage = FlutterSecureStorage();
///
/// Защищенное хранилище для ключей [Hive]
///
const FlutterSecureStorage hiveKeysStorage = FlutterSecureStorage();
/// --- Hive
Box<AppLocale> get hiveLang => Hive.box<AppLocale>(Storage.hiveLang);
Box<ThemeMode> get hiveTheme => Hive.box<ThemeMode>(Storage.hiveThemeMode);

9
lib/common/typedef.dart Normal file
View File

@@ -0,0 +1,9 @@
///
/// Сокращение стандартной [Map] до более лаконичного [Json]
///
typedef Json = Map<String, dynamic>;
///
/// Сокращение [Map<String, dynamic>] до более лаконичного [JsonEntry]
///
typedef JsonEntry = MapEntry<String, dynamic>;

146
lib/common/typography.dart Normal file
View File

@@ -0,0 +1,146 @@
import 'package:remever/common/widgets/typography.dart';
/// -- Regular --
class Regular12px extends TypographyTypeRegular {
@override
double get size => 12;
}
class Regular13px extends TypographyTypeRegular {
@override
double get size => 13;
}
class Regular14px extends TypographyTypeRegular {
@override
double get size => 14;
}
class Regular16px extends TypographyTypeRegular {
@override
double get size => 16;
}
class Regular17px extends TypographyTypeRegular {
@override
double get size => 17;
}
// -- Medium --
class Medium12px extends TypographyTypeMedium {
@override
double get size => 12;
}
class Medium13px extends TypographyTypeMedium {
@override
double get size => 13;
}
class Medium14px extends TypographyTypeMedium {
@override
double get size => 14;
}
class Medium16px extends TypographyTypeMedium {
@override
double get size => 16;
}
// -- SemiBold --
class SemiBold10px extends TypographyTypeSemiBold {
@override
double get size => 10;
}
class SemiBold11px extends TypographyTypeSemiBold {
@override
double get size => 11;
}
class SemiBold12px extends TypographyTypeSemiBold {
@override
double get size => 12;
}
class SemiBold13px extends TypographyTypeSemiBold {
@override
double get size => 13;
}
class SemiBold14px extends TypographyTypeSemiBold {
@override
double get size => 14;
}
class SemiBold18px extends TypographyTypeSemiBold {
@override
double get size => 18;
}
class SemiBold22px extends TypographyTypeSemiBold {
@override
double get size => 22;
}
class SemiBold24px extends TypographyTypeSemiBold {
@override
double get size => 24;
}
class SemiBold28px extends TypographyTypeSemiBold {
@override
double get size => 28;
}
// -- Bold --
class Bold10px extends TypographyTypeBold {
@override
double get size => 10;
}
class Bold12px extends TypographyTypeBold {
@override
double get size => 12;
}
class Bold13px extends TypographyTypeBold {
@override
double get size => 13;
}
class Bold14px extends TypographyTypeBold {
@override
double get size => 14;
}
class Bold16px extends TypographyTypeBold {
@override
double get size => 16;
}
class Bold18px extends TypographyTypeBold {
@override
double get size => 18;
}
class Bold24px extends TypographyTypeBold {
@override
double get size => 24;
}
// -- HeadBold --
class HeadBold20px extends TypographyTypeHeadBold {
@override
double get size => 20;
}
class HeadBold28px extends TypographyTypeHeadBold {
@override
double get size => 28;
}

79
lib/common/utils.dart Normal file
View File

@@ -0,0 +1,79 @@
///
/// Сервис со вспомогательными функциями
///
class Utils {
///
/// Склонение числительных
///
/// [number] Число, по которому будем склонять [int]
/// [titles] Возможные наборы данных. Должно быть 3 варианта [List<String>]
///
/// Пример:
///
/// ```dart
/// print(HelperService.declOfNum(1, ['секунда', 'секунды', 'секунд'])); // секунда
/// print(HelperService.declOfNum(2, ['секунда', 'секунды', 'секунд'])); // секунды
/// print(HelperService.declOfNum(5, ['секунда', 'секунды', 'секунд'])); // секунд
/// ```
///
static T declOfNum<T>(int number, List<T> titles) {
List<int> cases = <int>[2, 0, 1, 1, 1, 2];
return titles[(number % 100 > 4 && number % 100 < 20)
? 2
: cases[(number % 10 < 5) ? number % 10 : 5]];
}
}
typedef ValidatorFunc = String? Function(String? value);
class Validators {
///
/// Комбинирование нескольких валидаторов
/// Исполнение идет в порядке их передачи
///
static String? combine(List<ValidatorFunc> validators, String? value) {
for (ValidatorFunc vfunc in validators) {
final String? result = vfunc.call(value);
if (result != null) {
return result;
}
}
return null;
}
///
/// Метод валидации данных
///
static String? string(String? value) {
if (value == null || value.isEmpty) {
return 'Значение не может быть пустым';
}
if (value.length < 3) {
return 'Значение не может быть меньше 3 символов';
}
return null;
}
static String? email(String? email) {
if (email == null) return null;
if (email.isEmpty) return 'Поле e-mail пустое';
if (!email.contains('@')) return 'Неверный e-mail';
return null;
}
static String? phone(String? phone) {
if (phone == null) return null;
if (phone.isEmpty) return 'Введите номер телефона';
if (phone.length != 18) return 'Неверный формат номера';
return null;
}
}

View File

@@ -0,0 +1,21 @@
// Flutter imports:
import 'package:flutter/material.dart';
class BottomSafeSpace extends StatelessWidget {
///
/// Отступ от нижней границы экрана
///
/// Для iOS значение будет не нулевое если есть "полоска"
/// Для Android в основном будет 0
///
const BottomSafeSpace({
super.key,
});
@override
Widget build(BuildContext context) {
return SizedBox(
height: MediaQuery.of(context).padding.bottom,
);
}
}

View File

@@ -0,0 +1,24 @@
// Flutter imports:
import 'package:flutter/material.dart';
class LooseFocus extends StatelessWidget {
///
/// Теряет фокус если тапнули по пустой области
///
const LooseFocus({
required this.child,
super.key,
});
/// Потомок
final Widget child;
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => FocusScope.of(context).requestFocus(FocusNode()),
child: child,
);
}
}

View File

@@ -0,0 +1,68 @@
// Flutter imports:
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
///
/// Направления свайпов
///
enum SwipeDirection {
///
/// Свайп вниз
///
DOWN,
///
/// Свайп вверх
///
UP,
}
class SwipeGesture extends StatelessWidget {
///
/// Отслеживание свайпа по виджету
///
const SwipeGesture({
required this.swipe,
required this.onSwipe,
required this.child,
super.key,
});
/// Направление движения
final SwipeDirection swipe;
/// Обработка свайпа
final VoidCallback onSwipe;
/// Потомок
final Widget child;
void _onVerticalDragUpdate(DragUpdateDetails e) {
const int sensitivity = 3;
switch (swipe) {
case SwipeDirection.DOWN:
if (e.delta.dy > sensitivity) {
onSwipe();
}
break;
case SwipeDirection.UP:
if (e.delta.dy < sensitivity) {
onSwipe();
}
break;
}
}
@override
Widget build(BuildContext context) {
return GestureDetector(
dragStartBehavior: DragStartBehavior.start,
behavior: HitTestBehavior.opaque,
onVerticalDragUpdate: _onVerticalDragUpdate,
child: child,
);
}
}

View File

@@ -0,0 +1,256 @@
// Flutter imports:
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
// Package imports:
import 'package:google_fonts/google_fonts.dart';
export '../../common/typography.dart';
abstract class TypographyType {
///
/// Размер шрифта
///
double get size;
///
/// Получение [TextStyle] для типа шрифта
///
TextStyle get style;
}
abstract class TypographyTypeRegular extends TypographyType {
///
/// Высота линии
///
double get height => 1.21;
@override
TextStyle get style {
return GoogleFonts.inter(
fontWeight: FontWeight.w400,
fontSize: size.sp,
height: height,
);
}
}
abstract class TypographyTypeMedium extends TypographyType {
///
/// Высота линии
///
double get height => 1.15;
@override
TextStyle get style {
return GoogleFonts.inter(
fontWeight: FontWeight.w500,
fontSize: size.sp,
height: height,
);
}
}
abstract class TypographyTypeSemiBold extends TypographyType {
///
/// Высота линии
///
double get height => 1.21;
@override
TextStyle get style {
return GoogleFonts.inter(
fontWeight: FontWeight.w600,
fontSize: size.sp,
height: height,
);
}
}
abstract class TypographyTypeBold extends TypographyType {
///
/// Высота линии
///
double get height => 1.21;
@override
TextStyle get style {
return GoogleFonts.inter(
fontWeight: FontWeight.w700,
fontSize: size.sp,
height: height,
);
}
}
abstract class TypographyTypeHeadBold extends TypographyType {
///
/// Высота линии
///
double get height => 1.36;
@override
TextStyle get style {
return GoogleFonts.inter(
fontWeight: FontWeight.w700,
fontSize: size.sp,
height: height,
);
}
}
///
/// Дополнительные возможности [AppTypography]
///
enum TypographyFeature {
///
/// Использование [Text]
///
DEFAULT,
///
/// Использование [RichText]
///
RICH,
///
/// Использование [SelectableText]
///
SELECTABLE,
}
///
/// Виджет для отображения текста в приложении
///
/// Тесно использует [TypographyType]
/// На его основе выбирается стиль текста
///
/// Важно: планшетная верстка не учитывается в стилях - меняется сам стиль,
/// поэтому контролировать нужно извне
///
class AppTypography extends StatelessWidget {
const AppTypography(
this.text, {
this.type,
this.color,
this.maxLines = 1,
this.textAlign = TextAlign.left,
this.fontWeight,
this.overflow = TextOverflow.ellipsis,
this.height,
this.softWrap,
this.textDecoration = TextDecoration.none,
super.key,
}) : typographyFeature = TypographyFeature.DEFAULT,
children = null;
const AppTypography.rich(
this.text, {
this.type,
this.color,
this.maxLines,
this.textAlign = TextAlign.left,
this.fontWeight,
this.overflow,
this.height,
this.softWrap,
this.textDecoration = TextDecoration.none,
this.children,
super.key,
}) : typographyFeature = TypographyFeature.RICH;
const AppTypography.selectable(
this.text, {
this.type,
this.color,
this.maxLines,
this.textAlign = TextAlign.left,
this.fontWeight,
this.overflow,
this.height,
this.softWrap,
this.textDecoration = TextDecoration.none,
super.key,
}) : typographyFeature = TypographyFeature.SELECTABLE,
children = null;
/// Текст
final String text;
/// Стиль текста
final TypographyType? type;
/// Цвет текста
final Color? color;
/// Кол-во линий
final int? maxLines;
/// Направление текста
final TextAlign textAlign;
/// Жирность шрифта
final FontWeight? fontWeight;
/// Overflow
final TextOverflow? overflow;
/// Межстрочный интервал
final double? height;
/// Мягкий перенос
final bool? softWrap;
/// Декорация текста
final TextDecoration textDecoration;
/// Использование фич типографии
final TypographyFeature typographyFeature;
/// Список [TextSpan]
final List<InlineSpan>? children;
@override
Widget build(BuildContext context) {
final DefaultTextStyle textStyle = DefaultTextStyle.of(context);
final TextStyle computeTextStyle =
type == null
? textStyle.style
: type!.style.copyWith(
color: color ?? textStyle.style.color,
fontWeight: fontWeight,
height: height,
decoration: textDecoration,
);
switch (typographyFeature) {
case TypographyFeature.DEFAULT:
return Text(
text,
maxLines: maxLines,
textAlign: textAlign,
overflow: overflow,
softWrap: softWrap,
style: computeTextStyle,
);
case TypographyFeature.RICH:
return RichText(
text: TextSpan(
text: text,
children: children,
style: computeTextStyle,
),
);
case TypographyFeature.SELECTABLE:
return SelectableText(
text,
maxLines: maxLines,
textAlign: textAlign,
style: computeTextStyle,
);
}
}
}

View File

@@ -0,0 +1,15 @@
// Flutter imports:
import 'package:flutter/material.dart';
class TypographySpan extends TextSpan {
///
/// Упрощенный вариант [TextSpan]
///
const TypographySpan(
this.text, {
super.style,
});
// ignore: annotate_overrides, overridden_fields
final String text;
}

View File

@@ -0,0 +1,35 @@
// Flutter imports:
import 'package:flutter/material.dart';
///
/// Виджет условной отрисовки
///
class Wif extends StatelessWidget {
/// Условие по которому будет происходить отрисовка
final bool condition;
/// Построение содержимого
final WidgetBuilder builder;
/// Виджет если условие не удовлетворительно
final WidgetBuilder? fallback;
///
/// Виджет условной отрисовки
///
const Wif({
required this.condition,
required this.builder,
this.fallback,
super.key,
});
@override
Widget build(BuildContext context) {
return condition
? builder(context)
: fallback != null
? fallback!(context)
: const Offstage();
}
}

View File

@@ -0,0 +1,61 @@
// Flutter imports:
import 'package:flutter/widgets.dart';
import 'package:remever/components/extensions/context.dart';
///
/// Разделитель между виджетами
///
class HSpace extends StatelessWidget {
/// Высота разделителя
final double height;
/// Флаг что необходимо использовать [SliverToBoxAdapter]
final bool useSliver;
///
/// Разделитель между виджетами
///
const HSpace(this.height, {super.key}) : useSliver = false;
const HSpace.sliver(this.height, {super.key}) : useSliver = true;
@override
Widget build(BuildContext context) {
Widget child = SizedBox(height: height.h);
if (useSliver) {
child = SliverToBoxAdapter(child: child);
}
return child;
}
}
///
/// Разделитель между виджетами
///
class WSpace extends StatelessWidget {
/// Ширина разделителя
final double width;
/// Флаг что необходимо использовать [SliverToBoxAdapter]
final bool useSliver;
///
/// Разделитель между виджетами
///
const WSpace(this.width, {super.key}) : useSliver = false;
const WSpace.sliver(this.width, {super.key}) : useSliver = true;
@override
Widget build(BuildContext context) {
Widget child = SizedBox(width: width.w);
if (useSliver) {
child = SliverToBoxAdapter(child: child);
}
return child;
}
}

View File

@@ -0,0 +1,50 @@
// Dart imports:
import 'dart:async';
// Flutter imports:
import 'package:flutter/material.dart';
///
/// Компонент для выполнения отложенных операций
///
class Debouncer {
///
/// Компонент для выполнения отложенных операций
///
/// Пример использования
///
/// ```dart
/// final Debouncer _searchDelayer = Debouncer(
/// delay: const Duration(milliseconds: 300),
/// );
///
/// Widget _buildButton(BuildContext) {
/// return ElevatedButton(
/// onPressed: () {
/// _debouncer.run(() {
/// debugPrint('Type your code here');
/// });
/// },
/// );
/// }
/// ```
///
Debouncer({
required this.delay,
});
/// Время через которое необходимо вызывать функцию
final Duration delay;
/// Таймер
Timer? _timer;
///
/// Запуск отложенного события
///
void run(VoidCallback action) {
_timer?.cancel();
_timer = Timer(delay, action);
}
}

30
lib/components/env.dart Normal file
View File

@@ -0,0 +1,30 @@
// Package imports:
import 'package:get_it/get_it.dart';
part 'env/dev_env.dart';
part 'env/prod_env.dart';
///
/// Базовый класс ENV
///
abstract class Env {
///
/// Указание на URL бекенда
///
Uri get url;
///
/// Режим работы приложения
///
String get mode;
///
/// Получение ENV более удобным для написания кода способом
///
static Env get get => Env.of();
///
/// Получение ENV
///
static Env of() => GetIt.I.get<Env>();
}

12
lib/components/env/dev_env.dart vendored Normal file
View File

@@ -0,0 +1,12 @@
part of '../env.dart';
///
/// DEV сборка
///
class DevEnv extends Env {
@override
Uri get url => Uri.parse('https://api.remever.dizoft-studio.ru');
@override
String get mode => 'dev';
}

12
lib/components/env/prod_env.dart vendored Normal file
View File

@@ -0,0 +1,12 @@
part of '../env.dart';
///
/// Продакшн сборка
///
class ProdEnv extends Env {
@override
Uri get url => Uri.parse('https://api.remever.dizoft-studio.ru');
@override
String get mode => 'prod';
}

View File

@@ -0,0 +1,21 @@
// Flutter imports:
import 'package:flutter/widgets.dart';
/// Подключение сторонней библиотеки
/// Сделано для упрощения чтения импортов
export 'package:flutter_screenutil/flutter_screenutil.dart';
///
/// Маштабирование в зависимости от контекста
///
extension ScaleFromContext on BuildContext {
///
/// Screen Width
///
double get sw => MediaQuery.of(this).size.width;
///
/// Screen Height
///
double get sh => MediaQuery.of(this).size.height;
}

View File

@@ -0,0 +1,21 @@
///
/// Расширение для работы с [Duration]
///
extension AppDuration on Duration {
///
/// Получение длительности в формате mm:ss
///
String get mmss => hhmmss.substring('00:'.length);
///
/// Получение длительности в формате HH:mm:ss
///
String get hhmmss {
String twoDigits(int n) => n.toString().padLeft(2, '0');
String twoDigitMinutes = twoDigits(inMinutes.remainder(60));
String twoDigitSeconds = twoDigits(inSeconds.remainder(60));
return '${twoDigits(inHours)}:$twoDigitMinutes:$twoDigitSeconds';
}
}

View File

@@ -0,0 +1,26 @@
// Dart imports:
import 'dart:async';
// Flutter imports:
import 'package:flutter/material.dart';
extension StateExtension on State {
/// [setState] when it's not building, then wait until next frame built.
FutureOr<void> safeSetState(FutureOr<dynamic> Function() fn) async {
await fn();
if (mounted &&
!context.debugDoingBuild &&
context.owner?.debugBuilding == false) {
// ignore: invalid_use_of_protected_member, no-empty-block
setState(() {});
}
final Completer<void> completer = Completer<void>();
WidgetsBinding.instance.addPostFrameCallback((_) {
completer.complete();
});
return completer.future;
}
}

View File

@@ -0,0 +1,31 @@
// Flutter imports:
import 'package:flutter/material.dart' show Color;
///
/// Расширение для работы со строками
///
extension MString on String {
///
/// Слово с заглавной буквы
///
String capitalyze() {
final String str = toLowerCase();
final String first = str.substring(0, 1);
return '${first.toUpperCase()}${str.substring(1)}';
}
///
/// Парсинг hex string в color
///
Color get toColor {
String res = replaceFirst('#', '');
return Color(
int.parse(
res.length == 8 ? res : (res.length == 6 ? 'FF$res' : res),
radix: 16,
),
);
}
}

View File

@@ -0,0 +1,21 @@
// Flutter imports:
import 'package:flutter/material.dart' show ThemeMode;
/// Расширение [ThemeMode]
extension ThemModeExtension on ThemeMode {
///
/// Получение инверсивного [ThemeMode]
///
ThemeMode get inversed {
switch (this) {
case ThemeMode.system:
return ThemeMode.light;
case ThemeMode.light:
return ThemeMode.dark;
case ThemeMode.dark:
return ThemeMode.light;
}
}
}

View File

@@ -0,0 +1,38 @@
// Flutter imports:
import 'package:flutter/material.dart';
// Package imports:
import 'package:get_it/get_it.dart';
import 'package:hive_ce_flutter/hive_flutter.dart';
import 'package:remever/common/storage.dart';
import 'package:remever/services/core/theme_service.dart';
// Project imports:
/// Описание функции для построения содержимого [ThemeModeListener]
typedef ThemeModeBuilder =
Widget Function(BuildContext context, ThemeMode themeMode);
class ThemeModeListener extends StatelessWidget {
///
/// Слушатель изменений темы
///
const ThemeModeListener({required this.builder, super.key});
/// Построитель
final ThemeModeBuilder builder;
@override
Widget build(BuildContext context) {
final ThemeService service = GetIt.I.get<ThemeService>();
return ValueListenableBuilder<Box<ThemeMode>>(
valueListenable: hiveTheme.listenable(),
builder: (BuildContext context, _, Widget? child) {
if (child != null) return child;
return builder(context, service.themeMode);
},
);
}
}

View File

@@ -0,0 +1,39 @@
// Flutter imports:
import 'package:flutter/material.dart' show ChangeNotifier;
///
/// Динамические параметры для конфигурирования приложения
///
class AppSettingsNotifier extends ChangeNotifier {
///
/// Динамические параметры для конфигурирования приложения
///
AppSettingsNotifier({
this.debugMode = false,
this.showFps = false,
});
/// Включение дебаг мода
bool debugMode;
/// Отображение FPS
bool showFps;
///
/// Переключение режима "Дебаг"
///
void toggleDebugMode() {
debugMode = !debugMode;
notifyListeners();
}
///
/// Переключение режима "Дебаг"
///
void toggleFps() {
showFps = !showFps;
notifyListeners();
}
}

View File

@@ -0,0 +1,18 @@
// Flutter imports:
import 'package:flutter/foundation.dart';
class CollectionData extends ChangeNotifier {
CollectionData({this.showFAB = true});
/// Флаг показа
bool showFAB;
/// Смена состояния показа Fab на экране коллекции
void showFab(bool show) {
if (showFAB != show) {
showFAB = show;
notifyListeners();
}
}
}

9
lib/env.dart Normal file
View File

@@ -0,0 +1,9 @@
// Package imports:
import 'package:get_it/get_it.dart';
// Project imports:
import '../../components/env.dart';
void configureEnv() {
GetIt.I.registerSingleton<Env>(DevEnv());
}

430
lib/gen/assets.gen.dart Normal file
View File

@@ -0,0 +1,430 @@
/// GENERATED CODE - DO NOT MODIFY BY HAND
/// *****************************************************
/// FlutterGen
/// *****************************************************
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: directives_ordering,unnecessary_import,implicit_dynamic_list_literal,deprecated_member_use
import 'package:flutter/widgets.dart';
class $AssetsIconsGen {
const $AssetsIconsGen();
/// File path: assets/icons/back.png
AssetGenImage get back => const AssetGenImage('assets/icons/back.png');
/// File path: assets/icons/call.png
AssetGenImage get call => const AssetGenImage('assets/icons/call.png');
/// File path: assets/icons/mail.png
AssetGenImage get mail => const AssetGenImage('assets/icons/mail.png');
/// File path: assets/icons/type=arrow_down.png
AssetGenImage get typeArrowDown =>
const AssetGenImage('assets/icons/type=arrow_down.png');
/// File path: assets/icons/type=back.png
AssetGenImage get typeBack =>
const AssetGenImage('assets/icons/type=back.png');
/// File path: assets/icons/type=bold.png
AssetGenImage get typeBold =>
const AssetGenImage('assets/icons/type=bold.png');
/// File path: assets/icons/type=capsLock.png
AssetGenImage get typeCapsLock =>
const AssetGenImage('assets/icons/type=capsLock.png');
/// File path: assets/icons/type=cards.png
AssetGenImage get typeCards =>
const AssetGenImage('assets/icons/type=cards.png');
/// File path: assets/icons/type=check.png
AssetGenImage get typeCheck =>
const AssetGenImage('assets/icons/type=check.png');
/// File path: assets/icons/type=check_round.png
AssetGenImage get typeCheckRound =>
const AssetGenImage('assets/icons/type=check_round.png');
/// File path: assets/icons/type=checkbox_empty_.png
AssetGenImage get typeCheckboxEmpty =>
const AssetGenImage('assets/icons/type=checkbox_empty_.png');
/// File path: assets/icons/type=checkbox_fill.png
AssetGenImage get typeCheckboxFill =>
const AssetGenImage('assets/icons/type=checkbox_fill.png');
/// File path: assets/icons/type=close.png
AssetGenImage get typeClose =>
const AssetGenImage('assets/icons/type=close.png');
/// File path: assets/icons/type=collection.png
AssetGenImage get typeCollection =>
const AssetGenImage('assets/icons/type=collection.png');
/// File path: assets/icons/type=copy.png
AssetGenImage get typeCopy =>
const AssetGenImage('assets/icons/type=copy.png');
/// File path: assets/icons/type=create card.png
AssetGenImage get typeCreateCard =>
const AssetGenImage('assets/icons/type=create card.png');
/// File path: assets/icons/type=danger.png
AssetGenImage get typeDanger =>
const AssetGenImage('assets/icons/type=danger.png');
/// File path: assets/icons/type=description.png
AssetGenImage get typeDescription =>
const AssetGenImage('assets/icons/type=description.png');
/// File path: assets/icons/type=download.png
AssetGenImage get typeDownload =>
const AssetGenImage('assets/icons/type=download.png');
/// File path: assets/icons/type=edit.png
AssetGenImage get typeEdit =>
const AssetGenImage('assets/icons/type=edit.png');
/// File path: assets/icons/type=flip.png
AssetGenImage get typeFlip =>
const AssetGenImage('assets/icons/type=flip.png');
/// File path: assets/icons/type=flip_2.png
AssetGenImage get typeFlip2 =>
const AssetGenImage('assets/icons/type=flip_2.png');
/// File path: assets/icons/type=heading.png
AssetGenImage get typeHeading =>
const AssetGenImage('assets/icons/type=heading.png');
/// File path: assets/icons/type=hide.png
AssetGenImage get typeHide =>
const AssetGenImage('assets/icons/type=hide.png');
/// File path: assets/icons/type=img.png
AssetGenImage get typeImg => const AssetGenImage('assets/icons/type=img.png');
/// File path: assets/icons/type=in focus.png
AssetGenImage get typeInFocus =>
const AssetGenImage('assets/icons/type=in focus.png');
/// File path: assets/icons/type=info.png
AssetGenImage get typeInfo =>
const AssetGenImage('assets/icons/type=info.png');
/// File path: assets/icons/type=learn.png
AssetGenImage get typeLearn =>
const AssetGenImage('assets/icons/type=learn.png');
/// File path: assets/icons/type=like.png
AssetGenImage get typeLike =>
const AssetGenImage('assets/icons/type=like.png');
/// File path: assets/icons/type=like_18_18.png
AssetGenImage get typeLike1818 =>
const AssetGenImage('assets/icons/type=like_18_18.png');
/// File path: assets/icons/type=like_fill.png
AssetGenImage get typeLikeFill =>
const AssetGenImage('assets/icons/type=like_fill.png');
/// File path: assets/icons/type=markers.png
AssetGenImage get typeMarkers =>
const AssetGenImage('assets/icons/type=markers.png');
/// File path: assets/icons/type=menu_vertical.png
AssetGenImage get typeMenuVertical =>
const AssetGenImage('assets/icons/type=menu_vertical.png');
/// File path: assets/icons/type=minus.png
AssetGenImage get typeMinus =>
const AssetGenImage('assets/icons/type=minus.png');
/// File path: assets/icons/type=move.png
AssetGenImage get typeMove =>
const AssetGenImage('assets/icons/type=move.png');
/// File path: assets/icons/type=number_markers.png
AssetGenImage get typeNumberMarkers =>
const AssetGenImage('assets/icons/type=number_markers.png');
/// File path: assets/icons/type=paste.png
AssetGenImage get typePaste =>
const AssetGenImage('assets/icons/type=paste.png');
/// File path: assets/icons/type=photo.png
AssetGenImage get typePhoto =>
const AssetGenImage('assets/icons/type=photo.png');
/// File path: assets/icons/type=plus.png
AssetGenImage get typePlus =>
const AssetGenImage('assets/icons/type=plus.png');
/// File path: assets/icons/type=public.png
AssetGenImage get typePublic =>
const AssetGenImage('assets/icons/type=public.png');
/// File path: assets/icons/type=remember.png
AssetGenImage get typeRemember =>
const AssetGenImage('assets/icons/type=remember.png');
/// File path: assets/icons/type=search.png
AssetGenImage get typeSearch =>
const AssetGenImage('assets/icons/type=search.png');
/// File path: assets/icons/type=setting.png
AssetGenImage get typeSetting =>
const AssetGenImage('assets/icons/type=setting.png');
/// File path: assets/icons/type=share.png
AssetGenImage get typeShare =>
const AssetGenImage('assets/icons/type=share.png');
/// File path: assets/icons/type=show.png
AssetGenImage get typeShow =>
const AssetGenImage('assets/icons/type=show.png');
/// File path: assets/icons/type=sort.png
AssetGenImage get typeSort =>
const AssetGenImage('assets/icons/type=sort.png');
/// File path: assets/icons/type=sort_A.png
AssetGenImage get typeSortA =>
const AssetGenImage('assets/icons/type=sort_A.png');
/// File path: assets/icons/type=sort_Z.png
AssetGenImage get typeSortZ =>
const AssetGenImage('assets/icons/type=sort_Z.png');
/// File path: assets/icons/type=sort_down.png
AssetGenImage get typeSortDown =>
const AssetGenImage('assets/icons/type=sort_down.png');
/// File path: assets/icons/type=sort_up.png
AssetGenImage get typeSortUp =>
const AssetGenImage('assets/icons/type=sort_up.png');
/// File path: assets/icons/type=stat.png
AssetGenImage get typeStat =>
const AssetGenImage('assets/icons/type=stat.png');
/// File path: assets/icons/type=trash.png
AssetGenImage get typeTrash =>
const AssetGenImage('assets/icons/type=trash.png');
/// List of all assets
List<AssetGenImage> get values => [
back,
call,
mail,
typeArrowDown,
typeBack,
typeBold,
typeCapsLock,
typeCards,
typeCheck,
typeCheckRound,
typeCheckboxEmpty,
typeCheckboxFill,
typeClose,
typeCollection,
typeCopy,
typeCreateCard,
typeDanger,
typeDescription,
typeDownload,
typeEdit,
typeFlip,
typeFlip2,
typeHeading,
typeHide,
typeImg,
typeInFocus,
typeInfo,
typeLearn,
typeLike,
typeLike1818,
typeLikeFill,
typeMarkers,
typeMenuVertical,
typeMinus,
typeMove,
typeNumberMarkers,
typePaste,
typePhoto,
typePlus,
typePublic,
typeRemember,
typeSearch,
typeSetting,
typeShare,
typeShow,
typeSort,
typeSortA,
typeSortZ,
typeSortDown,
typeSortUp,
typeStat,
typeTrash,
];
}
class $AssetsImagesGen {
const $AssetsImagesGen();
/// File path: assets/images/.gitkeep
String get aGitkeep => 'assets/images/.gitkeep';
/// File path: assets/images/Property Apple.png
AssetGenImage get propertyApple =>
const AssetGenImage('assets/images/Property Apple.png');
/// File path: assets/images/Property Google disk.png
AssetGenImage get propertyGoogleDisk =>
const AssetGenImage('assets/images/Property Google disk.png');
/// File path: assets/images/Property Google.png
AssetGenImage get propertyGoogle =>
const AssetGenImage('assets/images/Property Google.png');
/// File path: assets/images/Property Mail.ru.png
AssetGenImage get propertyMailRu =>
const AssetGenImage('assets/images/Property Mail.ru.png');
/// File path: assets/images/Property TG.png
AssetGenImage get propertyTG =>
const AssetGenImage('assets/images/Property TG.png');
/// File path: assets/images/Property VK.png
AssetGenImage get propertyVK =>
const AssetGenImage('assets/images/Property VK.png');
/// File path: assets/images/Property YA.png
AssetGenImage get propertyYA =>
const AssetGenImage('assets/images/Property YA.png');
/// File path: assets/images/img.png
AssetGenImage get img => const AssetGenImage('assets/images/img.png');
/// File path: assets/images/img_card.png
AssetGenImage get imgCard =>
const AssetGenImage('assets/images/img_card.png');
/// File path: assets/images/logo.png
AssetGenImage get logo => const AssetGenImage('assets/images/logo.png');
/// File path: assets/images/no_data.png
AssetGenImage get noData => const AssetGenImage('assets/images/no_data.png');
/// File path: assets/images/quote.png
AssetGenImage get quote => const AssetGenImage('assets/images/quote.png');
/// List of all assets
List<dynamic> get values => [
aGitkeep,
propertyApple,
propertyGoogleDisk,
propertyGoogle,
propertyMailRu,
propertyTG,
propertyVK,
propertyYA,
img,
imgCard,
logo,
noData,
quote,
];
}
class $AssetsSvgGen {
const $AssetsSvgGen();
/// File path: assets/svg/.gitkeep
String get aGitkeep => 'assets/svg/.gitkeep';
/// List of all assets
List<String> get values => [aGitkeep];
}
class Assets {
const Assets._();
static const $AssetsIconsGen icons = $AssetsIconsGen();
static const $AssetsImagesGen images = $AssetsImagesGen();
static const $AssetsSvgGen svg = $AssetsSvgGen();
}
class AssetGenImage {
const AssetGenImage(this._assetName, {this.size, this.flavors = const {}});
final String _assetName;
final Size? size;
final Set<String> flavors;
Image image({
Key? key,
AssetBundle? bundle,
ImageFrameBuilder? frameBuilder,
ImageErrorWidgetBuilder? errorBuilder,
String? semanticLabel,
bool excludeFromSemantics = false,
double? scale,
double? width,
double? height,
Color? color,
Animation<double>? opacity,
BlendMode? colorBlendMode,
BoxFit? fit,
AlignmentGeometry alignment = Alignment.center,
ImageRepeat repeat = ImageRepeat.noRepeat,
Rect? centerSlice,
bool matchTextDirection = false,
bool gaplessPlayback = true,
bool isAntiAlias = false,
String? package,
FilterQuality filterQuality = FilterQuality.medium,
int? cacheWidth,
int? cacheHeight,
}) {
return Image.asset(
_assetName,
key: key,
bundle: bundle,
frameBuilder: frameBuilder,
errorBuilder: errorBuilder,
semanticLabel: semanticLabel,
excludeFromSemantics: excludeFromSemantics,
scale: scale,
width: width,
height: height,
color: color,
opacity: opacity,
colorBlendMode: colorBlendMode,
fit: fit,
alignment: alignment,
repeat: repeat,
centerSlice: centerSlice,
matchTextDirection: matchTextDirection,
gaplessPlayback: gaplessPlayback,
isAntiAlias: isAntiAlias,
package: package,
filterQuality: filterQuality,
cacheWidth: cacheWidth,
cacheHeight: cacheHeight,
);
}
ImageProvider provider({AssetBundle? bundle, String? package}) {
return AssetImage(_assetName, bundle: bundle, package: package);
}
String get path => _assetName;
String get keyName => _assetName;
}

View File

@@ -0,0 +1,15 @@
// Package imports:
import 'package:hive_ce_flutter/hive_flutter.dart';
///
/// Вспомогательный класс для открытия [Hive] хранилищ
///
class HiveCreator<T> {
///
/// Открывает [Hive] хранилище с типом [T]
///
Future<Box<T>> open(String name, [HiveCipher? cipher]) {
return Hive.openBox<T>(name, encryptionCipher: cipher);
}
}

26
lib/i18n/en.i18n.json Normal file
View File

@@ -0,0 +1,26 @@
{
"app_name": "Tesmit",
"home_screen": {
"title" : "Home screen"
},
"settings_screen": {
"common": "Common",
"title" : "Settings",
"lang": "Language",
"lang_selection": "Language selection",
"theme": {
"dark_theme" : "Dark theme",
"light_theme" : "Light theme"
}
},
"auth_screen": {
"title": "Authorization"
},
"gal_screen": {
"title": "Gallery"
}
}

26
lib/i18n/ru.i18n.json Normal file
View File

@@ -0,0 +1,26 @@
{
"app_name": "Tesmit",
"home_screen": {
"title" : "Главная"
},
"settings_screen": {
"common": "Общее",
"title" : "Настройки",
"lang": "Язык",
"lang_selection": "Выбор языка",
"theme": {
"dark_theme" : "Темная тема",
"light_theme" : "Светлая тема"
}
},
"auth_screen": {
"title": "Авторизация"
},
"gal_screen": {
"title": "Галерея Краснодар"
}
}

182
lib/i18n/strings.g.dart Normal file
View File

@@ -0,0 +1,182 @@
/// Generated file. Do not edit.
///
/// Source: lib/i18n
/// To regenerate, run: `dart run slang`
///
/// Locales: 2
/// Strings: 20 (10 per locale)
///
/// Built on 2025-02-15 at 13:48 UTC
// coverage:ignore-file
// ignore_for_file: type=lint, unused_import
import 'package:flutter/widgets.dart';
import 'package:intl/intl.dart';
import 'package:slang/generated.dart';
import 'package:slang_flutter/slang_flutter.dart';
export 'package:slang_flutter/slang_flutter.dart';
import 'strings_en.g.dart' deferred as l_en;
part 'strings_ru.g.dart';
/// Supported locales.
///
/// Usage:
/// - LocaleSettings.setLocale(AppLocale.ru) // set locale
/// - Locale locale = AppLocale.ru.flutterLocale // get flutter locale from enum
/// - if (LocaleSettings.currentLocale == AppLocale.ru) // locale check
enum AppLocale with BaseAppLocale<AppLocale, Translations> {
ru(languageCode: 'ru'),
en(languageCode: 'en');
const AppLocale({
required this.languageCode,
this.scriptCode, // ignore: unused_element
this.countryCode, // ignore: unused_element
});
@override final String languageCode;
@override final String? scriptCode;
@override final String? countryCode;
@override
Future<Translations> build({
Map<String, Node>? overrides,
PluralResolver? cardinalResolver,
PluralResolver? ordinalResolver,
}) async {
switch (this) {
case AppLocale.ru:
return TranslationsRu(
overrides: overrides,
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
case AppLocale.en:
await l_en.loadLibrary();
return l_en.TranslationsEn(
overrides: overrides,
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
}
}
@override
Translations buildSync({
Map<String, Node>? overrides,
PluralResolver? cardinalResolver,
PluralResolver? ordinalResolver,
}) {
switch (this) {
case AppLocale.ru:
return TranslationsRu(
overrides: overrides,
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
case AppLocale.en:
return l_en.TranslationsEn(
overrides: overrides,
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
}
}
/// Gets current instance managed by [LocaleSettings].
Translations get translations => LocaleSettings.instance.getTranslations(this);
}
/// Method A: Simple
///
/// No rebuild after locale change.
/// Translation happens during initialization of the widget (call of t).
/// Configurable via 'translate_var'.
///
/// Usage:
/// String a = t.someKey.anotherKey;
/// String b = t['someKey.anotherKey']; // Only for edge cases!
Translations get t => LocaleSettings.instance.currentTranslations;
/// Method B: Advanced
///
/// All widgets using this method will trigger a rebuild when locale changes.
/// Use this if you have e.g. a settings page where the user can select the locale during runtime.
///
/// Step 1:
/// wrap your App with
/// TranslationProvider(
/// child: MyApp()
/// );
///
/// Step 2:
/// final t = Translations.of(context); // Get t variable.
/// String a = t.someKey.anotherKey; // Use t variable.
/// String b = t['someKey.anotherKey']; // Only for edge cases!
class TranslationProvider extends BaseTranslationProvider<AppLocale, Translations> {
TranslationProvider({required super.child}) : super(settings: LocaleSettings.instance);
static InheritedLocaleData<AppLocale, Translations> of(BuildContext context) => InheritedLocaleData.of<AppLocale, Translations>(context);
}
/// Method B shorthand via [BuildContext] extension method.
/// Configurable via 'translate_var'.
///
/// Usage (e.g. in a widget's build method):
/// context.t.someKey.anotherKey
extension BuildContextTranslationsExtension on BuildContext {
Translations get t => TranslationProvider.of(this).translations;
}
/// Manages all translation instances and the current locale
class LocaleSettings extends BaseFlutterLocaleSettings<AppLocale, Translations> {
LocaleSettings._() : super(
utils: AppLocaleUtils.instance,
lazy: true,
);
static final instance = LocaleSettings._();
// static aliases (checkout base methods for documentation)
static AppLocale get currentLocale => instance.currentLocale;
static Stream<AppLocale> getLocaleStream() => instance.getLocaleStream();
static Future<AppLocale> setLocale(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocale(locale, listenToDeviceLocale: listenToDeviceLocale);
static Future<AppLocale> setLocaleRaw(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRaw(rawLocale, listenToDeviceLocale: listenToDeviceLocale);
static Future<AppLocale> useDeviceLocale() => instance.useDeviceLocale();
static Future<void> setPluralResolver({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolver(
language: language,
locale: locale,
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
// synchronous versions
static AppLocale setLocaleSync(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocaleSync(locale, listenToDeviceLocale: listenToDeviceLocale);
static AppLocale setLocaleRawSync(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRawSync(rawLocale, listenToDeviceLocale: listenToDeviceLocale);
static AppLocale useDeviceLocaleSync() => instance.useDeviceLocaleSync();
static void setPluralResolverSync({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolverSync(
language: language,
locale: locale,
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
}
/// Provides utility functions without any side effects.
class AppLocaleUtils extends BaseAppLocaleUtils<AppLocale, Translations> {
AppLocaleUtils._() : super(
baseLocale: AppLocale.ru,
locales: AppLocale.values,
);
static final instance = AppLocaleUtils._();
// static aliases (checkout base methods for documentation)
static AppLocale parse(String rawLocale) => instance.parse(rawLocale);
static AppLocale parseLocaleParts({required String languageCode, String? scriptCode, String? countryCode}) => instance.parseLocaleParts(languageCode: languageCode, scriptCode: scriptCode, countryCode: countryCode);
static AppLocale findDeviceLocale() => instance.findDeviceLocale();
static List<Locale> get supportedLocales => instance.supportedLocales;
static List<String> get supportedLocalesRaw => instance.supportedLocalesRaw;
}

119
lib/i18n/strings_en.g.dart Normal file
View File

@@ -0,0 +1,119 @@
///
/// Generated file. Do not edit.
///
// coverage:ignore-file
// ignore_for_file: type=lint, unused_import
import 'package:flutter/widgets.dart';
import 'package:intl/intl.dart';
import 'package:slang/generated.dart';
import 'strings.g.dart';
// Path: <root>
class TranslationsEn extends Translations {
/// You can call this constructor and build your own translation instance of this locale.
/// Constructing via the enum [AppLocale.build] is preferred.
TranslationsEn({Map<String, Node>? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver})
: assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'),
$meta = TranslationMetadata(
locale: AppLocale.en,
overrides: overrides ?? {},
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
),
super(cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver) {
super.$meta.setFlatMapFunction($meta.getTranslation); // copy base translations to super.$meta
$meta.setFlatMapFunction(_flatMapFunction);
}
/// Metadata for the translations of <en>.
@override final TranslationMetadata<AppLocale, Translations> $meta;
/// Access flat map
@override dynamic operator[](String key) => $meta.getTranslation(key) ?? super.$meta.getTranslation(key);
late final TranslationsEn _root = this; // ignore: unused_field
// Translations
@override String get app_name => 'Tesmit';
@override late final _TranslationsHomeScreenEn home_screen = _TranslationsHomeScreenEn._(_root);
@override late final _TranslationsSettingsScreenEn settings_screen = _TranslationsSettingsScreenEn._(_root);
@override late final _TranslationsAuthScreenEn auth_screen = _TranslationsAuthScreenEn._(_root);
@override late final _TranslationsGalScreenEn gal_screen = _TranslationsGalScreenEn._(_root);
}
// Path: home_screen
class _TranslationsHomeScreenEn extends TranslationsHomeScreenRu {
_TranslationsHomeScreenEn._(TranslationsEn root) : this._root = root, super.internal(root);
final TranslationsEn _root; // ignore: unused_field
// Translations
@override String get title => 'Home screen';
}
// Path: settings_screen
class _TranslationsSettingsScreenEn extends TranslationsSettingsScreenRu {
_TranslationsSettingsScreenEn._(TranslationsEn root) : this._root = root, super.internal(root);
final TranslationsEn _root; // ignore: unused_field
// Translations
@override String get common => 'Common';
@override String get title => 'Settings';
@override String get lang => 'Language';
@override String get lang_selection => 'Language selection';
@override late final _TranslationsSettingsScreenThemeEn theme = _TranslationsSettingsScreenThemeEn._(_root);
}
// Path: auth_screen
class _TranslationsAuthScreenEn extends TranslationsAuthScreenRu {
_TranslationsAuthScreenEn._(TranslationsEn root) : this._root = root, super.internal(root);
final TranslationsEn _root; // ignore: unused_field
// Translations
@override String get title => 'Authorization';
}
// Path: gal_screen
class _TranslationsGalScreenEn extends TranslationsGalScreenRu {
_TranslationsGalScreenEn._(TranslationsEn root) : this._root = root, super.internal(root);
final TranslationsEn _root; // ignore: unused_field
// Translations
@override String get title => 'Gallery';
}
// Path: settings_screen.theme
class _TranslationsSettingsScreenThemeEn extends TranslationsSettingsScreenThemeRu {
_TranslationsSettingsScreenThemeEn._(TranslationsEn root) : this._root = root, super.internal(root);
final TranslationsEn _root; // ignore: unused_field
// Translations
@override String get dark_theme => 'Dark theme';
@override String get light_theme => 'Light theme';
}
/// Flat map(s) containing all translations.
/// Only for edge cases! For simple maps, use the map function of this library.
extension on TranslationsEn {
dynamic _flatMapFunction(String path) {
switch (path) {
case 'app_name': return 'Tesmit';
case 'home_screen.title': return 'Home screen';
case 'settings_screen.common': return 'Common';
case 'settings_screen.title': return 'Settings';
case 'settings_screen.lang': return 'Language';
case 'settings_screen.lang_selection': return 'Language selection';
case 'settings_screen.theme.dark_theme': return 'Dark theme';
case 'settings_screen.theme.light_theme': return 'Light theme';
case 'auth_screen.title': return 'Authorization';
case 'gal_screen.title': return 'Gallery';
default: return null;
}
}
}

121
lib/i18n/strings_ru.g.dart Normal file
View File

@@ -0,0 +1,121 @@
///
/// Generated file. Do not edit.
///
// coverage:ignore-file
// ignore_for_file: type=lint, unused_import
part of 'strings.g.dart';
// Path: <root>
typedef TranslationsRu = Translations; // ignore: unused_element
class Translations implements BaseTranslations<AppLocale, Translations> {
/// Returns the current translations of the given [context].
///
/// Usage:
/// final t = Translations.of(context);
static Translations of(BuildContext context) => InheritedLocaleData.of<AppLocale, Translations>(context).translations;
/// You can call this constructor and build your own translation instance of this locale.
/// Constructing via the enum [AppLocale.build] is preferred.
Translations({Map<String, Node>? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver})
: assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'),
$meta = TranslationMetadata(
locale: AppLocale.ru,
overrides: overrides ?? {},
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
) {
$meta.setFlatMapFunction(_flatMapFunction);
}
/// Metadata for the translations of <ru>.
@override final TranslationMetadata<AppLocale, Translations> $meta;
/// Access flat map
dynamic operator[](String key) => $meta.getTranslation(key);
late final Translations _root = this; // ignore: unused_field
// Translations
String get app_name => 'Tesmit';
late final TranslationsHomeScreenRu home_screen = TranslationsHomeScreenRu.internal(_root);
late final TranslationsSettingsScreenRu settings_screen = TranslationsSettingsScreenRu.internal(_root);
late final TranslationsAuthScreenRu auth_screen = TranslationsAuthScreenRu.internal(_root);
late final TranslationsGalScreenRu gal_screen = TranslationsGalScreenRu.internal(_root);
}
// Path: home_screen
class TranslationsHomeScreenRu {
TranslationsHomeScreenRu.internal(this._root);
final Translations _root; // ignore: unused_field
// Translations
String get title => 'Главная';
}
// Path: settings_screen
class TranslationsSettingsScreenRu {
TranslationsSettingsScreenRu.internal(this._root);
final Translations _root; // ignore: unused_field
// Translations
String get common => 'Общее';
String get title => 'Настройки';
String get lang => 'Язык';
String get lang_selection => 'Выбор языка';
late final TranslationsSettingsScreenThemeRu theme = TranslationsSettingsScreenThemeRu.internal(_root);
}
// Path: auth_screen
class TranslationsAuthScreenRu {
TranslationsAuthScreenRu.internal(this._root);
final Translations _root; // ignore: unused_field
// Translations
String get title => 'Авторизация';
}
// Path: gal_screen
class TranslationsGalScreenRu {
TranslationsGalScreenRu.internal(this._root);
final Translations _root; // ignore: unused_field
// Translations
String get title => 'Галерея Краснодар';
}
// Path: settings_screen.theme
class TranslationsSettingsScreenThemeRu {
TranslationsSettingsScreenThemeRu.internal(this._root);
final Translations _root; // ignore: unused_field
// Translations
String get dark_theme => 'Темная тема';
String get light_theme => 'Светлая тема';
}
/// Flat map(s) containing all translations.
/// Only for edge cases! For simple maps, use the map function of this library.
extension on Translations {
dynamic _flatMapFunction(String path) {
switch (path) {
case 'app_name': return 'Tesmit';
case 'home_screen.title': return 'Главная';
case 'settings_screen.common': return 'Общее';
case 'settings_screen.title': return 'Настройки';
case 'settings_screen.lang': return 'Язык';
case 'settings_screen.lang_selection': return 'Выбор языка';
case 'settings_screen.theme.dark_theme': return 'Темная тема';
case 'settings_screen.theme.light_theme': return 'Светлая тема';
case 'auth_screen.title': return 'Авторизация';
case 'gal_screen.title': return 'Галерея Краснодар';
default: return null;
}
}
}

43
lib/inject.config.dart Normal file
View File

@@ -0,0 +1,43 @@
// dart format width=80
// GENERATED CODE - DO NOT MODIFY BY HAND
// **************************************************************************
// InjectableConfigGenerator
// **************************************************************************
// ignore_for_file: type=lint
// coverage:ignore-file
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:get_it/get_it.dart' as _i174;
import 'package:injectable/injectable.dart' as _i526;
import 'services/auth_interface.dart' as _i78;
import 'services/auth_service.dart' as _i706;
import 'services/core/enc_keys_service.dart' as _i439;
import 'services/core/lang_service.dart' as _i68;
import 'services/core/theme_service.dart' as _i84;
import 'services/warmup_service.dart' as _i564;
extension GetItInjectableX on _i174.GetIt {
// initializes the registration of main-scope dependencies inside of GetIt
Future<_i174.GetIt> $initGetIt({
String? environment,
_i526.EnvironmentFilter? environmentFilter,
}) async {
final gh = _i526.GetItHelper(this, environment, environmentFilter);
gh.factory<_i68.LangService>(() => _i68.LangService());
gh.factory<_i439.EncKeysService>(() => _i439.EncKeysService());
gh.factory<_i84.ThemeService>(() => _i84.ThemeService());
gh.singleton<_i78.AuthInterface>(() => _i706.AuthService());
await gh.singletonAsync<_i564.WarmupService>(() {
final i = _i564.WarmupService(
gh<_i84.ThemeService>(),
gh<_i68.LangService>(),
gh<_i439.EncKeysService>(),
);
return i.common().then((_) => i);
}, preResolve: true);
return this;
}
}

11
lib/inject.dart Normal file
View File

@@ -0,0 +1,11 @@
// Package imports:
import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart';
// Project imports:
import '../inject.config.dart';
final GetIt getIt = GetIt.instance;
@InjectableInit(initializerName: r'$initGetIt', preferRelativeImports: true)
Future<void> configureDependencies() async => getIt.$initGetIt();

View File

@@ -0,0 +1,9 @@
///
/// Интерфейс сервиса, который должен быть "прогрет"
///
abstract class IWarmupService {
///
/// Функция для прогрева
///
Future<void> init();
}

52
lib/main.dart Normal file
View File

@@ -0,0 +1,52 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:hive_ce_flutter/hive_flutter.dart';
import 'package:remever/app.dart';
import 'package:remever/env.dart';
import 'package:remever/i18n/strings.g.dart';
import 'package:remever/inject.dart';
import 'package:remever/router.dart';
import 'package:remever/services/warmup_service.dart';
void _onError(Object error, StackTrace trace) {
debugPrint('error ${error.toString()}');
}
void _flutterError(FlutterErrorDetails details) {
debugPrint('error ${details.context}');
}
bool _platformDispatcher(Object exception, StackTrace stackTrace) {
if ('$exception'.toLowerCase().contains('dio')) return true;
debugPrint('error ${exception.toString()}');
return true;
}
void main() {
runZonedGuarded(() async {
WidgetsFlutterBinding.ensureInitialized();
await Hive.initFlutter();
await configureDependencies();
configureEnv();
if (!kIsWeb) {
await ScreenUtil.ensureScreenSize();
}
FlutterError.onError = _flutterError;
PlatformDispatcher.instance.onError = _platformDispatcher;
getIt.registerSingleton<AppRouter>(AppRouter());
await getIt<WarmupService>().firstStart();
runApp(TranslationProvider(child: const MyApp()));
}, _onError);
}

View File

@@ -0,0 +1,29 @@
import 'package:hive_ce_flutter/hive_flutter.dart';
import 'package:remever/i18n/strings.g.dart';
class AppLocaleAdapter extends TypeAdapter<AppLocale> {
@override
int get typeId => 101;
@override
AppLocale read(BinaryReader reader) {
final int tm = reader.readInt();
return AppLocale.values.elementAt(tm);
}
@override
void write(BinaryWriter writer, AppLocale obj) {
writer.writeInt(obj.index);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is AppLocaleAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,30 @@
// Flutter imports:
import 'package:flutter/material.dart' show ThemeMode;
import 'package:hive_ce_flutter/hive_flutter.dart';
class ThemeModeAdapter extends TypeAdapter<ThemeMode> {
@override
int get typeId => 100;
@override
ThemeMode read(BinaryReader reader) {
final int tm = reader.readInt();
return ThemeMode.values.elementAt(tm);
}
@override
void write(BinaryWriter writer, ThemeMode obj) {
writer.writeInt(obj.index);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ThemeModeAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,65 @@
import 'package:remever/common/typedef.dart';
enum LogLevel {
ERROR,
INFO,
SUCCESS,
DEBUG,
WARNING;
String get errName {
switch (this) {
case LogLevel.ERROR:
return 'ERROR';
case LogLevel.INFO:
return 'INFO';
case LogLevel.SUCCESS:
return 'SUCCESS';
case LogLevel.DEBUG:
return 'DEBUG';
case LogLevel.WARNING:
return 'WARNING';
}
}
}
class LogEntity {
LogEntity({
required this.level,
required this.message,
this.context = const <String, dynamic>{},
});
LogEntity.error({
required this.message,
this.context = const <String, dynamic>{},
}) : level = LogLevel.ERROR;
LogEntity.info({
required this.message,
this.context = const <String, dynamic>{},
}) : level = LogLevel.INFO;
LogEntity.success({
required this.message,
this.context = const <String, dynamic>{},
}) : level = LogLevel.SUCCESS;
LogEntity.debug({
required this.message,
this.context = const <String, dynamic>{},
}) : level = LogLevel.DEBUG;
LogEntity.warning({
required this.message,
this.context = const <String, dynamic>{},
}) : level = LogLevel.WARNING;
final LogLevel level;
final String message;
final Json context;
}

33
lib/router.dart Normal file
View File

@@ -0,0 +1,33 @@
// Package imports:
import 'package:auto_route/auto_route.dart';
import 'package:remever/router.gr.dart';
@AutoRouterConfig(replaceInRouteName: 'Screen|Page,Route')
class AppRouter extends RootStackRouter {
@override
RouteType get defaultRouteType => const RouteType.material(); //.cupertino, .adaptive ..etc
@override
List<AutoRoute> get routes => <AutoRoute>[
AutoRoute(path: '/', page: SplashRoute.page),
AutoRoute(path: '/auth', page: AuthRoute.page),
AutoRoute(
path: '/home',
page: HomeRoute.page,
children: <AutoRoute>[
AutoRoute(path: 'settings', page: SettingsRoute.page),
AutoRoute(path: 'statistick', page: StatistickRoute.page),
AutoRoute(path: 'crud_collection', page: CrudCollection.page),
AutoRoute(path: 'collections', page: CollectionRoute.page),
],
),
// AutoRoute(path: '/home', page: HomeRoute.page),
// AutoRoute(path: '/logs', page: LogRoute.page),
// AutoRoute(path: '/sandbox', page: SandboxRoute.page),
];
@override
List<AutoRouteGuard> get guards => <AutoRouteGuard>[
// optionally add root guards here
];
}

150
lib/router.gr.dart Normal file
View File

@@ -0,0 +1,150 @@
// dart format width=80
// GENERATED CODE - DO NOT MODIFY BY HAND
// **************************************************************************
// AutoRouterGenerator
// **************************************************************************
// ignore_for_file: type=lint
// coverage:ignore-file
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:auto_route/auto_route.dart' as _i8;
import 'package:flutter/material.dart' as _i9;
import 'package:remever/screens/auth/auth_screen.dart' as _i1;
import 'package:remever/screens/collections/collections_screen.dart' as _i2;
import 'package:remever/screens/crud_collection/crud_collection.dart' as _i3;
import 'package:remever/screens/home/home_screen.dart' as _i4;
import 'package:remever/screens/settings/settings_screen.dart' as _i5;
import 'package:remever/screens/splash/splash_screen.dart' as _i6;
import 'package:remever/screens/statistick/statistick_screen.dart' as _i7;
/// generated route for
/// [_i1.AuthScreen]
class AuthRoute extends _i8.PageRouteInfo<void> {
const AuthRoute({List<_i8.PageRouteInfo>? children})
: super(AuthRoute.name, initialChildren: children);
static const String name = 'AuthRoute';
static _i8.PageInfo page = _i8.PageInfo(
name,
builder: (data) {
return const _i1.AuthScreen();
},
);
}
/// generated route for
/// [_i2.CollectionScreen]
class CollectionRoute extends _i8.PageRouteInfo<CollectionRouteArgs> {
CollectionRoute({_i9.Key? key, List<_i8.PageRouteInfo>? children})
: super(
CollectionRoute.name,
args: CollectionRouteArgs(key: key),
initialChildren: children,
);
static const String name = 'CollectionRoute';
static _i8.PageInfo page = _i8.PageInfo(
name,
builder: (data) {
final args = data.argsAs<CollectionRouteArgs>(
orElse: () => const CollectionRouteArgs(),
);
return _i2.CollectionScreen(key: args.key);
},
);
}
class CollectionRouteArgs {
const CollectionRouteArgs({this.key});
final _i9.Key? key;
@override
String toString() {
return 'CollectionRouteArgs{key: $key}';
}
}
/// generated route for
/// [_i3.CrudCollection]
class CrudCollection extends _i8.PageRouteInfo<void> {
const CrudCollection({List<_i8.PageRouteInfo>? children})
: super(CrudCollection.name, initialChildren: children);
static const String name = 'CrudCollection';
static _i8.PageInfo page = _i8.PageInfo(
name,
builder: (data) {
return const _i3.CrudCollection();
},
);
}
/// generated route for
/// [_i4.HomeScreen]
class HomeRoute extends _i8.PageRouteInfo<void> {
const HomeRoute({List<_i8.PageRouteInfo>? children})
: super(HomeRoute.name, initialChildren: children);
static const String name = 'HomeRoute';
static _i8.PageInfo page = _i8.PageInfo(
name,
builder: (data) {
return const _i4.HomeScreen();
},
);
}
/// generated route for
/// [_i5.SettingsScreen]
class SettingsRoute extends _i8.PageRouteInfo<void> {
const SettingsRoute({List<_i8.PageRouteInfo>? children})
: super(SettingsRoute.name, initialChildren: children);
static const String name = 'SettingsRoute';
static _i8.PageInfo page = _i8.PageInfo(
name,
builder: (data) {
return const _i5.SettingsScreen();
},
);
}
/// generated route for
/// [_i6.SplashScreen]
class SplashRoute extends _i8.PageRouteInfo<void> {
const SplashRoute({List<_i8.PageRouteInfo>? children})
: super(SplashRoute.name, initialChildren: children);
static const String name = 'SplashRoute';
static _i8.PageInfo page = _i8.PageInfo(
name,
builder: (data) {
return const _i6.SplashScreen();
},
);
}
/// generated route for
/// [_i7.StatistickScreen]
class StatistickRoute extends _i8.PageRouteInfo<void> {
const StatistickRoute({List<_i8.PageRouteInfo>? children})
: super(StatistickRoute.name, initialChildren: children);
static const String name = 'StatistickRoute';
static _i8.PageInfo page = _i8.PageInfo(
name,
builder: (data) {
return const _i7.StatistickScreen();
},
);
}

View File

@@ -0,0 +1,43 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:remever/common/resources.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:remever/screens/auth/cubit/auth_cubit.dart';
import 'package:remever/screens/auth/screens/code_auth.dart';
import 'package:remever/screens/auth/screens/email_auth.dart';
import 'package:remever/screens/auth/screens/initial_auth.dart';
@RoutePage()
class AuthScreen extends StatelessWidget {
const AuthScreen({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider<AuthCubit>(
create: (BuildContext context) => AuthCubit(),
child: SafeArea(
child: Scaffold(
backgroundColor: AppColors.bg,
body: SafeArea(child: _buildMain()),
),
),
);
}
///
/// Построение основного блока
///
Widget _buildMain() {
return PopScope(
canPop: false,
child: BlocBuilder<AuthCubit, AuthState>(
builder:
(BuildContext context, AuthState state) => state.when(
initial: () => InitialAuth(),
email: () => EmailAuth(),
code: (email, uuid) => CodeAuth(email: email, uuid: uuid),
),
),
);
}
}

View File

@@ -0,0 +1,46 @@
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:remever/common/functions.dart';
import 'package:remever/inject.dart';
import 'package:remever/router.gr.dart';
import 'package:remever/services/auth_interface.dart';
part 'auth_state.dart';
part 'auth_cubit.freezed.dart';
class AuthCubit extends Cubit<AuthState> {
AuthCubit() : super(AuthState.initial());
final AuthInterface _authInterface = getIt<AuthInterface>();
Future<void> toInitialState() async {
emit(AuthState.initial());
}
Future<void> toEmailState() async {
emit(AuthState.email());
}
Future<void> toCodeState(String email, String uuid) async {
emit(AuthState.code(email, uuid));
}
/// Авторизация
Future<void> login(String email) async {
final String? uuid = await _authInterface.login(email);
if (uuid == null) {
return;
}
toCodeState(email, uuid);
}
Future<void> sendCode(String code, String uid) async {
final bool res = await _authInterface.sendCode(code, uid);
if (!res) toInitialState();
globalRouter.replace(CollectionRoute());
}
}

View File

@@ -0,0 +1,458 @@
// 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 'auth_cubit.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(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#adding-getters-and-methods-to-our-models',
);
/// @nodoc
mixin _$AuthState {
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() email,
required TResult Function(String email, String uuid) code,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? email,
TResult? Function(String email, String uuid)? code,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? email,
TResult Function(String email, String uuid)? code,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_Initial value) initial,
required TResult Function(_Email value) email,
required TResult Function(_Code value) code,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_Initial value)? initial,
TResult? Function(_Email value)? email,
TResult? Function(_Code value)? code,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_Initial value)? initial,
TResult Function(_Email value)? email,
TResult Function(_Code value)? code,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $AuthStateCopyWith<$Res> {
factory $AuthStateCopyWith(AuthState value, $Res Function(AuthState) then) =
_$AuthStateCopyWithImpl<$Res, AuthState>;
}
/// @nodoc
class _$AuthStateCopyWithImpl<$Res, $Val extends AuthState>
implements $AuthStateCopyWith<$Res> {
_$AuthStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of AuthState
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
abstract class _$$InitialImplCopyWith<$Res> {
factory _$$InitialImplCopyWith(
_$InitialImpl value,
$Res Function(_$InitialImpl) then,
) = __$$InitialImplCopyWithImpl<$Res>;
}
/// @nodoc
class __$$InitialImplCopyWithImpl<$Res>
extends _$AuthStateCopyWithImpl<$Res, _$InitialImpl>
implements _$$InitialImplCopyWith<$Res> {
__$$InitialImplCopyWithImpl(
_$InitialImpl _value,
$Res Function(_$InitialImpl) _then,
) : super(_value, _then);
/// Create a copy of AuthState
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
class _$InitialImpl implements _Initial {
const _$InitialImpl();
@override
String toString() {
return 'AuthState.initial()';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType && other is _$InitialImpl);
}
@override
int get hashCode => runtimeType.hashCode;
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() email,
required TResult Function(String email, String uuid) code,
}) {
return initial();
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? email,
TResult? Function(String email, String uuid)? code,
}) {
return initial?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? email,
TResult Function(String email, String uuid)? code,
required TResult orElse(),
}) {
if (initial != null) {
return initial();
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_Initial value) initial,
required TResult Function(_Email value) email,
required TResult Function(_Code value) code,
}) {
return initial(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_Initial value)? initial,
TResult? Function(_Email value)? email,
TResult? Function(_Code value)? code,
}) {
return initial?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_Initial value)? initial,
TResult Function(_Email value)? email,
TResult Function(_Code value)? code,
required TResult orElse(),
}) {
if (initial != null) {
return initial(this);
}
return orElse();
}
}
abstract class _Initial implements AuthState {
const factory _Initial() = _$InitialImpl;
}
/// @nodoc
abstract class _$$EmailImplCopyWith<$Res> {
factory _$$EmailImplCopyWith(
_$EmailImpl value,
$Res Function(_$EmailImpl) then,
) = __$$EmailImplCopyWithImpl<$Res>;
}
/// @nodoc
class __$$EmailImplCopyWithImpl<$Res>
extends _$AuthStateCopyWithImpl<$Res, _$EmailImpl>
implements _$$EmailImplCopyWith<$Res> {
__$$EmailImplCopyWithImpl(
_$EmailImpl _value,
$Res Function(_$EmailImpl) _then,
) : super(_value, _then);
/// Create a copy of AuthState
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
class _$EmailImpl implements _Email {
const _$EmailImpl();
@override
String toString() {
return 'AuthState.email()';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType && other is _$EmailImpl);
}
@override
int get hashCode => runtimeType.hashCode;
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() email,
required TResult Function(String email, String uuid) code,
}) {
return email();
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? email,
TResult? Function(String email, String uuid)? code,
}) {
return email?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? email,
TResult Function(String email, String uuid)? code,
required TResult orElse(),
}) {
if (email != null) {
return email();
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_Initial value) initial,
required TResult Function(_Email value) email,
required TResult Function(_Code value) code,
}) {
return email(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_Initial value)? initial,
TResult? Function(_Email value)? email,
TResult? Function(_Code value)? code,
}) {
return email?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_Initial value)? initial,
TResult Function(_Email value)? email,
TResult Function(_Code value)? code,
required TResult orElse(),
}) {
if (email != null) {
return email(this);
}
return orElse();
}
}
abstract class _Email implements AuthState {
const factory _Email() = _$EmailImpl;
}
/// @nodoc
abstract class _$$CodeImplCopyWith<$Res> {
factory _$$CodeImplCopyWith(
_$CodeImpl value,
$Res Function(_$CodeImpl) then,
) = __$$CodeImplCopyWithImpl<$Res>;
@useResult
$Res call({String email, String uuid});
}
/// @nodoc
class __$$CodeImplCopyWithImpl<$Res>
extends _$AuthStateCopyWithImpl<$Res, _$CodeImpl>
implements _$$CodeImplCopyWith<$Res> {
__$$CodeImplCopyWithImpl(_$CodeImpl _value, $Res Function(_$CodeImpl) _then)
: super(_value, _then);
/// Create a copy of AuthState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({Object? email = null, Object? uuid = null}) {
return _then(
_$CodeImpl(
null == email
? _value.email
: email // ignore: cast_nullable_to_non_nullable
as String,
null == uuid
? _value.uuid
: uuid // ignore: cast_nullable_to_non_nullable
as String,
),
);
}
}
/// @nodoc
class _$CodeImpl implements _Code {
const _$CodeImpl(this.email, this.uuid);
@override
final String email;
@override
final String uuid;
@override
String toString() {
return 'AuthState.code(email: $email, uuid: $uuid)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$CodeImpl &&
(identical(other.email, email) || other.email == email) &&
(identical(other.uuid, uuid) || other.uuid == uuid));
}
@override
int get hashCode => Object.hash(runtimeType, email, uuid);
/// Create a copy of AuthState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$CodeImplCopyWith<_$CodeImpl> get copyWith =>
__$$CodeImplCopyWithImpl<_$CodeImpl>(this, _$identity);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() email,
required TResult Function(String email, String uuid) code,
}) {
return code(this.email, uuid);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? email,
TResult? Function(String email, String uuid)? code,
}) {
return code?.call(this.email, uuid);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? email,
TResult Function(String email, String uuid)? code,
required TResult orElse(),
}) {
if (code != null) {
return code(this.email, uuid);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_Initial value) initial,
required TResult Function(_Email value) email,
required TResult Function(_Code value) code,
}) {
return code(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_Initial value)? initial,
TResult? Function(_Email value)? email,
TResult? Function(_Code value)? code,
}) {
return code?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_Initial value)? initial,
TResult Function(_Email value)? email,
TResult Function(_Code value)? code,
required TResult orElse(),
}) {
if (code != null) {
return code(this);
}
return orElse();
}
}
abstract class _Code implements AuthState {
const factory _Code(final String email, final String uuid) = _$CodeImpl;
String get email;
String get uuid;
/// Create a copy of AuthState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
_$$CodeImplCopyWith<_$CodeImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,8 @@
part of 'auth_cubit.dart';
@freezed
class AuthState with _$AuthState {
const factory AuthState.initial() = _Initial;
const factory AuthState.email() = _Email;
const factory AuthState.code(String email, String uuid) = _Code;
}

View File

@@ -0,0 +1,139 @@
// Flutter imports:
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// Project imports:
import 'package:remever/common/resources.dart';
import 'package:remever/common/widgets/loose_focus.dart';
import 'package:remever/common/widgets/typography.dart';
import 'package:remever/common/widgets/wspace.dart';
import 'package:remever/components/extensions/context.dart';
import 'package:remever/gen/assets.gen.dart';
import 'package:remever/screens/auth/cubit/auth_cubit.dart';
import 'package:remever/screens/auth/widgets/pin_code.dart';
import 'package:remever/screens/auth/widgets/timer.dart';
import 'package:remever/widgets/primary_button.dart';
///
/// Отрисовка экрана ввода кода подтверждения
///
class CodeAuth extends StatefulWidget {
const CodeAuth({super.key, required this.email, required this.uuid});
final String email;
final String uuid;
@override
CodeAuthState createState() => CodeAuthState();
}
class CodeAuthState extends State<CodeAuth> {
final TextEditingController _pinController = TextEditingController();
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
// Константы для стилей и отступов
static final EdgeInsetsGeometry _padding = EdgeInsets.all(16).r;
static final _regular14 = Regular14px();
static final _medium14 = Medium14px();
void _onEnterTap() async {
if (!_formKey.currentState!.validate()) return;
await context.read<AuthCubit>().sendCode(_pinController.text, widget.uuid);
}
@override
Widget build(BuildContext context) {
return LooseFocus(
child: Scaffold(
backgroundColor: AppColors.bg,
body: Padding(
padding: _padding,
child: SingleChildScrollView(
physics: BouncingScrollPhysics(),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
HSpace(148),
_buildLogo(),
const HSpace(60),
_buildEmailField(),
const HSpace(24),
_buildInstructions(),
HSpace(16),
_buildPinCode(),
HSpace(12),
_buildResendTimer(),
HSpace(50),
_buildLoginButton(),
const HSpace(104),
],
),
),
),
),
);
}
Widget _buildLogo() {
return Assets.images.logo.image(width: 308.w, height: 100.h);
}
Widget _buildEmailField() {
return Container(
height: 48.h,
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.all(Radius.circular(8)).r,
),
child: Padding(
padding: const EdgeInsets.all(12).r,
child: Row(
children: <Widget>[
AppTypography(widget.email, type: _medium14),
Spacer(),
_buildCloseButton(),
],
),
),
);
}
Widget _buildCloseButton() {
return InkWell(
onTap: () {
// Добавьте логику для смены e-mail или закрытия экрана
},
child: SizedBox.square(
dimension: 24.r,
child: DecoratedBox(
decoration: BoxDecoration(
color: AppColors.bg,
shape: BoxShape.circle,
),
child: Icon(Icons.close, color: AppColors.disabled, size: 16.r),
),
),
);
}
Widget _buildInstructions() {
return AppTypography(
'Ниже введите код, который получили на e-mail',
type: _regular14,
);
}
Widget _buildPinCode() {
return Form(key: _formKey, child: PinCode(pinController: _pinController));
}
Widget _buildResendTimer() {
return ResendTimer(onTap: () async {});
}
Widget _buildLoginButton() {
return PrimaryButton(
onTap: _onEnterTap,
child: AppTypography('Войти', type: _medium14, color: AppColors.white),
);
}
}

View File

@@ -0,0 +1,99 @@
// Flutter imports:
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// Project imports:
import 'package:remever/common/resources.dart';
import 'package:remever/common/widgets/typography.dart';
import 'package:remever/common/widgets/wspace.dart';
import 'package:remever/components/extensions/context.dart';
import 'package:remever/gen/assets.gen.dart';
import 'package:remever/screens/auth/cubit/auth_cubit.dart';
import 'package:remever/screens/auth/widgets/auth_text_field.dart';
import 'package:remever/widgets/primary_button.dart';
///
/// Отрисовка экрана авторизации по e-mail
///
class EmailAuth extends StatefulWidget {
const EmailAuth({super.key});
@override
EmailAuthState createState() => EmailAuthState();
}
class EmailAuthState extends State<EmailAuth> {
final TextEditingController _emailController = TextEditingController();
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
// Константы для стилей и отступов
static final EdgeInsetsGeometry _padding = EdgeInsets.all(16).r;
static final _regular14 = Regular14px();
static final _medium14 = Medium14px();
@override
void dispose() {
_emailController.dispose();
super.dispose();
}
void _onEnterTap() async {
if (!_formKey.currentState!.validate()) return;
await context.read<AuthCubit>().login(_emailController.text);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.bg,
body: Padding(
padding: _padding,
child: SingleChildScrollView(
physics: BouncingScrollPhysics(),
child: Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
HSpace(148),
_buildLogo(),
const HSpace(60),
_buildEmailField(),
const HSpace(24),
_buildInstructions(),
HSpace(145),
_buildLoginButton(),
const HSpace(104),
],
),
),
),
),
);
}
Widget _buildLogo() {
return Assets.images.logo.image(width: 308.w, height: 100.h);
}
Widget _buildEmailField() {
return Form(
key: _formKey,
child: AuthTextField(email: _emailController, autofocus: true),
);
}
Widget _buildInstructions() {
return AppTypography(
'Для авторизации в приложении мы отправим код на указанный e-mail',
type: _regular14,
maxLines: 3,
);
}
Widget _buildLoginButton() {
return PrimaryButton(
onTap: _onEnterTap,
child: AppTypography('Войти', type: _medium14, color: AppColors.white),
);
}
}

View File

@@ -0,0 +1,101 @@
// Flutter imports:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// Project imports:
import 'package:remever/common/resources.dart';
import 'package:remever/common/widgets/typography.dart';
import 'package:remever/common/widgets/wspace.dart';
import 'package:remever/components/extensions/context.dart';
import 'package:remever/gen/assets.gen.dart';
import 'package:remever/screens/auth/cubit/auth_cubit.dart';
import 'package:remever/widgets/primary_button.dart';
///
/// Отрисовка инициализации авторизации
///
class InitialAuth extends StatelessWidget {
const InitialAuth({super.key});
// Константы для стилей и отступов
static final EdgeInsetsGeometry _padding = EdgeInsets.all(16).r;
static final _regular14 = Regular14px();
void _onEnterTap(BuildContext context) {
context.read<AuthCubit>().toEmailState();
}
void _onPolicyTap() {}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.bg,
body: Padding(
padding: _padding,
child: SingleChildScrollView(
physics: BouncingScrollPhysics(),
child: Expanded(
child: Column(
children: <Widget>[
HSpace(148),
_buildLogo(),
_buildQuote(),
HSpace(245),
_buildLoginButton(context),
HSpace(28),
_buildPolicyLink(),
_buildBottomSpacing(),
],
),
),
),
),
);
}
Widget _buildLogo() {
return Assets.images.logo.image(width: 308.w, height: 100.h);
}
Widget _buildQuote() {
return Column(
children: <Widget>[
HSpace(16),
Assets.images.quote.image(width: 324.w, height: 64.h),
],
);
}
Widget _buildLoginButton(BuildContext context) {
return PrimaryButton(
onTap: () => _onEnterTap(context),
child: AppTypography(
'Войти по e-mail',
type: _regular14,
color: AppColors.white,
),
);
}
Widget _buildPolicyLink() {
return InkWell(
onTap: () => _onPolicyTap(),
child: AppTypography.rich(
'Отправляя данные, Вы подтверждаете согласие с ',
type: _regular14,
color: AppColors.disabled,
children: <TextSpan>[
TextSpan(
text: 'Политикой обработки персональных данных',
style: _regular14.style.copyWith(color: AppColors.primary),
),
],
),
);
}
Widget _buildBottomSpacing() {
return HSpace(40);
}
}

View File

@@ -0,0 +1,64 @@
// Flutter imports:
import 'package:flutter/material.dart';
import 'package:remever/common/resources.dart';
import 'package:remever/common/typography.dart';
import 'package:remever/components/extensions/context.dart';
class AuthTextField extends StatelessWidget {
const AuthTextField({required this.email, super.key, this.autofocus = true});
final TextEditingController email;
final bool autofocus;
@override
Widget build(BuildContext context) {
final OutlineInputBorder border = OutlineInputBorder(
borderRadius: const BorderRadius.all(Radius.circular(8)).r,
borderSide: BorderSide(color: AppColors.gray),
);
return TextFormField(
autofocus: autofocus,
controller: email,
validator: (String? value) {
if (value == null || value == '') return 'Поле не может быть пустым';
if (!value.contains('@') || !value.contains('.')) {
return 'Неверный e-mail';
}
return null;
},
cursorColor: AppColors.primary,
decoration: InputDecoration(
filled: true,
fillColor: AppColors.white,
focusedBorder: border,
enabledBorder: border,
errorBorder: border,
focusedErrorBorder: border,
hintText: 'Введите e-mail',
hintStyle: const TextStyle(fontWeight: FontWeight.w400, height: 1.2),
errorStyle: SemiBold12px().style.copyWith(color: AppColors.red),
suffixIconConstraints: const BoxConstraints(minWidth: 0, minHeight: 0),
suffixIcon: Padding(
padding: const EdgeInsets.only(right: 12).r,
child: InkWell(
onTap: () {
email.clear();
},
child: SizedBox.square(
dimension: 24.r,
child: DecoratedBox(
decoration: BoxDecoration(
color: AppColors.bg,
shape: BoxShape.circle,
),
child: Icon(Icons.close, color: AppColors.disabled, size: 16.r),
),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,50 @@
// Flutter imports:
import 'package:flutter/material.dart';
import 'package:pin_code_fields/pin_code_fields.dart';
import 'package:remever/common/resources.dart';
import 'package:remever/components/extensions/context.dart';
class PinCode extends StatefulWidget {
const PinCode({super.key, required this.pinController});
final TextEditingController pinController;
@override
State<PinCode> createState() => _PinCodeState();
}
class _PinCodeState extends State<PinCode> {
@override
Widget build(BuildContext context) {
return PinCodeTextField(
controller: widget.pinController,
appContext: context,
length: 6,
keyboardType: TextInputType.number,
animationType: AnimationType.slide,
hapticFeedbackTypes: HapticFeedbackTypes.medium,
autoFocus: true,
enableActiveFill: true,
cursorColor: AppColors.primary,
validator: (value) {
if (value == null || value.isEmpty) return 'Поле не может быть пустым';
if (value.length < 5) return 'Слишком мало символов';
return null;
},
pinTheme: PinTheme(
fieldWidth: 48.w,
fieldHeight: 62.h,
borderRadius: BorderRadius.circular(8).r,
activeFillColor: AppColors.white,
selectedFillColor: AppColors.white,
inactiveColor: AppColors.white,
selectedColor: AppColors.white,
activeColor: AppColors.white,
shape: PinCodeFieldShape.box,
inactiveFillColor: AppColors.white,
),
onCompleted: (String value) async {},
);
}
}

View File

@@ -0,0 +1,95 @@
// Dart imports:
import 'dart:async';
// Flutter imports:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
// Package imports:
import 'package:provider/provider.dart';
import 'package:remever/common/resources.dart';
import 'package:remever/common/widgets/typography.dart';
import 'package:remever/common/widgets/w_if.dart';
import 'package:remever/common/widgets/wspace.dart';
import 'package:remever/components/extensions/context.dart';
import 'package:remever/components/extensions/state.dart';
class ResendTimer extends StatefulWidget {
const ResendTimer({required this.onTap, super.key});
final Function() onTap;
@override
State<ResendTimer> createState() => _ResendTimerState();
}
class _ResendTimerState extends State<ResendTimer> {
Timer? _timer;
int _start = 60;
void _startTimer() {
const Duration oneSec = Duration(seconds: 1);
_timer = Timer.periodic(oneSec, (Timer timer) {
if (_start > 0) {
safeSetState(() {
_start--;
});
} else {
_timer?.cancel();
}
});
}
@override
void initState() {
_startTimer();
super.initState();
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Wif(
condition: _start == 0,
builder: (BuildContext context) => _buildResend(),
fallback: (BuildContext context) => _buildTimer(),
);
}
Widget _buildResend() {
return InkWell(
onTap: () async {
await widget.onTap();
safeSetState(() => _start = 60);
_startTimer();
},
child: AppTypography(
'Получить новый код на e-mail',
type: Regular14px(),
color: AppColors.primary,
),
);
}
Widget _buildTimer() {
return Row(
children: <Widget>[
AppTypography(
'Получить новый код можно будет через: ',
type: Regular14px(),
color: AppColors.disabled,
),
AppTypography(
'$_start сек',
type: Regular14px(),
color: AppColors.disabled,
),
],
);
}
}

View File

@@ -0,0 +1,119 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it_mixin/get_it_mixin.dart';
import 'package:remever/common/resources.dart';
import 'package:remever/components/extensions/context.dart';
import 'package:remever/components/notifiers/home_screen_data.dart';
import 'package:remever/screens/collections/cubit/collection_cubit.dart';
@RoutePage()
class CollectionScreen extends StatelessWidget with GetItMixin {
CollectionScreen({super.key});
/// Флаг что надо показывать Fab
bool get _showFab {
return watchOnly<CollectionData, bool>((CollectionData d) => d.showFAB);
}
@override
Widget build(BuildContext context) {
return BlocProvider<CollectionCubit>(
create: (context) => CollectionCubit(),
child: Scaffold(
backgroundColor: AppColors.bg,
// appBar: const CollectionsAppBar(),
body: _buildMain(context),
floatingActionButton: Builder(
builder: (BuildContext context) {
return AnimatedOpacity(
opacity: _showFab ? 1.0 : 0.0,
duration: const Duration(milliseconds: 200),
child: FloatingActionButton(
backgroundColor: AppColors.primary,
onPressed: () {},
// context.read<HomeCubit>().toCrudCollection(CrudType.CREATE),
child: const Icon(Icons.add),
),
);
},
),
),
);
}
///
/// Построение основго экрана
///
Widget _buildMain(BuildContext context) {
return BlocBuilder<CollectionCubit, CollectionState>(
builder: (context, state) {
return state.when(
loading: () => _LoadingList(),
data:
() => const Column(
children: <Widget>[
// CollectionsFilters(),
_CollectionList(),
],
),
empty: () => _EmptyList(),
error: () => _ErrorList(),
);
},
);
}
}
class _LoadingList extends StatelessWidget {
const _LoadingList();
@override
Widget build(BuildContext context) {
return const Placeholder(color: Colors.green);
}
}
class _ErrorList extends StatelessWidget {
const _ErrorList();
@override
Widget build(BuildContext context) {
return const Placeholder(color: Colors.brown);
}
}
class _EmptyList extends StatelessWidget {
const _EmptyList();
@override
Widget build(BuildContext context) {
return const Placeholder(color: Colors.red);
}
}
class _CollectionList extends StatelessWidget {
const _CollectionList();
// @override
@override
Widget build(BuildContext context) {
print('build _CollectionList');
final CollectionCubit collectionCubit = context.read<CollectionCubit>();
collectionCubit.initScrollListener();
return Expanded(
child: ListView.builder(
controller: collectionCubit.collectionController,
itemCount: 20,
padding: const EdgeInsets.symmetric(horizontal: 16).r,
itemBuilder:
(BuildContext context, int index) => Padding(
padding: const EdgeInsets.only(bottom: 8).r,
// child: const CollectionCard(),
),
),
);
}
}

View File

@@ -0,0 +1,61 @@
import 'package:bloc/bloc.dart';
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:remever/components/notifiers/home_screen_data.dart';
import 'package:remever/inject.dart';
part 'collection_state.dart';
part 'collection_cubit.freezed.dart';
class CollectionCubit extends Cubit<CollectionState> {
CollectionCubit() : super(CollectionState.loading());
/// Нотифаер домашнего экрана
CollectionData get _cd => getIt<CollectionData>();
/// Контроллер скролла для коллекции
final ScrollController collectionController = ScrollController();
/// Позиция скролле
double _previousScrollOffset = 0.0;
/// Индекс выбранной фильтрации коллекции
int collectionFiltersIndex = 0;
void initScrollListener() {
collectionController.addListener(() {
final double currentScrollOffset = collectionController.offset;
final bool isScrollingDown = currentScrollOffset > _previousScrollOffset;
if (isScrollingDown) {
_cd.showFab(false);
} else {
_cd.showFab(true);
}
_previousScrollOffset = currentScrollOffset;
});
}
@override
Future<void> close() {
collectionController.dispose();
return super.close();
}
Future<void> toLoadingState() async {
emit(CollectionState.loading());
}
Future<void> toDataState() async {
emit(CollectionState.data());
}
Future<void> toEmptyState() async {
emit(CollectionState.empty());
}
Future<void> toErrorState() async {
emit(CollectionState.error());
}
}

View File

@@ -0,0 +1,560 @@
// 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 'collection_cubit.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(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#adding-getters-and-methods-to-our-models',
);
/// @nodoc
mixin _$CollectionState {
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() loading,
required TResult Function() data,
required TResult Function() empty,
required TResult Function() error,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? loading,
TResult? Function()? data,
TResult? Function()? empty,
TResult? Function()? error,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? loading,
TResult Function()? data,
TResult Function()? empty,
TResult Function()? error,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_Loading value) loading,
required TResult Function(_Data value) data,
required TResult Function(_Empty value) empty,
required TResult Function(_Error value) error,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_Loading value)? loading,
TResult? Function(_Data value)? data,
TResult? Function(_Empty value)? empty,
TResult? Function(_Error value)? error,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_Loading value)? loading,
TResult Function(_Data value)? data,
TResult Function(_Empty value)? empty,
TResult Function(_Error value)? error,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $CollectionStateCopyWith<$Res> {
factory $CollectionStateCopyWith(
CollectionState value,
$Res Function(CollectionState) then,
) = _$CollectionStateCopyWithImpl<$Res, CollectionState>;
}
/// @nodoc
class _$CollectionStateCopyWithImpl<$Res, $Val extends CollectionState>
implements $CollectionStateCopyWith<$Res> {
_$CollectionStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of CollectionState
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
abstract class _$$LoadingImplCopyWith<$Res> {
factory _$$LoadingImplCopyWith(
_$LoadingImpl value,
$Res Function(_$LoadingImpl) then,
) = __$$LoadingImplCopyWithImpl<$Res>;
}
/// @nodoc
class __$$LoadingImplCopyWithImpl<$Res>
extends _$CollectionStateCopyWithImpl<$Res, _$LoadingImpl>
implements _$$LoadingImplCopyWith<$Res> {
__$$LoadingImplCopyWithImpl(
_$LoadingImpl _value,
$Res Function(_$LoadingImpl) _then,
) : super(_value, _then);
/// Create a copy of CollectionState
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
class _$LoadingImpl implements _Loading {
const _$LoadingImpl();
@override
String toString() {
return 'CollectionState.loading()';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType && other is _$LoadingImpl);
}
@override
int get hashCode => runtimeType.hashCode;
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() loading,
required TResult Function() data,
required TResult Function() empty,
required TResult Function() error,
}) {
return loading();
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? loading,
TResult? Function()? data,
TResult? Function()? empty,
TResult? Function()? error,
}) {
return loading?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? loading,
TResult Function()? data,
TResult Function()? empty,
TResult Function()? error,
required TResult orElse(),
}) {
if (loading != null) {
return loading();
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_Loading value) loading,
required TResult Function(_Data value) data,
required TResult Function(_Empty value) empty,
required TResult Function(_Error value) error,
}) {
return loading(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_Loading value)? loading,
TResult? Function(_Data value)? data,
TResult? Function(_Empty value)? empty,
TResult? Function(_Error value)? error,
}) {
return loading?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_Loading value)? loading,
TResult Function(_Data value)? data,
TResult Function(_Empty value)? empty,
TResult Function(_Error value)? error,
required TResult orElse(),
}) {
if (loading != null) {
return loading(this);
}
return orElse();
}
}
abstract class _Loading implements CollectionState {
const factory _Loading() = _$LoadingImpl;
}
/// @nodoc
abstract class _$$DataImplCopyWith<$Res> {
factory _$$DataImplCopyWith(
_$DataImpl value,
$Res Function(_$DataImpl) then,
) = __$$DataImplCopyWithImpl<$Res>;
}
/// @nodoc
class __$$DataImplCopyWithImpl<$Res>
extends _$CollectionStateCopyWithImpl<$Res, _$DataImpl>
implements _$$DataImplCopyWith<$Res> {
__$$DataImplCopyWithImpl(_$DataImpl _value, $Res Function(_$DataImpl) _then)
: super(_value, _then);
/// Create a copy of CollectionState
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
class _$DataImpl implements _Data {
const _$DataImpl();
@override
String toString() {
return 'CollectionState.data()';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType && other is _$DataImpl);
}
@override
int get hashCode => runtimeType.hashCode;
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() loading,
required TResult Function() data,
required TResult Function() empty,
required TResult Function() error,
}) {
return data();
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? loading,
TResult? Function()? data,
TResult? Function()? empty,
TResult? Function()? error,
}) {
return data?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? loading,
TResult Function()? data,
TResult Function()? empty,
TResult Function()? error,
required TResult orElse(),
}) {
if (data != null) {
return data();
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_Loading value) loading,
required TResult Function(_Data value) data,
required TResult Function(_Empty value) empty,
required TResult Function(_Error value) error,
}) {
return data(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_Loading value)? loading,
TResult? Function(_Data value)? data,
TResult? Function(_Empty value)? empty,
TResult? Function(_Error value)? error,
}) {
return data?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_Loading value)? loading,
TResult Function(_Data value)? data,
TResult Function(_Empty value)? empty,
TResult Function(_Error value)? error,
required TResult orElse(),
}) {
if (data != null) {
return data(this);
}
return orElse();
}
}
abstract class _Data implements CollectionState {
const factory _Data() = _$DataImpl;
}
/// @nodoc
abstract class _$$EmptyImplCopyWith<$Res> {
factory _$$EmptyImplCopyWith(
_$EmptyImpl value,
$Res Function(_$EmptyImpl) then,
) = __$$EmptyImplCopyWithImpl<$Res>;
}
/// @nodoc
class __$$EmptyImplCopyWithImpl<$Res>
extends _$CollectionStateCopyWithImpl<$Res, _$EmptyImpl>
implements _$$EmptyImplCopyWith<$Res> {
__$$EmptyImplCopyWithImpl(
_$EmptyImpl _value,
$Res Function(_$EmptyImpl) _then,
) : super(_value, _then);
/// Create a copy of CollectionState
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
class _$EmptyImpl implements _Empty {
const _$EmptyImpl();
@override
String toString() {
return 'CollectionState.empty()';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType && other is _$EmptyImpl);
}
@override
int get hashCode => runtimeType.hashCode;
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() loading,
required TResult Function() data,
required TResult Function() empty,
required TResult Function() error,
}) {
return empty();
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? loading,
TResult? Function()? data,
TResult? Function()? empty,
TResult? Function()? error,
}) {
return empty?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? loading,
TResult Function()? data,
TResult Function()? empty,
TResult Function()? error,
required TResult orElse(),
}) {
if (empty != null) {
return empty();
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_Loading value) loading,
required TResult Function(_Data value) data,
required TResult Function(_Empty value) empty,
required TResult Function(_Error value) error,
}) {
return empty(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_Loading value)? loading,
TResult? Function(_Data value)? data,
TResult? Function(_Empty value)? empty,
TResult? Function(_Error value)? error,
}) {
return empty?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_Loading value)? loading,
TResult Function(_Data value)? data,
TResult Function(_Empty value)? empty,
TResult Function(_Error value)? error,
required TResult orElse(),
}) {
if (empty != null) {
return empty(this);
}
return orElse();
}
}
abstract class _Empty implements CollectionState {
const factory _Empty() = _$EmptyImpl;
}
/// @nodoc
abstract class _$$ErrorImplCopyWith<$Res> {
factory _$$ErrorImplCopyWith(
_$ErrorImpl value,
$Res Function(_$ErrorImpl) then,
) = __$$ErrorImplCopyWithImpl<$Res>;
}
/// @nodoc
class __$$ErrorImplCopyWithImpl<$Res>
extends _$CollectionStateCopyWithImpl<$Res, _$ErrorImpl>
implements _$$ErrorImplCopyWith<$Res> {
__$$ErrorImplCopyWithImpl(
_$ErrorImpl _value,
$Res Function(_$ErrorImpl) _then,
) : super(_value, _then);
/// Create a copy of CollectionState
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
class _$ErrorImpl implements _Error {
const _$ErrorImpl();
@override
String toString() {
return 'CollectionState.error()';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType && other is _$ErrorImpl);
}
@override
int get hashCode => runtimeType.hashCode;
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() loading,
required TResult Function() data,
required TResult Function() empty,
required TResult Function() error,
}) {
return error();
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? loading,
TResult? Function()? data,
TResult? Function()? empty,
TResult? Function()? error,
}) {
return error?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? loading,
TResult Function()? data,
TResult Function()? empty,
TResult Function()? error,
required TResult orElse(),
}) {
if (error != null) {
return error();
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_Loading value) loading,
required TResult Function(_Data value) data,
required TResult Function(_Empty value) empty,
required TResult Function(_Error value) error,
}) {
return error(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_Loading value)? loading,
TResult? Function(_Data value)? data,
TResult? Function(_Empty value)? empty,
TResult? Function(_Error value)? error,
}) {
return error?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_Loading value)? loading,
TResult Function(_Data value)? data,
TResult Function(_Empty value)? empty,
TResult Function(_Error value)? error,
required TResult orElse(),
}) {
if (error != null) {
return error(this);
}
return orElse();
}
}
abstract class _Error implements CollectionState {
const factory _Error() = _$ErrorImpl;
}

View File

@@ -0,0 +1,9 @@
part of 'collection_cubit.dart';
@freezed
class CollectionState with _$CollectionState {
const factory CollectionState.loading() = _Loading;
const factory CollectionState.data() = _Data;
const factory CollectionState.empty() = _Empty;
const factory CollectionState.error() = _Error;
}

View File

@@ -0,0 +1,87 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:remever/common/functions.dart';
import 'package:remever/common/resources.dart';
import 'package:remever/common/widgets/typography.dart';
import 'package:remever/common/widgets/wspace.dart';
import 'package:remever/components/extensions/context.dart';
import 'package:remever/gen/assets.gen.dart';
import 'package:remever/router.gr.dart';
import 'package:remever/screens/dialogs/filters_dialog.dart';
import 'package:remever/screens/home/home_screen.dart';
class CollectionsAppBar extends StatelessWidget implements PreferredSizeWidget {
const CollectionsAppBar({super.key});
@override
Size get preferredSize => Size.fromHeight(66.h);
@override
Widget build(BuildContext context) {
return AppBar(
toolbarHeight: 66.h,
backgroundColor: AppColors.white,
shadowColor: Colors.transparent,
title: Row(
children: <Widget>[
GestureDetector(
// onLongPress: () => context.pushRoute(const SandboxRoute()),
child: AppTypography(
'Коллекции',
type: SemiBold28px(),
color: AppColors.body_text,
),
),
const WSpace(2),
Container(
height: 22.h,
width: 38.w,
decoration: BoxDecoration(
color: AppColors.secondary,
borderRadius: BorderRadius.circular(40).r,
),
child: Center(
child: AppTypography(
'2213',
type: Regular12px(),
color: AppColors.body_text,
),
),
),
],
),
actions: <Widget>[
AppBarIconButton(icon: Assets.icons.typeSearch, onTap: () {}),
AppBarIconButton(icon: Assets.icons.typeDownload, onTap: () {}),
AppBarIconButton(
icon: Assets.icons.typeSort,
onTap: () {
showCuperModalBottomSheet(
context: context,
height: 424.h,
builder: (BuildContext context) => const FiltersDialog(),
);
},
),
],
);
}
}
class AppBarIconButton extends StatelessWidget {
const AppBarIconButton({required this.icon, required this.onTap, super.key});
final AssetGenImage icon;
final void Function()? onTap;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: SizedBox(
width: 48.h,
child: Center(child: icon.image(height: 24.h, width: 24.w)),
),
);
}
}

View File

@@ -0,0 +1,12 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
@RoutePage()
class CrudCollection extends StatelessWidget {
const CrudCollection({super.key});
@override
Widget build(BuildContext context) {
return const Placeholder(color: Colors.blue);
}
}

View File

@@ -0,0 +1,76 @@
// Flutter imports:
import 'package:flutter/material.dart';
// Project imports:
import 'package:remever/common/resources.dart';
import 'package:remever/common/widgets/typography.dart';
import 'package:remever/components/extensions/context.dart';
import 'package:remever/gen/assets.gen.dart';
class DialogHeader extends StatelessWidget {
const DialogHeader({
super.key,
this.title = '',
this.paddingSize = 28,
this.action,
});
final String title;
final double paddingSize;
final Widget? action;
// Константы для стилей и отступов
static final double _headerHeight = 56.h;
static const BoxDecoration _headerDecoration = BoxDecoration(
border: Border(bottom: BorderSide(color: AppColors.gray, width: 0.5)),
);
static final _medium16Style = Medium16px();
static final double _iconSize = 24.r;
static const BoxDecoration _closeButtonDecoration = BoxDecoration(
shape: BoxShape.circle,
color: AppColors.bg,
);
@override
Widget build(BuildContext context) {
return Container(
height: _headerHeight.h,
decoration: _headerDecoration,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: paddingSize).r,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
_buildTitle(),
if (action != null) action!,
_buildCloseButton(context),
],
),
),
);
}
/// Построение заголовка диалога
Widget _buildTitle() {
return AppTypography(title, type: _medium16Style);
}
/// Построение кнопки закрытия диалога
Widget _buildCloseButton(BuildContext context) {
return GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: Container(
height: _iconSize.h,
width: _iconSize.w,
decoration: _closeButtonDecoration,
child: Center(
child: Assets.icons.typeClose.image(
color: AppColors.gray,
height: _iconSize.h,
width: _iconSize.w,
),
),
),
);
}
}

View File

@@ -0,0 +1,69 @@
// Flutter imports:
import 'package:flutter/material.dart';
// Project imports:
import 'package:remever/common/resources.dart';
import 'package:remever/common/widgets/typography.dart';
import 'package:remever/components/extensions/context.dart';
class DialogItem extends StatelessWidget {
const DialogItem({
super.key,
required this.onTap,
this.child,
this.title = '',
this.dimension = 24,
this.color,
});
final VoidCallback? onTap;
final Widget? child;
final String title;
final Color? color;
final double dimension;
// Константы для стилей и отступов
static final double _itemHeight = 56.h;
static const BoxDecoration _itemDecoration = BoxDecoration(
border: Border(bottom: BorderSide(color: AppColors.gray, width: 0.5)),
);
static final EdgeInsetsGeometry _itemPadding =
EdgeInsets.symmetric(horizontal: 28).r;
static final EdgeInsetsGeometry _iconPadding = EdgeInsets.only(right: 8).r;
static final _regular17Style = Regular17px();
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
height: _itemHeight.h,
decoration: _itemDecoration,
child: Padding(
padding: _itemPadding,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[_buildIcon(), _buildTitle()],
),
),
),
);
}
/// Построение иконки
Widget _buildIcon() {
return Padding(
padding: _iconPadding,
child: SizedBox.square(dimension: dimension.r, child: child),
);
}
/// Построение заголовка
Widget _buildTitle() {
return AppTypography(
title,
color: color ?? AppColors.black,
type: _regular17Style,
);
}
}

View File

@@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import 'package:remever/common/widgets/bottom_safe_space.dart';
import 'package:remever/gen/assets.gen.dart';
import 'package:remever/screens/dialogs/dialog_header.dart';
import 'package:remever/screens/dialogs/dialog_item.dart';
class FiltersDialog extends StatelessWidget {
const FiltersDialog({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
const DialogHeader(title: 'Сортировка'),
DialogItem(
title: 'Сначала Мои коллекции',
child: Assets.icons.typeSortA.image(),
onTap: () {
Navigator.pop(context);
},
),
DialogItem(
title: 'Сначала коллекции на изучении',
child: Assets.icons.typeSortA.image(),
onTap: () {
Navigator.pop(context);
},
),
DialogItem(
title: 'По дате обновления',
child: Assets.icons.typeSortDown.image(),
onTap: () {
Navigator.pop(context);
},
),
DialogItem(
title: 'По уровню изученности',
child: Assets.icons.typeSortDown.image(),
onTap: () {
Navigator.pop(context);
},
),
DialogItem(
title: 'По популярностии',
child: Assets.icons.typeSortDown.image(),
onTap: () {
Navigator.pop(context);
},
),
DialogItem(
title: 'По количеству карточек',
child: Assets.icons.typeSortDown.image(),
onTap: () {
Navigator.pop(context);
},
),
const BottomSafeSpace(),
],
);
}
}

View File

@@ -0,0 +1,136 @@
// Flutter imports:
import 'package:flutter/material.dart';
// Package imports:
import 'package:auto_route/auto_route.dart';
import 'package:remever/common/resources.dart';
import 'package:remever/common/widgets/wspace.dart';
import 'package:remever/components/extensions/context.dart';
import 'package:remever/gen/assets.gen.dart';
import 'package:remever/router.gr.dart';
@RoutePage()
class HomeScreen extends StatelessWidget {
///
/// Основной экран на котором расположен бар навигации
///
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
print('build home screen');
return SafeArea(
top: false,
child: AutoTabsScaffold(
routes: <PageRouteInfo>[
SettingsRoute(),
StatistickRoute(),
CrudCollection(),
CollectionRoute(),
],
bottomNavigationBuilder: (_, TabsRouter tabsRouter) {
return SizedBox(
height: 73.h,
child: Stack(
alignment: Alignment.bottomCenter,
children: <Widget>[
_buildBackgroundBar(tabsRouter),
_buildCentralButton(),
],
),
);
},
),
);
}
/// Построение заднего фона и кнопок нижнего бара
Widget _buildBackgroundBar(TabsRouter tabsRouter) {
return Container(
height: 64.h,
color: AppColors.white,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 32).r,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
_BottomBarIcon(
icon: Assets.icons.typeSetting,
isActive: tabsRouter.activeIndex == 0,
onTap: () => tabsRouter.setActiveIndex(0),
),
_BottomBarIcon(
icon: Assets.icons.typeStat,
isActive: tabsRouter.activeIndex == 1,
onTap: () => tabsRouter.setActiveIndex(1),
),
const WSpace(60),
_BottomBarIcon(
icon: Assets.icons.typeCreateCard,
isActive: tabsRouter.activeIndex == 2,
onTap: () => tabsRouter.setActiveIndex(2),
),
_BottomBarIcon(
icon: Assets.icons.typeCollection,
isActive: tabsRouter.activeIndex == 3,
onTap: () => tabsRouter.setActiveIndex(3),
),
],
),
),
);
}
/// Построение центральной кнопки
Widget _buildCentralButton() {
return Align(
alignment: Alignment.topCenter,
child: GestureDetector(
onTap: () {
// Логика нажатия на центральную кнопку
},
child: SizedBox.square(
dimension: 60.r,
child: DecoratedBox(
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: AppColors.primary,
),
child: Center(
child: Assets.icons.typeLearn.image(
color: AppColors.white,
height: 24.h,
width: 24.w,
),
),
),
),
),
);
}
}
class _BottomBarIcon extends StatelessWidget {
final AssetGenImage icon;
final bool isActive;
final VoidCallback onTap;
const _BottomBarIcon({
required this.icon,
required this.isActive,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: icon.image(
height: 24.h,
width: 24.w,
color: isActive ? AppColors.primary : Colors.black,
),
);
}
}

View File

@@ -0,0 +1,12 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
@RoutePage()
class SettingsScreen extends StatelessWidget {
const SettingsScreen({super.key});
@override
Widget build(BuildContext context) {
return const Placeholder(color: Colors.red);
}
}

View File

@@ -0,0 +1,78 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:remever/common/resources.dart';
import 'package:remever/common/widgets/wspace.dart';
import 'package:remever/components/extensions/context.dart';
import 'package:remever/components/extensions/state.dart';
import 'package:remever/gen/assets.gen.dart';
import 'package:remever/router.gr.dart';
import 'package:remever/services/auth_interface.dart';
@RoutePage()
class SplashScreen extends StatefulWidget {
///
/// Стартовый экран
///
const SplashScreen({super.key});
@override
State<SplashScreen> createState() => _SplashScreenState();
}
///
/// Состояние экрана
///
class _SplashScreenState extends State<SplashScreen> {
/// Сервисы
final AuthInterface _authIf = GetIt.I.get<AuthInterface>();
/// Запуск анимации лого
bool _launchLogo = false;
///
/// Навигация на основной экран
///
void _navigate() async {
// context.replaceRoute(const HomeRoute());
// return;
final bool isAuth = await _authIf.isAuth;
if (!mounted) return;
context.replaceRoute(isAuth ? CollectionRoute() : const AuthRoute());
}
@override
void initState() {
WidgetsBinding.instance.addPostFrameCallback((Duration timeStamp) {
Future<void>.delayed(const Duration(milliseconds: 900), () {
safeSetState(() => _launchLogo = !_launchLogo);
});
Future<void>.delayed(const Duration(seconds: 4), _navigate);
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.bg,
body: SafeArea(
child: Column(
children: <Widget>[
HSpace(148),
AnimatedOpacity(
opacity: _launchLogo ? 1 : 0,
duration: const Duration(seconds: 3),
child: Assets.images.logo.image(width: 308.w, height: 100.h),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,12 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
@RoutePage()
class StatistickScreen extends StatelessWidget {
const StatistickScreen({super.key});
@override
Widget build(BuildContext context) {
return const Placeholder(color: Colors.orange);
}
}

View File

@@ -0,0 +1,16 @@
///
///
///
abstract interface class AuthInterface {
/// Проверка авторизации
Future<bool> get isAuth;
/// Получение токена
Future<String?> get token;
/// Авторизация
Future<String?> login(String email);
/// Отправка кода
Future<bool> sendCode(String code, String uid);
}

View File

@@ -0,0 +1,72 @@
import 'package:dio/dio.dart';
import 'package:dio_smart_retry/dio_smart_retry.dart';
import 'package:injectable/injectable.dart';
import 'package:remever/common/resources.dart';
import 'package:remever/common/services/api_client.dart';
import 'package:remever/common/storage.dart';
import 'package:remever/common/typedef.dart';
import 'package:remever/services/auth_interface.dart';
///
/// Сервис авторизации
///
@Singleton(as: AuthInterface)
final class AuthService implements AuthInterface {
@override
Future<bool> get isAuth async => await token != null;
@override
Future<String?> get token async {
final String? accessToken = await authSecStorage.read(
key: StorageKeys.accessToken,
);
return accessToken;
}
@override
Future<String?> login(String email) async {
try {
final Response<dynamic> result = await apiClient.post(
'/auth/email/send',
options: Options()..disableRetry = true,
data: <String, dynamic>{'email': email.toLowerCase()},
);
final Json response = Json.from(result.data);
if (response['success'] == false) return null;
return response['result']['authUid'];
} catch (e) {
return null;
}
}
@override
Future<bool> sendCode(String code, String uid) async {
try {
final Response<dynamic> result = await apiClient.post(
'/auth/code/login',
options: Options()..disableRetry = true,
data: <String, dynamic>{'authUid': uid, 'confirmCode': code},
);
final Json response = Json.from(result.data);
final bool success = response['success'] ?? false;
if (success) {
await authSecStorage.write(
key: StorageKeys.accessToken,
value: response['result']['token'],
);
}
return success;
} catch (e) {
return false;
}
}
}

View File

@@ -0,0 +1,4 @@
///
/// Базовый сервис для сервисов, которые не будут меняться от проекта к проекту
///
abstract class CoreService {}

View File

@@ -0,0 +1,52 @@
// Dart imports:
import 'dart:convert';
import 'dart:typed_data';
import 'package:hive_ce/hive.dart';
import 'package:remever/common/storage.dart';
import 'core_service.dart';
///
/// Сервис для работы с ключами шифрования
///
class EncKeysService extends CoreService {
///
/// Получение ключа для шифрования
///
Future<String> getRawKey(String keyFor) async {
String? encKey = await hiveKeysStorage.read(key: keyFor);
if (encKey == null) {
final List<int> hiveKey = Hive.generateSecureKey();
encKey = base64UrlEncode(hiveKey);
await hiveKeysStorage.write(key: keyFor, value: encKey);
}
return encKey;
}
///
/// Получение ключа шифрования
///
/// Если ключа ранее не существовало - он будет создан
///
Future<Uint8List> getKey(String keyFor) async {
String? encKey = await getRawKey(keyFor);
return base64Url.decode(encKey);
}
///
/// Получение ключа шифрования в виде строки
///
/// Если ключа ранее не существовало - он будет создан
///
Future<String> getStringKey(String keyFor) async {
final Uint8List key = await getKey(keyFor);
return String.fromCharCodes(key);
}
}

View File

@@ -0,0 +1,42 @@
import 'package:remever/common/resources.dart';
import 'package:remever/common/storage.dart';
import 'package:remever/i18n/strings.g.dart';
import 'package:remever/interfaces/warmup_service.dart';
import 'core_service.dart';
///
/// Сервис для работы с языками приложения
///
class LangService extends CoreService implements IWarmupService {
///
/// Установка языка при первом запуске
///
@override
Future<void> init() async {
final AppLocale deviceLocale = await LocaleSettings.useDeviceLocale();
AppLocale? locale = hiveLang.get(StorageKeys.langCode);
if (locale == null) {
LocaleSettings.setLocale(deviceLocale);
hiveLang.put(StorageKeys.langCode, deviceLocale);
} else {
LocaleSettings.setLocale(locale);
}
}
///
/// Получение текущей локали
///
AppLocale get locale =>
hiveLang.get(StorageKeys.langCode, defaultValue: AppLocale.en)!;
///
/// Запись и установка языка
///
Future<void> setLanguage(AppLocale locale) async {
await hiveLang.put(StorageKeys.langCode, locale);
LocaleSettings.setLocale(locale);
}
}

View File

@@ -0,0 +1,48 @@
// Flutter imports:
import 'package:flutter/material.dart' show ThemeMode;
import 'package:remever/common/resources.dart';
import 'package:remever/common/storage.dart';
import 'package:remever/components/extensions/theme_mode.dart';
import 'package:remever/interfaces/warmup_service.dart';
import 'core_service.dart';
///
/// Сервис для работы с темой приложения
///
class ThemeService extends CoreService implements IWarmupService {
@override
Future<void> init() async {
final ThemeMode? theme = hiveTheme.get(StorageKeys.themeKey);
if (theme == null) {
await hiveTheme.put(StorageKeys.themeKey, ThemeMode.light);
}
}
///
/// Получение текущего значения для темы
///
/// По-умолчанию - светлый
///
ThemeMode get themeMode {
return hiveTheme.get(StorageKeys.themeKey, defaultValue: ThemeMode.light)!;
}
///
/// Обновление темы приложения
///
/// [oldThemeMode] должно быть текущее значение темы. Автоматически будет
/// вызван метод {inversed}
///
Future<void> update(ThemeMode oldThemeMode) async {
return updateInversed(oldThemeMode.inversed);
}
///
/// Обновление темы приложения
///
Future<void> updateInversed(ThemeMode themeMode) async {
return hiveTheme.put(StorageKeys.themeKey, themeMode);
}
}

View File

@@ -0,0 +1,106 @@
// Dart imports:
import 'dart:async';
// Flutter imports:
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:hive_ce/hive.dart';
import 'package:injectable/injectable.dart';
import 'package:remever/common/resources.dart';
import 'package:remever/components/notifiers/app_settings.dart';
import 'package:remever/components/notifiers/home_screen_data.dart';
import 'package:remever/helpers/hive_creator.dart';
import 'package:remever/i18n/strings.g.dart';
import 'package:remever/interfaces/warmup_service.dart';
import 'package:remever/models/adapters/app_locale_adapter.dart';
import 'package:remever/models/adapters/theme_mode_adapter.dart';
import '../inject.dart';
import 'core/enc_keys_service.dart';
import 'core/lang_service.dart';
import 'core/theme_service.dart';
///
/// Сервис прогрева приложения
///
@Singleton()
class WarmupService {
WarmupService(this._themeService, this._langService, this._encKeysService);
/// Сервисы
final ThemeService _themeService;
final LangService _langService;
final EncKeysService _encKeysService;
/// [Completer] для прогрева приложения
final Completer<bool> _firstStartCompleter = Completer<bool>();
Completer<bool> get firstStartCompleter => _firstStartCompleter;
@PostConstruct(preResolve: true)
Future<void> common() async {
await _registerHiveAdapters();
await _openHiveBoxes();
await _registerNotifiers();
}
///
/// Инициализация для запуска приложения
///
Future<void> firstStart() async {
await _setStoragesValue();
}
///
/// Проставнока изначальных значений хранилищ
///
Future<void> _setStoragesValue() async {
final List<IWarmupService> services = <IWarmupService>[
_themeService,
_langService,
];
for (final IWarmupService service in services) {
await service.init();
}
}
///
/// Регистрация [Hive] адаптеров
///
Future<void> _registerHiveAdapters() async {
Hive.registerAdapter<ThemeMode>(ThemeModeAdapter());
Hive.registerAdapter<AppLocale>(AppLocaleAdapter());
}
///
/// Открытие [Hive] хранилищ
///
Future<void> _openHiveBoxes() async {
final Map<String, HiveCreator<dynamic>> storageNames =
<String, HiveCreator<dynamic>>{
Storage.storageAuth: HiveCreator<String>(),
Storage.hiveThemeMode: HiveCreator<ThemeMode>(),
Storage.hiveLang: HiveCreator<AppLocale>(),
};
for (MapEntry<String, HiveCreator<dynamic>> storage
in storageNames.entries) {
final String name = storage.key;
final Uint8List key = await _encKeysService.getKey(name);
await storage.value.open(name, HiveAesCipher(key));
}
}
///
/// Регистрация нотификаторов
///
Future<void> _registerNotifiers() async {
getIt.registerLazySingleton<AppSettingsNotifier>(
() => AppSettingsNotifier(debugMode: kDebugMode),
);
getIt.registerLazySingleton<CollectionData>(() => CollectionData());
}
}

View File

@@ -0,0 +1,91 @@
// Flutter imports:
import 'package:flutter/material.dart';
import 'package:remever/common/resources.dart';
class CustomTheme extends ValueNotifier<ThemeMode> {
/// Текущая тема
static bool _isDarkTheme = false;
CustomTheme(super.value);
@override
ThemeMode get value => _isDarkTheme ? ThemeMode.dark : ThemeMode.light;
bool get isDark => _isDarkTheme;
///
/// Смена темы
///
void toggleTheme() {
_isDarkTheme = !_isDarkTheme;
notifyListeners();
}
///
/// Темная тема
///
static ThemeData get darkTheme {
return ThemeData.dark(useMaterial3: true);
}
///
/// Светлая тема
///
static ThemeData get lightTheme {
return ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSwatch(backgroundColor: Colors.white),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(foregroundColor: Colors.black),
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
backgroundColor: AppColors.app_blue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
segmentedButtonTheme: SegmentedButtonThemeData(
style: SegmentedButton.styleFrom(
selectedBackgroundColor: const Color(0xFFF3F3FF),
selectedForegroundColor: AppColors.app_blue,
overlayColor: AppColors.app_blue,
foregroundColor: Colors.black,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: const BorderSide(color: AppColors.app_border, width: 1),
),
side: const BorderSide(color: AppColors.app_border, width: 1),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.app_blue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
side: const BorderSide(
color: AppColors.app_blue,
width: 1.3,
style: BorderStyle.solid,
),
),
),
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
selectedItemColor: AppColors.app_blue,
unselectedItemColor: Color(0xFF888B98),
),
progressIndicatorTheme: const ProgressIndicatorThemeData(
color: AppColors.app_blue,
),
tabBarTheme: const TabBarTheme(
labelColor: Colors.black,
indicatorColor: AppColors.app_blue,
tabAlignment: TabAlignment.start,
dividerColor: AppColors.app_border,
),
);
}
}

View File

@@ -0,0 +1,65 @@
// Flutter imports:
import 'package:flutter/material.dart';
///
/// Расширение для темы всего приложения
/// Для [Theme]
///
/// Используется для хранения общих типов цветов
///
class AppThemeExtension extends ThemeExtension<AppThemeExtension> {
AppThemeExtension({
required this.background,
required this.textColor,
required this.appBarBackground,
});
AppThemeExtension.light()
: background = Colors.white,
textColor = Colors.black,
appBarBackground = Colors.indigo;
AppThemeExtension.dark()
: background = Colors.black,
textColor = Colors.white,
appBarBackground = Colors.lightGreen;
/// Цвет фона
final Color background;
/// Цвет текста
final Color textColor;
/// Цвет фона [AppBar]
final Color appBarBackground;
@override
ThemeExtension<AppThemeExtension> copyWith({
Color? background,
Color? textColor,
Color? appBarBackground,
}) {
return AppThemeExtension(
background: background ?? this.background,
textColor: textColor ?? this.textColor,
appBarBackground: appBarBackground ?? this.appBarBackground,
);
}
@override
ThemeExtension<AppThemeExtension> lerp(
ThemeExtension<AppThemeExtension>? other,
double t,
) {
if (other is! AppThemeExtension) {
return this;
}
return AppThemeExtension(
background: Color.lerp(background, other.background, t)!,
textColor: Color.lerp(textColor, other.textColor, t)!,
appBarBackground:
Color.lerp(appBarBackground, other.appBarBackground, t)!,
);
}
}

View File

@@ -0,0 +1,21 @@
// Flutter imports:
import 'package:flutter/material.dart';
class BottomSafeSpace extends StatelessWidget {
///
/// Отступ от нижней границы экрана
///
/// Для iOS значение будет не нулевое если есть "полоска"
/// Для Android в основном будет 0
///
const BottomSafeSpace({
super.key,
});
@override
Widget build(BuildContext context) {
return SizedBox(
height: MediaQuery.of(context).padding.bottom,
);
}
}

View File

@@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
import 'package:remever/common/resources.dart';
import 'package:remever/components/extensions/context.dart';
import 'package:remever/components/extensions/state.dart';
class PrimaryButton extends StatefulWidget {
const PrimaryButton({
required this.child,
required this.onTap,
super.key,
this.height = 52,
this.width = double.infinity,
});
final Widget child;
final double height;
final double width;
final Function() onTap;
@override
State<PrimaryButton> createState() => _PrimaryButtonState();
}
class _PrimaryButtonState extends State<PrimaryButton> {
bool isLoading = false;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () async {
safeSetState(() => isLoading = !isLoading);
await widget.onTap();
safeSetState(() => isLoading = !isLoading);
},
child: SizedBox(
height: widget.height.h,
width: widget.width,
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(16)).r,
color: AppColors.primary,
),
child: Center(
child:
isLoading
? const CircularProgressIndicator(
color: AppColors.bg,
backgroundColor: Colors.transparent,
)
: widget.child,
),
),
),
);
}
}