first commit
This commit is contained in:
183
lib/app.dart
Normal file
183
lib/app.dart
Normal 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;
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
40
lib/common/events/common_events.dart
Normal file
40
lib/common/events/common_events.dart
Normal 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;
|
||||
}
|
||||
7
lib/common/events/events.dart
Normal file
7
lib/common/events/events.dart
Normal 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
64
lib/common/functions.dart
Normal 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
11
lib/common/getters.dart
Normal 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>();
|
||||
}
|
||||
84
lib/common/mixin/subscriptionable.dart
Normal file
84
lib/common/mixin/subscriptionable.dart
Normal 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
131
lib/common/resources.dart
Normal 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);
|
||||
}
|
||||
84
lib/common/services/api_client.dart
Normal file
84
lib/common/services/api_client.dart
Normal 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
25
lib/common/storage.dart
Normal 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
9
lib/common/typedef.dart
Normal 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
146
lib/common/typography.dart
Normal 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
79
lib/common/utils.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
21
lib/common/widgets/bottom_safe_space.dart
Normal file
21
lib/common/widgets/bottom_safe_space.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
24
lib/common/widgets/loose_focus.dart
Normal file
24
lib/common/widgets/loose_focus.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
68
lib/common/widgets/swipe_gesture.dart
Normal file
68
lib/common/widgets/swipe_gesture.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
256
lib/common/widgets/typography.dart
Normal file
256
lib/common/widgets/typography.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
15
lib/common/widgets/typography_span.dart
Normal file
15
lib/common/widgets/typography_span.dart
Normal 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;
|
||||
}
|
||||
35
lib/common/widgets/w_if.dart
Normal file
35
lib/common/widgets/w_if.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
61
lib/common/widgets/wspace.dart
Normal file
61
lib/common/widgets/wspace.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
50
lib/components/debouncer.dart
Normal file
50
lib/components/debouncer.dart
Normal 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
30
lib/components/env.dart
Normal 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
12
lib/components/env/dev_env.dart
vendored
Normal 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
12
lib/components/env/prod_env.dart
vendored
Normal 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';
|
||||
}
|
||||
21
lib/components/extensions/context.dart
Normal file
21
lib/components/extensions/context.dart
Normal 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;
|
||||
}
|
||||
21
lib/components/extensions/duration_extensions.dart
Normal file
21
lib/components/extensions/duration_extensions.dart
Normal 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';
|
||||
}
|
||||
}
|
||||
26
lib/components/extensions/state.dart
Normal file
26
lib/components/extensions/state.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
31
lib/components/extensions/string.dart
Normal file
31
lib/components/extensions/string.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
21
lib/components/extensions/theme_mode.dart
Normal file
21
lib/components/extensions/theme_mode.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
38
lib/components/listeners/theme_listener.dart
Normal file
38
lib/components/listeners/theme_listener.dart
Normal 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);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
39
lib/components/notifiers/app_settings.dart
Normal file
39
lib/components/notifiers/app_settings.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
18
lib/components/notifiers/home_screen_data.dart
Normal file
18
lib/components/notifiers/home_screen_data.dart
Normal 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
9
lib/env.dart
Normal 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
430
lib/gen/assets.gen.dart
Normal 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;
|
||||
}
|
||||
15
lib/helpers/hive_creator.dart
Normal file
15
lib/helpers/hive_creator.dart
Normal 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
26
lib/i18n/en.i18n.json
Normal 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
26
lib/i18n/ru.i18n.json
Normal 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
182
lib/i18n/strings.g.dart
Normal 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
119
lib/i18n/strings_en.g.dart
Normal 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
121
lib/i18n/strings_ru.g.dart
Normal 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
43
lib/inject.config.dart
Normal 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
11
lib/inject.dart
Normal 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();
|
||||
9
lib/interfaces/warmup_service.dart
Normal file
9
lib/interfaces/warmup_service.dart
Normal file
@@ -0,0 +1,9 @@
|
||||
///
|
||||
/// Интерфейс сервиса, который должен быть "прогрет"
|
||||
///
|
||||
abstract class IWarmupService {
|
||||
///
|
||||
/// Функция для прогрева
|
||||
///
|
||||
Future<void> init();
|
||||
}
|
||||
52
lib/main.dart
Normal file
52
lib/main.dart
Normal 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);
|
||||
}
|
||||
29
lib/models/adapters/app_locale_adapter.dart
Normal file
29
lib/models/adapters/app_locale_adapter.dart
Normal 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;
|
||||
}
|
||||
30
lib/models/adapters/theme_mode_adapter.dart
Normal file
30
lib/models/adapters/theme_mode_adapter.dart
Normal 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;
|
||||
}
|
||||
65
lib/models/log_entity.dart
Normal file
65
lib/models/log_entity.dart
Normal 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
33
lib/router.dart
Normal 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
150
lib/router.gr.dart
Normal 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();
|
||||
},
|
||||
);
|
||||
}
|
||||
43
lib/screens/auth/auth_screen.dart
Normal file
43
lib/screens/auth/auth_screen.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
46
lib/screens/auth/cubit/auth_cubit.dart
Normal file
46
lib/screens/auth/cubit/auth_cubit.dart
Normal 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());
|
||||
}
|
||||
}
|
||||
458
lib/screens/auth/cubit/auth_cubit.freezed.dart
Normal file
458
lib/screens/auth/cubit/auth_cubit.freezed.dart
Normal 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;
|
||||
}
|
||||
8
lib/screens/auth/cubit/auth_state.dart
Normal file
8
lib/screens/auth/cubit/auth_state.dart
Normal 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;
|
||||
}
|
||||
139
lib/screens/auth/screens/code_auth.dart
Normal file
139
lib/screens/auth/screens/code_auth.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
99
lib/screens/auth/screens/email_auth.dart
Normal file
99
lib/screens/auth/screens/email_auth.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
101
lib/screens/auth/screens/initial_auth.dart
Normal file
101
lib/screens/auth/screens/initial_auth.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
64
lib/screens/auth/widgets/auth_text_field.dart
Normal file
64
lib/screens/auth/widgets/auth_text_field.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
50
lib/screens/auth/widgets/pin_code.dart
Normal file
50
lib/screens/auth/widgets/pin_code.dart
Normal 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 {},
|
||||
);
|
||||
}
|
||||
}
|
||||
95
lib/screens/auth/widgets/timer.dart
Normal file
95
lib/screens/auth/widgets/timer.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
119
lib/screens/collections/collections_screen.dart
Normal file
119
lib/screens/collections/collections_screen.dart
Normal 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(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
61
lib/screens/collections/cubit/collection_cubit.dart
Normal file
61
lib/screens/collections/cubit/collection_cubit.dart
Normal 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());
|
||||
}
|
||||
}
|
||||
560
lib/screens/collections/cubit/collection_cubit.freezed.dart
Normal file
560
lib/screens/collections/cubit/collection_cubit.freezed.dart
Normal 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;
|
||||
}
|
||||
9
lib/screens/collections/cubit/collection_state.dart
Normal file
9
lib/screens/collections/cubit/collection_state.dart
Normal 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;
|
||||
}
|
||||
87
lib/screens/collections/widgets/collections_app_bar.dart
Normal file
87
lib/screens/collections/widgets/collections_app_bar.dart
Normal 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)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
12
lib/screens/crud_collection/crud_collection.dart
Normal file
12
lib/screens/crud_collection/crud_collection.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
76
lib/screens/dialogs/dialog_header.dart
Normal file
76
lib/screens/dialogs/dialog_header.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
69
lib/screens/dialogs/dialog_item.dart
Normal file
69
lib/screens/dialogs/dialog_item.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
61
lib/screens/dialogs/filters_dialog.dart
Normal file
61
lib/screens/dialogs/filters_dialog.dart
Normal 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(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
136
lib/screens/home/home_screen.dart
Normal file
136
lib/screens/home/home_screen.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
12
lib/screens/settings/settings_screen.dart
Normal file
12
lib/screens/settings/settings_screen.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
78
lib/screens/splash/splash_screen.dart
Normal file
78
lib/screens/splash/splash_screen.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
12
lib/screens/statistick/statistick_screen.dart
Normal file
12
lib/screens/statistick/statistick_screen.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
16
lib/services/auth_interface.dart
Normal file
16
lib/services/auth_interface.dart
Normal 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);
|
||||
}
|
||||
72
lib/services/auth_service.dart
Normal file
72
lib/services/auth_service.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
4
lib/services/core/core_service.dart
Normal file
4
lib/services/core/core_service.dart
Normal file
@@ -0,0 +1,4 @@
|
||||
///
|
||||
/// Базовый сервис для сервисов, которые не будут меняться от проекта к проекту
|
||||
///
|
||||
abstract class CoreService {}
|
||||
52
lib/services/core/enc_keys_service.dart
Normal file
52
lib/services/core/enc_keys_service.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
42
lib/services/core/lang_service.dart
Normal file
42
lib/services/core/lang_service.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
48
lib/services/core/theme_service.dart
Normal file
48
lib/services/core/theme_service.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
106
lib/services/warmup_service.dart
Normal file
106
lib/services/warmup_service.dart
Normal 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());
|
||||
}
|
||||
}
|
||||
91
lib/theme/custom_theme.dart
Normal file
91
lib/theme/custom_theme.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
65
lib/theme/extensions/app_theme_extensions.dart
Normal file
65
lib/theme/extensions/app_theme_extensions.dart
Normal 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)!,
|
||||
);
|
||||
}
|
||||
}
|
||||
21
lib/widgets/bottom_safe_space.dart
Normal file
21
lib/widgets/bottom_safe_space.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
56
lib/widgets/primary_button.dart
Normal file
56
lib/widgets/primary_button.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user