first commit
This commit is contained in:
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user