first commit

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

View File

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

View File

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

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

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

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

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

View File

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

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

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

View File

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

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

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

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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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