Обновлен проект. Добавлена БД

This commit is contained in:
2025-03-03 23:57:55 +03:00
parent 273e68557a
commit cb6ce05059
726 changed files with 9424 additions and 478 deletions

View File

@@ -8,6 +8,7 @@ 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:oktoast/oktoast.dart';
import 'package:provider/provider.dart';
import 'package:remever/common/events/common_events.dart';
import 'package:remever/common/events/events.dart';
@@ -137,7 +138,7 @@ class _MyAppState extends State<MyApp>
///
Widget _buildMaterialApp(ThemeMode themeMode) {
return MaterialApp.router(
title: 'Who Will Win',
title: 'Remever',
theme: CustomTheme.lightTheme,
darkTheme: CustomTheme.darkTheme,
themeMode: themeMode,
@@ -147,35 +148,44 @@ class _MyAppState extends State<MyApp>
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 OKToast(
position: ToastPosition.bottom,
backgroundColor: Colors.black.withOpacity(0.7),
child: MediaQuery(
data: MediaQuery.of(
context,
).copyWith(textScaler: TextScaler.noScaling),
child: 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();
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,
),
);
}
if (value.showFps) {
return Material(
child: FPSWidget(
alignment: Alignment.centerLeft,
child: result,
),
);
}
return result;
return result;
},
);
},
);
},
),
),
);
},
);

View File

@@ -16,10 +16,10 @@ extension NotificationEventTypeExtension on NotificationEventType {
return AppColors.gray;
case NotificationEventType.ERROR:
return AppColors.red;
return AppColors.info_red;
case NotificationEventType.WARNING:
return AppColors.yellowL;
return AppColors.info_yellow;
}
}
}

View File

@@ -90,42 +90,25 @@ abstract class AppColors {
static const Color disabled = Color(0xFF8B8B8B);
static const Color gray = Color(0xFFE0E0E0);
static const Color body_text = Color(0xFF080514);
static const Color gray_bg = Color(0xFFEFEFF4);
static const Color secondary = Color(0xFFDBD7F4);
static const Color danger = Color(0xFFFF5C69);
static const Color question = Color(0xFFF4D7E2);
static const Color answer = Color(0xFFD7F4EA);
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 primary_blue = Color(0xFF1837B4);
static const Color additional_blue = Color(0xFF1837B4);
static const Color primary_gray = Color(0xFFF1F2F4);
static const Color primary_red = Color(0xFFFF543D);
static const Color secondary_red = Color(0xFFFFF2F1);
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);
static const Color text_gray = Color(0xFF80899C);
static const Color text_black = Color(0xFF0B0C0E);
static const Color text_white = Color(0xFFFFFFFF);
static const Color grayscale_100 = Color(0xFFE2E4E9);
static const Color info_red = Color(0xFFDA4E4E);
static const Color info_yellow = Color(0xFFFFF8E0);
static const Color info_gray = Color(0xFFA8A8A8);
}

37
lib/common/toast.dart Normal file
View File

@@ -0,0 +1,37 @@
// Flutter imports:
import 'package:flutter/widgets.dart';
// Package imports:
import 'package:oktoast/oktoast.dart' show ToastPosition, showToastWidget;
import 'package:remever/common/resources.dart';
import 'package:remever/common/widgets/info_toast.dart';
// Project imports:
///
/// Класс для отображения тостов
///
final class Toast {
///
/// Показать информационный тост
///
static void show(Widget child, {Duration? duration}) {
showToastWidget(
InfoToast(child: child),
duration: duration,
handleTouch: true,
);
}
///
/// Показать тост с иконкой для закрытия
///
static void showDismissible(String message, {Duration? duration}) {
showToastWidget(
InfoToast.dismissible(message: message, bgColor: AppColors.white),
position: ToastPosition.top.copyWith(offset: 50),
duration: duration,
handleTouch: true,
);
}
}

View File

@@ -1,3 +1,4 @@
// Project imports:
import 'package:remever/common/widgets/typography.dart';
/// -- Regular --
@@ -7,11 +8,6 @@ class Regular12px extends TypographyTypeRegular {
double get size => 12;
}
class Regular13px extends TypographyTypeRegular {
@override
double get size => 13;
}
class Regular14px extends TypographyTypeRegular {
@override
double get size => 14;
@@ -76,14 +72,9 @@ class SemiBold14px extends TypographyTypeSemiBold {
double get size => 14;
}
class SemiBold18px extends TypographyTypeSemiBold {
class SemiBold20px extends TypographyTypeSemiBold {
@override
double get size => 18;
}
class SemiBold22px extends TypographyTypeSemiBold {
@override
double get size => 22;
double get size => 20;
}
class SemiBold24px extends TypographyTypeSemiBold {
@@ -95,7 +86,6 @@ class SemiBold28px extends TypographyTypeSemiBold {
@override
double get size => 28;
}
// -- Bold --
class Bold10px extends TypographyTypeBold {
@@ -108,29 +98,14 @@ class Bold12px extends TypographyTypeBold {
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 {
class Bold34px 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;
double get size => 34;
}
// -- HeadBold --
@@ -139,8 +114,3 @@ class HeadBold20px extends TypographyTypeHeadBold {
@override
double get size => 20;
}
class HeadBold28px extends TypographyTypeHeadBold {
@override
double get size => 28;
}

View File

@@ -0,0 +1,162 @@
// Flutter imports:
import 'package:flutter/material.dart';
// Package imports:
import 'package:oktoast/oktoast.dart' show dismissAllToast;
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';
// Project imports:
import 'package:remever/gen/assets.gen.dart';
enum ToastType {
///
/// Стандартный тост
///
DEFAULT,
///
/// Закрываемый вручную тост
///
DISMISSIBLE,
}
class InfoToast extends StatelessWidget {
///
/// Виджет отображающийся при использовании [Toast.show]
///
const InfoToast({required this.child, super.key})
: toastType = ToastType.DEFAULT,
message = '',
type = null,
textColor = null,
bgColor = null;
const InfoToast.dismissible({
required this.message,
this.type,
this.textColor,
this.bgColor,
super.key,
}) : toastType = ToastType.DISMISSIBLE,
child = const SizedBox();
/// Сообщение в [ToastType.DISMISSIBLE]
final String message;
/// Сообщение в [ToastType.DEFAULT]
final Widget child;
/// Тип тоста
final ToastType toastType;
/// Тип типографии
final TypographyType? type;
/// Цвет текста
final Color? textColor;
/// Фоновый цвет
final Color? bgColor;
@override
Widget build(BuildContext context) {
return switch (toastType) {
ToastType.DEFAULT => _Toast(child: child),
ToastType.DISMISSIBLE => _DismissibleToast(
message: message,
type: type,
textColor: textColor,
bgColor: bgColor,
),
};
}
}
class _DismissibleToast extends StatelessWidget {
const _DismissibleToast({
required this.message,
this.type,
this.textColor,
this.bgColor,
});
final String message;
final TypographyType? type;
final Color? textColor;
final Color? bgColor;
@override
Widget build(BuildContext context) {
return Material(
child: SizedBox(
width: MediaQuery.sizeOf(context).width * 0.9,
// height: 60.h,
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: 60.h),
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8)).r,
color: bgColor ?? AppColors.white,
border: Border.all(color: AppColors.gray, width: 0.5.w),
),
child: Padding(
padding: const EdgeInsets.all(12).r,
child: Row(
children: <Widget>[
Assets.icons.typeDanger.image(height: 24.h, width: 24.w),
const WSpace(8),
Flexible(
fit: FlexFit.tight,
child: AppTypography(
message,
type: type,
color: textColor ?? AppColors.white,
textAlign: TextAlign.start,
maxLines: 3,
),
),
InkWell(
onTap: dismissAllToast,
child: SizedBox.square(
dimension: 24.r,
child: const DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColors.gray_bg,
),
child: Icon(Icons.close, color: AppColors.disabled),
),
),
),
],
),
),
),
),
),
);
}
}
class _Toast extends StatelessWidget {
const _Toast({required this.child});
final Widget child;
@override
Widget build(BuildContext context) {
return Material(
child: SizedBox(
height: 40,
width: MediaQuery.sizeOf(context).width * 0.6,
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(15)).r,
child: child,
),
),
);
}
}

View File

@@ -1,9 +1,10 @@
// Flutter imports:
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
// Package imports:
import 'package:google_fonts/google_fonts.dart';
import 'package:remever/common/resources.dart';
import 'package:remever/components/extensions/context.dart';
export '../../common/typography.dart';
@@ -31,6 +32,7 @@ abstract class TypographyTypeRegular extends TypographyType {
fontWeight: FontWeight.w400,
fontSize: size.sp,
height: height,
color: AppColors.text_black,
);
}
}
@@ -47,6 +49,7 @@ abstract class TypographyTypeMedium extends TypographyType {
fontWeight: FontWeight.w500,
fontSize: size.sp,
height: height,
color: AppColors.text_black,
);
}
}
@@ -63,6 +66,7 @@ abstract class TypographyTypeSemiBold extends TypographyType {
fontWeight: FontWeight.w600,
fontSize: size.sp,
height: height,
color: AppColors.text_black,
);
}
}
@@ -79,6 +83,7 @@ abstract class TypographyTypeBold extends TypographyType {
fontWeight: FontWeight.w700,
fontSize: size.sp,
height: height,
color: AppColors.text_black,
);
}
}
@@ -95,6 +100,7 @@ abstract class TypographyTypeHeadBold extends TypographyType {
fontWeight: FontWeight.w700,
fontSize: size.sp,
height: height,
color: AppColors.text_black,
);
}
}
@@ -218,7 +224,7 @@ class AppTypography extends StatelessWidget {
type == null
? textStyle.style
: type!.style.copyWith(
color: color ?? textStyle.style.color,
color: color ?? Colors.black,
fontWeight: fontWeight,
height: height,
decoration: textDecoration,

BIN
lib/database/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,3 @@
// We use a conditional export to expose the right connection factory depending
// on the platform.
export 'native.dart';

View File

@@ -0,0 +1,128 @@
// Dart imports:
import 'dart:io';
import 'dart:isolate';
// Flutter imports:
import 'package:flutter/foundation.dart' show kDebugMode;
// Package imports:
import 'package:drift/drift.dart';
import 'package:drift/isolate.dart';
import 'package:drift/native.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:remever/inject.dart';
import 'package:remever/services/core/enc_keys_service.dart';
/// Obtains a database connection for running drift in a Dart VM.
///
/// The [NativeDatabase] from drift will synchronously use sqlite3's C APIs.
/// To move synchronous database work off the main thread, we use a
/// [DriftIsolate], which can run queries in a background isolate under the
/// hood.
///
DatabaseConnection connect() {
return DatabaseConnection.delayed(
Future<DatabaseConnection>.sync(() async {
// Background isolates can't use platform channels, so let's use
// `path_provider` in the main isolate and just send the result containing
// the path over to the background isolate.
final Directory appDir = await getApplicationDocumentsDirectory();
final Directory dbDir = Directory(p.join(appDir.path, 'databases'));
if (!dbDir.existsSync()) {
await dbDir.create(recursive: true);
}
final String dbPath = p.join(appDir.path, 'databases', 'app.db.enc');
final EncKeysService encKeyService = getIt<EncKeysService>();
final String key = await encKeyService.getRawKey('db');
final ReceivePort receiveDriftIsolate = ReceivePort();
await Isolate.spawn(
_entrypointForDriftIsolate,
_IsolateStartRequest(receiveDriftIsolate.sendPort, dbPath, key),
);
final DriftIsolate driftIsolate =
await receiveDriftIsolate.first as DriftIsolate;
return driftIsolate.connect();
}),
);
}
/// The entrypoint of isolates can only take a single message, but we need two
/// (a send port to reach the originating isolate and the database's path that
/// should be opened on the background isolate). So, we bundle this information
/// in a single class.
class _IsolateStartRequest {
final SendPort talkToMain;
final String databasePath;
final String key;
_IsolateStartRequest(this.talkToMain, this.databasePath, this.key);
}
/// The entrypoint for a background isolate launching a drift server.
///
/// The main isolate can then connect to that isolate server to transparently
/// run queries in the background.
void _entrypointForDriftIsolate(_IsolateStartRequest request) {
/// @see https://drift.simonbinder.eu/docs/other-engines/encryption/
// open
// ..overrideFor(OperatingSystem.android, openCipherOnAndroid)
// ..overrideFor(
// OperatingSystem.linux,
// () => DynamicLibrary.open('libsqlcipher.so'),
// )
// ..overrideFor(
// OperatingSystem.windows,
// () => DynamicLibrary.open('sqlcipher.dll'),
// );
if (kDebugMode) {
print('[DRIFT] Path -> ${request.databasePath}');
}
// The native database synchronously uses sqlite3's C API with `dart:ffi` for
// a fast database implementation that doesn't require platform channels.
final NativeDatabase databaseImpl = NativeDatabase(
File(request.databasePath),
logStatements: false,
// setup: (Database db) {
// // Check that we're actually running with SQLCipher by quering the
// // cipher_version pragma.
// if (Platform.isAndroid) {
// final ResultSet result = db.select('pragma cipher_version');
// if (result.isEmpty) {
// throw UnsupportedError(
// 'This database needs to run with SQLCipher, but that library is '
// 'not available!',
// );
// }
// }
// // Then, apply the key to encrypt the database. Unfortunately, this
// // pragma doesn't seem to support prepared statements so we inline the
// // key.
// final String escapedKey = request.key.replaceAll("'", "''");
// db.execute("pragma key = '$escapedKey'");
// // Test that the key is correct by selecting from a table
// db.execute('select count(*) from sqlite_master');
// },
);
// We can use DriftIsolate.inCurrent because this function is the entrypoint
// of a background isolate itself.
final DriftIsolate driftServer = DriftIsolate.inCurrent(
() => DatabaseConnection(databaseImpl),
);
// Inform the main isolate about the server we just created.
request.talkToMain.send(driftServer);
}

View File

@@ -0,0 +1,22 @@
// Package imports:
import 'package:drift/drift.dart';
import 'package:remever/database/database.dart';
import 'package:remever/database/tables.dart';
part 'collections_dao.g.dart';
@DriftAccessor(tables: <Type>[Collections])
class CollectionsDao extends DatabaseAccessor<AppDatabase>
with _$CollectionsDaoMixin {
///
/// Репозиторий для работы с коллекциями
///
CollectionsDao(super.attachedDatabase);
///
/// Получение коллекций из базы данных
///
Stream<List<Collection>> getCollections() {
return db.managers.collections.watch();
}
}

View File

@@ -0,0 +1,8 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'collections_dao.dart';
// ignore_for_file: type=lint
mixin _$CollectionsDaoMixin on DatabaseAccessor<AppDatabase> {
$CollectionsTable get collections => attachedDatabase.collections;
}

View File

@@ -0,0 +1,63 @@
import 'package:drift/drift.dart';
import 'package:flutter/foundation.dart';
import 'package:injectable/injectable.dart';
import 'package:remever/database/dao/collections_dao.dart';
import 'package:remever/database/tables.dart';
import 'connection/connection.dart' as impl;
import 'package:uuid/uuid.dart';
part 'database.g.dart';
@DriftDatabase(
include: <String>{'sql.drift'},
daos: <Type>[CollectionsDao],
tables: <Type>[Collections],
)
@Singleton()
final class AppDatabase extends _$AppDatabase {
AppDatabase() : super(impl.connect());
AppDatabase.fromExcecutor(super.executor);
@override
int get schemaVersion => 1;
@override
MigrationStrategy get migration {
return MigrationStrategy(
beforeOpen: (OpeningDetails details) async {
await enableFK();
// await customStatement('PRAGMA journal_mode = WAL');
},
onCreate: (Migrator migrator) async {
await migrator.createAll();
},
onUpgrade: _onUpgrade,
);
}
Future<void> enableFK() => customStatement('PRAGMA foreign_keys = ON');
Future<void> disableFK() => customStatement('PRAGMA foreign_keys = OFF');
// ignore: long-method
Future<void> _onUpgrade(Migrator m, int from, int to) async {
await customStatement('PRAGMA foreign_keys = OFF');
for (int step = from + 1; step <= to; step++) {
switch (step) {}
}
// Assert that the schema is valid after migrations
if (kDebugMode) {
final List<QueryRow> wrongForeignKeys =
await customSelect('PRAGMA foreign_key_check').get();
assert(
wrongForeignKeys.isEmpty,
'${wrongForeignKeys.map((QueryRow e) => e.data)}',
);
}
}
// --
}

1035
lib/database/database.g.dart Normal file

File diff suppressed because it is too large Load Diff

1
lib/database/sql.drift Normal file
View File

@@ -0,0 +1 @@
import 'tables.dart';

77
lib/database/tables.dart Normal file
View File

@@ -0,0 +1,77 @@
// ignore_for_file: recursive_getters
// Dart imports:
import 'dart:convert';
import 'dart:ffi';
// Package imports:
import 'package:drift/drift.dart';
import 'package:uuid/uuid.dart';
///
/// Примесь для добавления основного ключа в виде текста
/// Для хранения uuid
///
mixin _UuidPrimaryKey on Table {
@override
// ignore: always_specify_types
Set<Column>? get primaryKey => <TextColumn>{id};
/// Идентификатор записи
TextColumn get id =>
text()
.withLength(min: 36, max: 36)
.clientDefault(() => const Uuid().v6())();
}
///
/// Примесь для добавления полей даты
///
mixin _Timestampable on Table {
/// Дата создания
DateTimeColumn get createdAt =>
dateTime()
.named('created_at')
.check(createdAt.isBiggerThan(Constant<DateTime>(DateTime(1950))))
.withDefault(currentDateAndTime)();
/// Дата последней модификации
DateTimeColumn get updatedAt =>
dateTime()
.named('updated_at')
.check(updatedAt.isBiggerThan(Constant<DateTime>(DateTime(1950))))
.withDefault(currentDateAndTime)();
}
///
/// Примесь для добавления полей даты удаления
///
mixin _Deletable on Table {
/// Дата удаления
DateTimeColumn get deletedAt =>
dateTime()
.named('deleted_at')
.nullable()
.check(deletedAt.isBiggerThan(Constant<DateTime>(DateTime(1950))))();
}
/// -- Таблицы --
///
///
///
@DataClassName('Collection')
class Collections extends Table with _UuidPrimaryKey, _Timestampable {
TextColumn get title => text()();
TextColumn get desc => text()();
TextColumn get image => text().nullable()();
TextColumn get payload => text().nullable()();
IntColumn get likesCount => integer().withDefault(Constant(0))();
BoolColumn get isLiked => boolean().withDefault(Constant(false))();
BoolColumn get isPublic => boolean().withDefault(Constant(false))();
BoolColumn get includeInTraining => boolean().withDefault(Constant(false))();
}

View File

@@ -6,7 +6,7 @@
/// Locales: 2
/// Strings: 20 (10 per locale)
///
/// Built on 2025-02-15 at 13:48 UTC
/// Built on 2025-03-03 at 20:52 UTC
// coverage:ignore-file
// ignore_for_file: type=lint, unused_import
@@ -32,8 +32,8 @@ enum AppLocale with BaseAppLocale<AppLocale, Translations> {
const AppLocale({
required this.languageCode,
this.scriptCode, // ignore: unused_element
this.countryCode, // ignore: unused_element
this.scriptCode, // ignore: unused_element, unused_element_parameter
this.countryCode, // ignore: unused_element, unused_element_parameter
});
@override final String languageCode;

View File

@@ -12,6 +12,7 @@
import 'package:get_it/get_it.dart' as _i174;
import 'package:injectable/injectable.dart' as _i526;
import 'database/database.dart' as _i565;
import 'services/auth_interface.dart' as _i78;
import 'services/auth_service.dart' as _i706;
import 'services/core/enc_keys_service.dart' as _i439;
@@ -29,6 +30,7 @@ extension GetItInjectableX on _i174.GetIt {
gh.factory<_i68.LangService>(() => _i68.LangService());
gh.factory<_i439.EncKeysService>(() => _i439.EncKeysService());
gh.factory<_i84.ThemeService>(() => _i84.ThemeService());
gh.singleton<_i565.AppDatabase>(() => _i565.AppDatabase());
gh.singleton<_i78.AuthInterface>(() => _i706.AuthService());
await gh.singletonAsync<_i564.WarmupService>(() {
final i = _i564.WarmupService(

View File

@@ -17,13 +17,18 @@ class AppRouter extends RootStackRouter {
children: <AutoRoute>[
AutoRoute(path: 'settings', page: SettingsRoute.page),
AutoRoute(path: 'statistick', page: StatistickRoute.page),
AutoRoute(path: 'crud_collection', page: CrudCollection.page),
AutoRoute(path: 'create_card', page: CreateRoute.page),
AutoRoute(path: 'collections', page: CollectionRoute.page),
],
),
AutoRoute(path: '/crud_collection', page: CrudCollectionRoute.page),
AutoRoute(path: '/crudFullField', page: CrudCollectionFullscreenField.page),
AutoRoute(path: '/collection_details', page: CollectionDetailRoute.page),
// AutoRoute(path: '/addTags', page: AddTagsRoute.page),
// AutoRoute(path: '/home', page: HomeRoute.page),
// AutoRoute(path: '/logs', page: LogRoute.page),
// AutoRoute(path: '/sandbox', page: SandboxRoute.page),
AutoRoute(path: '/sandbox', page: SandboxRoute.page),
];
@override

View File

@@ -9,25 +9,32 @@
// 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:auto_route/auto_route.dart' as _i12;
import 'package:flutter/cupertino.dart' as _i14;
import 'package:flutter/material.dart' as _i13;
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;
import 'package:remever/screens/collections/collection_detail_screen.dart'
as _i2;
import 'package:remever/screens/collections/collections_screen.dart' as _i3;
import 'package:remever/screens/create_card/create_screen.dart' as _i4;
import 'package:remever/screens/crud_collection/crud_collection.dart' as _i6;
import 'package:remever/screens/crud_collection/widgets/crud_collection_fullscreen_field.dart'
as _i5;
import 'package:remever/screens/home/home_screen.dart' as _i7;
import 'package:remever/screens/sandbox/sandbox_screen.dart' as _i8;
import 'package:remever/screens/settings/settings_screen.dart' as _i9;
import 'package:remever/screens/splash/splash_screen.dart' as _i10;
import 'package:remever/screens/statistick/statistick_screen.dart' as _i11;
/// generated route for
/// [_i1.AuthScreen]
class AuthRoute extends _i8.PageRouteInfo<void> {
const AuthRoute({List<_i8.PageRouteInfo>? children})
class AuthRoute extends _i12.PageRouteInfo<void> {
const AuthRoute({List<_i12.PageRouteInfo>? children})
: super(AuthRoute.name, initialChildren: children);
static const String name = 'AuthRoute';
static _i8.PageInfo page = _i8.PageInfo(
static _i12.PageInfo page = _i12.PageInfo(
name,
builder: (data) {
return const _i1.AuthScreen();
@@ -36,9 +43,25 @@ class AuthRoute extends _i8.PageRouteInfo<void> {
}
/// generated route for
/// [_i2.CollectionScreen]
class CollectionRoute extends _i8.PageRouteInfo<CollectionRouteArgs> {
CollectionRoute({_i9.Key? key, List<_i8.PageRouteInfo>? children})
/// [_i2.CollectionDetailScreen]
class CollectionDetailRoute extends _i12.PageRouteInfo<void> {
const CollectionDetailRoute({List<_i12.PageRouteInfo>? children})
: super(CollectionDetailRoute.name, initialChildren: children);
static const String name = 'CollectionDetailRoute';
static _i12.PageInfo page = _i12.PageInfo(
name,
builder: (data) {
return const _i2.CollectionDetailScreen();
},
);
}
/// generated route for
/// [_i3.CollectionScreen]
class CollectionRoute extends _i12.PageRouteInfo<CollectionRouteArgs> {
CollectionRoute({_i13.Key? key, List<_i12.PageRouteInfo>? children})
: super(
CollectionRoute.name,
args: CollectionRouteArgs(key: key),
@@ -47,13 +70,13 @@ class CollectionRoute extends _i8.PageRouteInfo<CollectionRouteArgs> {
static const String name = 'CollectionRoute';
static _i8.PageInfo page = _i8.PageInfo(
static _i12.PageInfo page = _i12.PageInfo(
name,
builder: (data) {
final args = data.argsAs<CollectionRouteArgs>(
orElse: () => const CollectionRouteArgs(),
);
return _i2.CollectionScreen(key: args.key);
return _i3.CollectionScreen(key: args.key);
},
);
}
@@ -61,7 +84,7 @@ class CollectionRoute extends _i8.PageRouteInfo<CollectionRouteArgs> {
class CollectionRouteArgs {
const CollectionRouteArgs({this.key});
final _i9.Key? key;
final _i13.Key? key;
@override
String toString() {
@@ -70,81 +93,200 @@ class CollectionRouteArgs {
}
/// generated route for
/// [_i3.CrudCollection]
class CrudCollection extends _i8.PageRouteInfo<void> {
const CrudCollection({List<_i8.PageRouteInfo>? children})
: super(CrudCollection.name, initialChildren: children);
/// [_i4.CreateScreen]
class CreateRoute extends _i12.PageRouteInfo<void> {
const CreateRoute({List<_i12.PageRouteInfo>? children})
: super(CreateRoute.name, initialChildren: children);
static const String name = 'CrudCollection';
static const String name = 'CreateRoute';
static _i8.PageInfo page = _i8.PageInfo(
static _i12.PageInfo page = _i12.PageInfo(
name,
builder: (data) {
return const _i3.CrudCollection();
return const _i4.CreateScreen();
},
);
}
/// generated route for
/// [_i4.HomeScreen]
class HomeRoute extends _i8.PageRouteInfo<void> {
const HomeRoute({List<_i8.PageRouteInfo>? children})
/// [_i5.CrudCollectionFullscreenField]
class CrudCollectionFullscreenField
extends _i12.PageRouteInfo<CrudCollectionFullscreenFieldArgs> {
CrudCollectionFullscreenField({
_i14.Key? key,
String title = '',
String? hint,
double height = 92,
List<_i12.PageRouteInfo>? children,
}) : super(
CrudCollectionFullscreenField.name,
args: CrudCollectionFullscreenFieldArgs(
key: key,
title: title,
hint: hint,
height: height,
),
initialChildren: children,
);
static const String name = 'CrudCollectionFullscreenField';
static _i12.PageInfo page = _i12.PageInfo(
name,
builder: (data) {
final args = data.argsAs<CrudCollectionFullscreenFieldArgs>(
orElse: () => const CrudCollectionFullscreenFieldArgs(),
);
return _i5.CrudCollectionFullscreenField(
key: args.key,
title: args.title,
hint: args.hint,
height: args.height,
);
},
);
}
class CrudCollectionFullscreenFieldArgs {
const CrudCollectionFullscreenFieldArgs({
this.key,
this.title = '',
this.hint,
this.height = 92,
});
final _i14.Key? key;
final String title;
final String? hint;
final double height;
@override
String toString() {
return 'CrudCollectionFullscreenFieldArgs{key: $key, title: $title, hint: $hint, height: $height}';
}
}
/// generated route for
/// [_i6.CrudCollectionScreen]
class CrudCollectionRoute extends _i12.PageRouteInfo<CrudCollectionRouteArgs> {
CrudCollectionRoute({
_i14.Key? key,
_i6.CrudType crudType = _i6.CrudType.CREATE,
List<_i12.PageRouteInfo>? children,
}) : super(
CrudCollectionRoute.name,
args: CrudCollectionRouteArgs(key: key, crudType: crudType),
initialChildren: children,
);
static const String name = 'CrudCollectionRoute';
static _i12.PageInfo page = _i12.PageInfo(
name,
builder: (data) {
final args = data.argsAs<CrudCollectionRouteArgs>(
orElse: () => const CrudCollectionRouteArgs(),
);
return _i6.CrudCollectionScreen(key: args.key, crudType: args.crudType);
},
);
}
class CrudCollectionRouteArgs {
const CrudCollectionRouteArgs({
this.key,
this.crudType = _i6.CrudType.CREATE,
});
final _i14.Key? key;
final _i6.CrudType crudType;
@override
String toString() {
return 'CrudCollectionRouteArgs{key: $key, crudType: $crudType}';
}
}
/// generated route for
/// [_i7.HomeScreen]
class HomeRoute extends _i12.PageRouteInfo<void> {
const HomeRoute({List<_i12.PageRouteInfo>? children})
: super(HomeRoute.name, initialChildren: children);
static const String name = 'HomeRoute';
static _i8.PageInfo page = _i8.PageInfo(
static _i12.PageInfo page = _i12.PageInfo(
name,
builder: (data) {
return const _i4.HomeScreen();
return const _i7.HomeScreen();
},
);
}
/// generated route for
/// [_i5.SettingsScreen]
class SettingsRoute extends _i8.PageRouteInfo<void> {
const SettingsRoute({List<_i8.PageRouteInfo>? children})
/// [_i8.SandboxScreen]
class SandboxRoute extends _i12.PageRouteInfo<void> {
const SandboxRoute({List<_i12.PageRouteInfo>? children})
: super(SandboxRoute.name, initialChildren: children);
static const String name = 'SandboxRoute';
static _i12.PageInfo page = _i12.PageInfo(
name,
builder: (data) {
return const _i8.SandboxScreen();
},
);
}
/// generated route for
/// [_i9.SettingsScreen]
class SettingsRoute extends _i12.PageRouteInfo<void> {
const SettingsRoute({List<_i12.PageRouteInfo>? children})
: super(SettingsRoute.name, initialChildren: children);
static const String name = 'SettingsRoute';
static _i8.PageInfo page = _i8.PageInfo(
static _i12.PageInfo page = _i12.PageInfo(
name,
builder: (data) {
return const _i5.SettingsScreen();
return const _i9.SettingsScreen();
},
);
}
/// generated route for
/// [_i6.SplashScreen]
class SplashRoute extends _i8.PageRouteInfo<void> {
const SplashRoute({List<_i8.PageRouteInfo>? children})
/// [_i10.SplashScreen]
class SplashRoute extends _i12.PageRouteInfo<void> {
const SplashRoute({List<_i12.PageRouteInfo>? children})
: super(SplashRoute.name, initialChildren: children);
static const String name = 'SplashRoute';
static _i8.PageInfo page = _i8.PageInfo(
static _i12.PageInfo page = _i12.PageInfo(
name,
builder: (data) {
return const _i6.SplashScreen();
return const _i10.SplashScreen();
},
);
}
/// generated route for
/// [_i7.StatistickScreen]
class StatistickRoute extends _i8.PageRouteInfo<void> {
const StatistickRoute({List<_i8.PageRouteInfo>? children})
/// [_i11.StatistickScreen]
class StatistickRoute extends _i12.PageRouteInfo<void> {
const StatistickRoute({List<_i12.PageRouteInfo>? children})
: super(StatistickRoute.name, initialChildren: children);
static const String name = 'StatistickRoute';
static _i8.PageInfo page = _i8.PageInfo(
static _i12.PageInfo page = _i12.PageInfo(
name,
builder: (data) {
return const _i7.StatistickScreen();
return const _i11.StatistickScreen();
},
);
}

View File

@@ -39,7 +39,10 @@ class AuthCubit extends Cubit<AuthState> {
Future<void> sendCode(String code, String uid) async {
final bool res = await _authInterface.sendCode(code, uid);
if (!res) toInitialState();
if (!res) {
toInitialState();
return;
}
globalRouter.replace(CollectionRoute());
}

View File

@@ -38,7 +38,7 @@ class AuthTextField extends StatelessWidget {
focusedErrorBorder: border,
hintText: 'Введите e-mail',
hintStyle: const TextStyle(fontWeight: FontWeight.w400, height: 1.2),
errorStyle: SemiBold12px().style.copyWith(color: AppColors.red),
errorStyle: SemiBold12px().style.copyWith(color: AppColors.info_red),
suffixIconConstraints: const BoxConstraints(minWidth: 0, minHeight: 0),
suffixIcon: Padding(
padding: const EdgeInsets.only(right: 12).r,

View File

@@ -0,0 +1,186 @@
import 'dart:math';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.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/screens/collections/widgets/learning_card.dart';
import 'package:remever/screens/dialogs/info_dialog.dart';
import 'package:remever/widgets/primary_button.dart';
@RoutePage()
class CollectionDetailScreen extends StatelessWidget {
const CollectionDetailScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.gray_bg,
appBar: _buildAppBar(context),
body: _buildMain(context),
);
}
/// Построение шапки
AppBar _buildAppBar(BuildContext context) {
return AppBar(
toolbarHeight: 56.h,
backgroundColor: AppColors.white,
shadowColor: Colors.transparent,
leading: IconButton(
onPressed: () async {
// context.read<HomeCubit>().toCollection();
context.back();
},
icon: const Icon(CupertinoIcons.left_chevron, color: Colors.black),
),
centerTitle: false,
title: _buildTitle(),
actions: <Widget>[
GestureDetector(
onTap: () {
showCupertinoModalBottomSheet(
topRadius: const Radius.circular(24).r,
backgroundColor: AppColors.white,
context: context,
builder: (BuildContext _) => const InfoDialog(),
);
},
child: Assets.icons.typeDescription.image(height: 24.h, width: 24.w),
),
const WSpace(16),
],
);
}
Widget _buildTitle() {
return Row(
children: <Widget>[_buildAvatar(), const WSpace(5), _buildInfo()],
);
}
///
/// Построение основной информации
///
Widget _buildInfo() {
return Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
// mainAxisSize: MainAxisSize.min,
children: <Widget>[
_buildCollectionTitle(),
const HSpace(4),
_buildCards(),
],
),
);
}
///
/// Построение кол-ва карточек и лайков
///
Widget _buildCards() {
return Row(
children: <Widget>[
Row(
children: <Widget>[
Assets.icons.typeCards.image(
height: 18.h,
width: 18.w,
color: AppColors.disabled,
),
const WSpace(2),
AppTypography(
0.toString(),
type: Regular14px(),
color: AppColors.disabled,
),
],
),
const WSpace(8),
],
);
}
///
/// Название коллекции
///
Widget _buildCollectionTitle() {
return AppTypography(
'Астрономия и тайная комната Харли Хоттера',
type: Medium16px(),
maxLines: 1,
softWrap: true,
color: AppColors.primary,
);
}
///
/// Обложка коллекции
///
Widget _buildAvatar() {
return SizedBox.square(
dimension: 40.r,
child: DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(image: Assets.images.img.provider()),
),
),
);
}
///
/// Построение основного содержимого
///
Widget _buildMain(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16).r,
child: _buildList(context),
);
}
/// Состояние пустого списка
Widget _buildEmptyList(BuildContext context) {
return Column(
children: <Widget>[
const HSpace(40),
Center(child: Assets.images.noData.image(width: 184.w, height: 101.h)),
const Spacer(),
_buildCreateBtn(context),
const HSpace(40),
],
);
}
///Кнопка создания
Widget _buildCreateBtn(BuildContext context) {
return PrimaryButton(
height: 52,
onTap: () {
// context.read<HomeCubit>().toCollection();
context.back();
},
color: AppColors.primary,
child: AppTypography('Создать карточку', type: Regular14px()),
);
}
///
/// Построение списка карточек
///
Widget _buildList(BuildContext context) {
return ListView.builder(
physics: BouncingScrollPhysics(),
itemCount: 10,
itemBuilder:
(BuildContext context, int index) => LearningCard(index: index),
);
}
}

View File

@@ -3,9 +3,17 @@ 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/common/widgets/typography.dart';
import 'package:remever/components/extensions/context.dart';
import 'package:remever/components/notifiers/home_screen_data.dart';
import 'package:remever/database/database.dart';
import 'package:remever/database/tables.dart';
import 'package:remever/inject.dart';
import 'package:remever/router.gr.dart';
import 'package:remever/screens/collections/cubit/collection_cubit.dart';
import 'package:remever/screens/collections/widgets/collection_card.dart';
import 'package:remever/screens/collections/widgets/collections_app_bar.dart';
import 'package:remever/screens/collections/widgets/collections_filters.dart';
@RoutePage()
class CollectionScreen extends StatelessWidget with GetItMixin {
@@ -20,10 +28,9 @@ class CollectionScreen extends StatelessWidget with GetItMixin {
Widget build(BuildContext context) {
return BlocProvider<CollectionCubit>(
create: (context) => CollectionCubit(),
child: Scaffold(
backgroundColor: AppColors.bg,
// appBar: const CollectionsAppBar(),
appBar: const CollectionsAppBar(),
body: _buildMain(context),
floatingActionButton: Builder(
builder: (BuildContext context) {
@@ -32,7 +39,9 @@ class CollectionScreen extends StatelessWidget with GetItMixin {
duration: const Duration(milliseconds: 200),
child: FloatingActionButton(
backgroundColor: AppColors.primary,
onPressed: () {},
onPressed: () {
context.pushRoute(CrudCollectionRoute());
},
// context.read<HomeCubit>().toCrudCollection(CrudType.CREATE),
child: const Icon(Icons.add),
),
@@ -49,17 +58,16 @@ class CollectionScreen extends StatelessWidget with GetItMixin {
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(),
return Column(
children: [
CollectionsFilters(),
state.when(
loading: () => _LoadingList(),
data: () => _CollectionList(),
empty: () => _EmptyList(),
error: () => _ErrorList(),
),
],
);
},
);
@@ -71,7 +79,10 @@ class _LoadingList extends StatelessWidget {
@override
Widget build(BuildContext context) {
return const Placeholder(color: Colors.green);
return SizedBox(
height: MediaQuery.sizeOf(context).height / 2,
child: Center(child: CircularProgressIndicator(color: AppColors.primary)),
);
}
}
@@ -80,7 +91,12 @@ class _ErrorList extends StatelessWidget {
@override
Widget build(BuildContext context) {
return const Placeholder(color: Colors.brown);
return SizedBox(
height: MediaQuery.sizeOf(context).height / 2,
child: Center(
child: AppTypography('Произошла ошибка при загрузке данных'),
),
);
}
}
@@ -89,7 +105,10 @@ class _EmptyList extends StatelessWidget {
@override
Widget build(BuildContext context) {
return const Placeholder(color: Colors.red);
return SizedBox(
height: MediaQuery.sizeOf(context).height / 2,
child: Center(child: AppTypography('Нет доступных коллекций')),
);
}
}
@@ -104,15 +123,41 @@ class _CollectionList extends StatelessWidget {
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(),
),
child: StreamBuilder(
stream: getIt<AppDatabase>().collectionsDao.getCollections(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(color: AppColors.primary),
);
}
if (snapshot.connectionState == ConnectionState.active ||
snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return _ErrorList();
}
if (snapshot.data == null || snapshot.data!.isEmpty) {
return _EmptyList();
}
return ListView.builder(
controller: collectionCubit.collectionController,
itemCount: snapshot.data!.length,
padding: const EdgeInsets.symmetric(horizontal: 16).r,
itemBuilder:
(BuildContext context, int index) => Padding(
padding: const EdgeInsets.only(bottom: 8).r,
child: const CollectionCard(),
),
);
}
return Center(
child: CircularProgressIndicator(color: AppColors.primary),
);
},
),
);
}

View File

@@ -2,13 +2,14 @@ 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/database/database.dart';
import 'package:remever/inject.dart';
part 'collection_state.dart';
part 'collection_cubit.freezed.dart';
class CollectionCubit extends Cubit<CollectionState> {
CollectionCubit() : super(CollectionState.loading());
CollectionCubit() : super(CollectionState.data());
/// Нотифаер домашнего экрана
CollectionData get _cd => getIt<CollectionData>();

View File

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

View File

@@ -0,0 +1,190 @@
import 'dart:math';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_slidable/flutter_slidable.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/collections/widgets/collection_progress_bar.dart';
import 'package:remever/screens/dialogs/action_dialog.dart';
class CollectionCard extends StatelessWidget {
const CollectionCard({super.key});
@override
Widget build(BuildContext context) {
return Slidable(
endActionPane: ActionPane(
extentRatio: 0.62,
motion: const StretchMotion(),
children: <Widget>[
const WSpace(8),
_buildSlidableAction(
context: context,
backgroundColor: const Color(0xFFD7E6F4),
foregroundColor: const Color(0xFF0058AB),
icon: Icons.info_outline,
onPressed: () {},
),
const WSpace(8),
_buildSlidableAction(
context: context,
backgroundColor: const Color(0xFFFFE4E6),
foregroundColor: const Color(0xFFFF5C69),
icon: Icons.favorite_border,
onPressed: () {},
),
const WSpace(8),
_buildSlidableAction(
context: context,
backgroundColor: AppColors.secondary,
foregroundColor: AppColors.primary,
icon: Icons.visibility_off_outlined,
onPressed: () {},
),
],
),
child: GestureDetector(
onTap: () => context.pushRoute(CollectionDetailRoute()),
child: Container(
constraints: BoxConstraints(minHeight: 66.h, maxHeight: 84.h),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12).r,
color: AppColors.white,
),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8).r,
child: Row(
children: <Widget>[
_buildAvatar(),
const WSpace(5),
_buildInfo(),
const Spacer(),
GestureDetector(
onTap: () {
showCuperModalBottomSheet(
context: context,
height: 477.h,
builder: (BuildContext context) => const ActionDialog(),
);
},
child: Assets.icons.typeMenuVertical.image(
height: 24.h,
width: 24.w,
),
),
],
),
),
),
);
}
///
/// Построение основной информации
///
Widget _buildInfo() {
return SizedBox(
width: 230.w,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
_buildTitle(),
const HSpace(4),
_buildLikeAndCardsLength(),
const HSpace(6),
const CollectionProgressBar(),
],
),
);
}
///
/// Построение кол-ва карточек и лайков
///
Widget _buildLikeAndCardsLength() {
return Row(
children: <Widget>[
_buildIconWithText(
icon: Assets.icons.typeCards,
color: AppColors.disabled,
text: Random().nextInt(654).toString(),
),
const WSpace(8),
_buildIconWithText(
icon: Assets.icons.typeLike1818,
color: AppColors.danger,
text: Random().nextInt(7689).toString(),
),
],
);
}
///
/// Название коллекции
///
Widget _buildTitle() {
return AppTypography(
'Астрономия',
type: Medium16px(),
maxLines: 2,
softWrap: true,
);
}
///
/// Обложка коллекции
///
Widget _buildAvatar() {
return SizedBox.square(
dimension: 50.r,
child: DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(image: Assets.images.img.provider()),
),
),
);
}
///
/// Кнопка в меню свайпа
///
Widget _buildSlidableAction({
required BuildContext context,
required Color backgroundColor,
required Color foregroundColor,
required IconData icon,
required VoidCallback onPressed,
}) {
return SlidableAction(
borderRadius: BorderRadius.circular(12).r,
onPressed: (_) => onPressed(),
backgroundColor: backgroundColor,
foregroundColor: foregroundColor,
icon: icon,
);
}
///
/// Построение инфо
///
Widget _buildIconWithText({
required AssetGenImage icon,
required Color color,
required String text,
}) {
return Row(
children: <Widget>[
icon.image(height: 18.h, width: 18.w, color: color),
const WSpace(2),
AppTypography(text, type: Regular14px(), color: color),
],
);
}
}

View File

@@ -0,0 +1,39 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:remever/common/resources.dart';
import 'package:remever/components/extensions/context.dart';
class CollectionProgressBar extends StatelessWidget {
const CollectionProgressBar({super.key});
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Container(
height: 2.h,
width: 230.w,
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(2)).r,
color: AppColors.gray,
),
),
Container(
height: 2.h,
width: Random().nextInt(230).w,
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(2)).r,
gradient: const LinearGradient(
colors: <Color>[
Color(0xFFA7DEDC),
Color(0xFF318D89),
Color(0xFF4633BF),
],
),
),
),
],
);
}
}

View File

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

View File

@@ -8,7 +8,6 @@ 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});
@@ -22,10 +21,12 @@ class CollectionsAppBar extends StatelessWidget implements PreferredSizeWidget {
toolbarHeight: 66.h,
backgroundColor: AppColors.white,
shadowColor: Colors.transparent,
// leading: SizedBox(),
leadingWidth: 0,
title: Row(
children: <Widget>[
GestureDetector(
// onLongPress: () => context.pushRoute(const SandboxRoute()),
onLongPress: () => context.pushRoute(const SandboxRoute()),
child: AppTypography(
'Коллекции',
type: SemiBold28px(),

View File

@@ -0,0 +1,76 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:remever/common/resources.dart';
import 'package:remever/common/widgets/typography.dart';
import 'package:remever/components/extensions/context.dart';
import 'package:remever/components/extensions/state.dart';
import 'package:remever/screens/collections/cubit/collection_cubit.dart';
class CollectionsFilters extends StatefulWidget {
const CollectionsFilters({super.key});
@override
State<CollectionsFilters> createState() => _CollectionsFiltersState();
}
class _CollectionsFiltersState extends State<CollectionsFilters> {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 28).r,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
_buildFilterButton(
context.read<CollectionCubit>().collectionFiltersIndex == 0
? AppColors.white
: AppColors.gray_bg,
'Все',
() {
context.read<CollectionCubit>().collectionFiltersIndex = 0;
safeSetState(() {});
},
),
_buildFilterButton(
context.read<CollectionCubit>().collectionFiltersIndex == 1
? AppColors.white
: AppColors.gray_bg,
'Публичные',
() {
context.read<CollectionCubit>().collectionFiltersIndex = 1;
safeSetState(() {});
},
),
_buildFilterButton(
context.read<CollectionCubit>().collectionFiltersIndex == 2
? AppColors.white
: AppColors.gray_bg,
'Подписки',
() {
context.read<CollectionCubit>().collectionFiltersIndex = 2;
safeSetState(() {});
},
),
],
),
);
}
///
/// Построение кнопки фильтра
///
Widget _buildFilterButton(Color color, String title, void Function()? onTap) {
return GestureDetector(
onTap: onTap,
child: Container(
width: 103.h,
height: 36.h,
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(16)).r,
color: color,
),
child: Center(child: AppTypography(title, type: SemiBold14px())),
),
);
}
}

View File

@@ -0,0 +1,249 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:readmore/readmore.dart';
import 'package:remever/common/functions.dart';
import 'package:remever/common/resources.dart';
import 'package:remever/common/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/gen/assets.gen.dart';
import 'package:remever/screens/dialogs/alert_dialog.dart';
import 'package:remever/screens/dialogs/replace_diaog.dart';
enum CardType { CREATE, SHOW }
class LearningCard extends StatelessWidget {
LearningCard({
required this.index,
super.key,
this.type = CardType.SHOW,
this.onTextTap,
});
final int index;
final CardType type;
void Function()? onTextTap;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 8).r,
child: Slidable(
enabled: type == CardType.SHOW ? true : false,
endActionPane: ActionPane(
extentRatio: 0.62,
motion: const StretchMotion(),
children: <Widget>[
const WSpace(8),
_buildSlidableAction(
context: context,
backgroundColor: const Color(0xFFFFE4E6),
foregroundColor: const Color(0xFFFF5C69),
icon: CupertinoIcons.trash,
onPressed: () {
showCuperModalBottomSheet(
context: context,
height: 262.h,
builder:
(BuildContext context) => const AlertInfoDialog(
title:
'Вы хотите удалить карточку?\nЭто действие необратимо',
acceptTitle: 'Да, удалить',
declineTitle: 'Нет, оставить',
),
);
},
),
const WSpace(8),
_buildSlidableAction(
context: context,
backgroundColor: const Color(0xFFD7E6F4),
foregroundColor: const Color(0xFF0058AB),
icon: CupertinoIcons.repeat,
onPressed: () {},
),
const WSpace(8),
_buildSlidableAction(
context: context,
backgroundColor: AppColors.secondary,
foregroundColor: AppColors.primary,
icon: CupertinoIcons.move,
onPressed: () {
showCuperModalBottomSheet(
context: context,
height: 394.h,
backgroundColor: AppColors.gray_bg,
builder: (BuildContext context) => const ReplaceDialog(),
);
},
),
],
),
child: Container(
width: double.infinity,
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(12)).r,
color: Colors.white,
),
constraints: BoxConstraints(minHeight: 50.h),
child: Stack(
children: <Widget>[
SizedBox(
height: 50.h,
width: double.infinity,
child: DecoratedBox(decoration: getDecoration()),
),
Padding(
padding: const EdgeInsets.all(12).r,
child: Column(
children: <Widget>[
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_buildImage(),
_buildText(context),
Wif(
condition: type == CardType.SHOW,
builder: (BuildContext context) => _editBtm(),
),
],
),
],
),
),
],
),
),
),
);
}
/// Кнопка редактирования
Widget _editBtm() {
return GestureDetector(
onTap: () {},
child: Assets.icons.typeEdit.image(height: 24.h, width: 24.w),
);
}
///
/// Кнопка в меню свайпа
///
Widget _buildSlidableAction({
required BuildContext context,
required Color backgroundColor,
required Color foregroundColor,
required IconData icon,
required VoidCallback onPressed,
}) {
return SlidableAction(
borderRadius: BorderRadius.circular(12).r,
onPressed: (_) => onPressed(),
backgroundColor: backgroundColor,
foregroundColor: foregroundColor,
icon: icon,
);
}
Widget _buildText(BuildContext context) {
return GestureDetector(
onTap: onTextTap,
child: SizedBox(
width:
type == CardType.CREATE
? 250.w
: index % 4 == 0
? 228.w
: 300.w,
child: ReadMoreText(
isExpandable: true,
index % 2 == 0
? 'Наше дело не так однозначно, как может показаться: высокотехнологичная концепция общественного уклада напрямую зависит от вывода текущих активов. Ясность нашей позиции очевидна: высокотехнологичная концепция общественного уклада влечет за собой процесс внедрения и модернизации первоочередных требований. Лишь многие известные личности будут указаны как претенденты на роль ключевых факторов.'
: 'Аргумент или не аргумент, вот вообще хз',
trimMode: TrimMode.Line,
trimLines: 3,
trimCollapsedText: '\nРазвернуть',
trimExpandedText: '\nСвернуть',
style: Regular16px().style,
moreStyle: Regular12px().style.copyWith(
color: AppColors.primary_blue,
),
lessStyle: Regular12px().style.copyWith(
color: AppColors.primary_blue,
),
),
),
);
}
/// Картинка
Widget _buildImage() {
return Wif(
condition: type == CardType.SHOW,
builder: (BuildContext context) {
return Wif(
condition: index % 4 == 0,
builder: (BuildContext context) {
return Padding(
padding: const EdgeInsets.only(right: 8).r,
child: SizedBox.square(
dimension: 64.r,
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8)).r,
image: DecorationImage(
image: Assets.images.imgCard.provider(),
),
),
),
),
);
},
);
},
fallback: (BuildContext context) {
return Padding(
padding: const EdgeInsets.only(right: 8).r,
child: SizedBox.square(
dimension: 64.r,
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8)).r,
gradient: const LinearGradient(
colors: <Color>[Color(0xFFDBD7F4), Color(0xFFB6AAFE)],
begin: Alignment.topRight,
end: Alignment.bottomLeft,
),
),
child: Center(
child: Assets.icons.typePhoto.image(
height: 24.h,
width: 24.w,
color: AppColors.primary,
),
),
),
),
);
},
);
}
/// Декорирование контейнера
BoxDecoration getDecoration() {
return BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(12)).r,
gradient: LinearGradient(
colors: <Color>[
index % 3 == 0 ? AppColors.question : AppColors.answer,
Colors.white,
],
begin: Alignment.topLeft,
end: const Alignment(-0.6, 1),
stops: const <double>[0.25, 0.25],
),
);
}
}

View File

@@ -0,0 +1,252 @@
import 'dart:math';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.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/collections/widgets/collection_progress_bar.dart';
import 'package:remever/screens/collections/widgets/learning_card.dart';
import 'package:remever/widgets/primary_button.dart';
@RoutePage()
class CreateScreen extends StatelessWidget {
const CreateScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.gray_bg,
appBar: _buildAppBar(),
body: _buildMain(context),
);
}
AppBar _buildAppBar() {
return AppBar(
toolbarHeight: 66.h,
backgroundColor: AppColors.white,
shadowColor: Colors.transparent,
title: AppTypography(
'Создать карточку',
type: SemiBold20px(),
color: AppColors.body_text,
),
centerTitle: true,
);
}
Widget _buildMain(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16).r,
child: Column(
children: <Widget>[
Expanded(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const HSpace(16),
_filters(),
const HSpace(16),
AppTypography('Коллекция', type: Medium16px()),
const HSpace(4),
_buildCollection(context),
const HSpace(16),
AppTypography('Вопрос', type: Medium16px()),
const HSpace(4),
LearningCard(
index: 0,
type: CardType.CREATE,
onTextTap: () {
context.pushRoute(
CrudCollectionFullscreenField(
title: 'Вопрос',
hint: '',
height: 313,
),
);
},
),
const HSpace(16),
AppTypography('Ответ', type: Medium16px()),
LearningCard(
index: 1,
type: CardType.CREATE,
onTextTap: () {
context.pushRoute(
CrudCollectionFullscreenField(
title: 'Ответ',
hint: '',
height: 313,
),
);
},
),
revertCard(),
],
),
),
),
_createBtn(),
const HSpace(31),
],
),
);
}
Widget revertCard() {
return SizedBox(
height: 52.h,
child: Row(
children: <Widget>[
Center(
child: Assets.icons.typeFlip2.image(height: 20.h, width: 20.w),
),
const WSpace(2),
Flexible(
fit: FlexFit.tight,
child: AppTypography(
'Создать карточку-наоборот',
type: Medium16px(),
),
),
SizedBox(
height: 20.h,
width: 36.w,
child: FittedBox(
fit: BoxFit.contain,
child: CupertinoSwitch(
activeTrackColor: AppColors.primary,
value: false,
onChanged: (bool value) {},
),
),
),
],
),
);
}
Row _filters() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
_buildFilterButton(AppColors.gray_bg, 'Запомнить', () {}),
_buildFilterButton(AppColors.white, 'Держать в фокусе', () {}),
],
);
}
Widget _createBtn() {
return PrimaryButton(
color: AppColors.primary,
child: AppTypography(
'Создать карточку',
type: Medium14px(),
color: AppColors.white,
),
onTap: () {},
);
}
///
/// Построение кнопки фильтра
///
Widget _buildFilterButton(Color color, String title, void Function()? onTap) {
return GestureDetector(
onTap: onTap,
child: Container(
width: 158.h,
height: 36.h,
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(16)).r,
color: color,
),
child: Center(child: AppTypography(title, type: SemiBold14px())),
),
);
}
Widget _buildCollection(BuildContext context) {
return GestureDetector(
onTap: () {},
child: Container(
constraints: BoxConstraints(minHeight: 66.h, maxHeight: 84.h),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12).r,
color: AppColors.white,
),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8).r,
child: Row(
children: <Widget>[_buildAvatar(), const WSpace(5), _buildInfo()],
),
),
);
}
///
/// Построение основной информации
///
Widget _buildInfo() {
return SizedBox(
width: 230.w,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
_buildTitle(),
const HSpace(4),
Row(
children: <Widget>[
Assets.icons.typeCards.image(
height: 18.h,
width: 18.w,
color: AppColors.disabled,
),
const WSpace(2),
AppTypography(
Random().nextInt(654).toString(),
type: Regular14px(),
color: AppColors.disabled,
),
],
),
const HSpace(6),
const CollectionProgressBar(),
],
),
);
}
///
/// Название коллекции
///
Widget _buildTitle() {
return AppTypography(
'Астрономия',
type: Medium16px(),
maxLines: 2,
softWrap: true,
);
}
///
/// Обложка коллекции
///
Widget _buildAvatar() {
return SizedBox.square(
dimension: 50.r,
child: DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(image: Assets.images.img.provider()),
),
),
);
}
}

View File

@@ -1,12 +1,377 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:remever/common/functions.dart';
import 'package:remever/common/resources.dart';
import 'package:remever/common/toast.dart';
import 'package:remever/common/widgets/bottom_safe_space.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/gen/assets.gen.dart';
import 'package:remever/router.gr.dart';
import 'package:remever/screens/crud_collection/widgets/crud_collection_field.dart';
import 'package:remever/screens/dialogs/alert_dialog.dart';
import 'package:remever/screens/dialogs/tags_dialog.dart';
import 'package:remever/widgets/primary_button.dart';
import '../../../components/extensions/state.dart';
enum CrudType { CREATE, EDIT }
@RoutePage()
class CrudCollection extends StatelessWidget {
const CrudCollection({super.key});
class CrudCollectionScreen extends StatefulWidget {
const CrudCollectionScreen({super.key, this.crudType = CrudType.CREATE});
final CrudType crudType;
@override
State<CrudCollectionScreen> createState() => _CrudCollectionScreenState();
}
class _CrudCollectionScreenState extends State<CrudCollectionScreen> {
/// Флаг публичности коллекции
bool _isPublic = false;
/// Смена публичности
void _setPublic(bool public) {
safeSetState(() => _isPublic = public);
}
@override
Widget build(BuildContext context) {
return const Placeholder(color: Colors.blue);
return Scaffold(
backgroundColor: AppColors.gray_bg,
appBar: _buildAppBar(context),
body: _buildMainBody(context),
);
}
/// Основное тело экрана
Widget _buildMainBody(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16).r,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const HSpace(16),
_buildPhotoAndTitle(context),
const HSpace(16),
..._buildDescription(context),
const HSpace(16),
_buildPublickSwitch(),
const HSpace(16),
AnimatedOpacity(
opacity: _isPublic ? 1 : 0,
duration: const Duration(seconds: 1),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
..._buildTagButton(),
const HSpace(16),
_buildTagsList(),
const HSpace(47),
],
),
),
_buildCreateBtn(context),
const BottomSafeSpace(),
],
),
),
);
}
///Кнопка создания
Widget _buildCreateBtn(BuildContext context) {
return PrimaryButton(
height: 52,
onTap: () {
if (true) {
Toast.showDismissible(
'Для создания публичной коллекции добавьте описание и тэги',
);
return;
}
// context.read<HomeCubit>().toCollection();
},
color: AppColors.primary,
child: AppTypography(
widget.crudType == CrudType.CREATE
? 'Создать коллекцию'
: 'Сохранить изменения',
type: Regular14px(),
color: Colors.white,
),
);
}
/// Построение списка тегов
Widget _buildTagsList() {
return SizedBox(
height: 68.h,
child: Row(
children: <Widget>[
Expanded(
child: Wrap(
runSpacing: 8.r,
spacing: 8.r,
children: List<Widget>.generate(6, (int index) {
return GestureDetector(
onTap: () {},
child: Container(
height: 30,
decoration: BoxDecoration(
borderRadius:
const BorderRadius.all(Radius.circular(6)).r,
color: const Color(0xFFFFE4E6),
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
AppTypography(
'tag $index',
type: Regular14px(),
height: 0.95,
color: AppColors.danger,
),
const WSpace(8),
Center(
child: Icon(
Icons.close,
size: 14.r,
color: AppColors.danger,
),
),
],
),
),
),
);
}),
),
),
const WSpace(9),
GestureDetector(
onTap: () {
showCuperModalBottomSheet(
context: context,
height: 270.h,
builder: (BuildContext context) => const TagsDialog(),
);
},
child: AppTypography(
'+13',
type: Medium16px(),
color: AppColors.primary,
),
),
],
),
);
}
/// Построение кнопки добавления тега
List<Widget> _buildTagButton() {
return <Widget>[
AppTypography('Тэги', type: SemiBold14px()),
const HSpace(4),
CrudCollectionField(
height: 42,
width: 348,
hint: 'Добавить тэг',
// onTap: () => context.pushRoute(const AddTagsRoute()),
),
];
}
/// Построение свитчера на публичность коллекции
Widget _buildPublickSwitch() {
return GestureDetector(
onTap: () => _setPublic(!_isPublic),
child: Row(
children: <Widget>[
SizedBox.square(
dimension: 20.r,
child: Assets.icons.typePublic.image(color: AppColors.primary),
),
const WSpace(2),
Flexible(
fit: FlexFit.tight,
child: AppTypography(
'Публичная коллекция',
type: Medium16px(),
color: AppColors.primary,
),
),
const WSpace(2),
SizedBox(
height: 20.h,
width: 36.w,
child: FittedBox(
fit: BoxFit.contain,
child: CupertinoSwitch(
activeTrackColor: AppColors.primary,
value: _isPublic,
onChanged: _setPublic,
),
),
),
],
),
);
}
/// Построение блока с описанием
List<Widget> _buildDescription(BuildContext context) {
return <Widget>[
AppTypography('Описание', type: SemiBold14px()),
const HSpace(4),
CrudCollectionField(
height: 110,
width: 348,
hint: 'Добавить описание',
onTap: () {
context.pushRoute(
CrudCollectionFullscreenField(title: 'Описание', height: 333),
);
},
),
];
}
/// Построение блока фото и заголовка
Widget _buildPhotoAndTitle(BuildContext context) {
return Row(
children: <Widget>[_buildPhoto(), const WSpace(8), _buildTitle(context)],
);
}
/// Построение поля для ввода заголовка
Widget _buildTitle(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
AppTypography('Название', type: SemiBold14px()),
const HSpace(4),
CrudCollectionField(
height: 91,
width: 225,
hint: 'Добавить название',
onTap: () {
context.pushRoute(
CrudCollectionFullscreenField(
title: 'Название',
hint: 'Максимальное количество символов - 250',
),
);
},
),
],
);
}
/// Построение обложки
Widget _buildPhoto() {
return SizedBox.square(
dimension: 115.r,
child: DecoratedBox(
decoration: const BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: <Color>[Color(0xFFB6AAFE), Color(0xFFDBD7F4)],
begin: Alignment.bottomLeft,
end: Alignment.topRight,
),
),
child: SizedBox.square(
dimension: 32.r,
child: Center(
child: Assets.icons.typePhoto.image(
height: 32.h,
width: 32.w,
color: AppColors.primary,
),
),
),
),
);
}
/// Построение шапки
AppBar _buildAppBar(BuildContext context) {
return AppBar(
toolbarHeight: 56.h,
backgroundColor: AppColors.white,
shadowColor: Colors.transparent,
leading: IconButton(
onPressed: () async {
if (widget.crudType == CrudType.EDIT) {
final bool? res = await showCuperModalBottomSheet(
context: context,
height: 262.h,
builder:
(BuildContext context) => const AlertInfoDialog(
title: 'Вы хотите сбросить все внесенные изменения?',
acceptTitle: 'Да, сбросить',
declineTitle: 'Нет, оставить',
),
);
if (res != null && res) context.back();
} else {
context.back();
}
},
icon: const Icon(CupertinoIcons.left_chevron, color: Colors.black),
),
centerTitle: true,
title: GestureDetector(
onLongPress: () => context.pushRoute(const SandboxRoute()),
child: AppTypography(
widget.crudType == CrudType.CREATE
? 'Создать коллекцию'
: 'Редактировать',
type: SemiBold20px(),
color: AppColors.body_text,
),
),
actions: <Widget>[
Wif(
condition: widget.crudType == CrudType.EDIT,
builder: (BuildContext context) {
return Padding(
padding: const EdgeInsets.only(right: 16).r,
child: GestureDetector(
onTap: () {
showCuperModalBottomSheet(
context: context,
height: 262.h,
builder:
(BuildContext context) => const AlertInfoDialog(
title: 'Вы хотите сбросить все внесенные изменения?',
acceptTitle: 'Да, сбросить',
declineTitle: 'Нет, оставить',
),
);
},
child: Assets.icons.typeTrash.image(
height: 24.h,
width: 24.w,
color: AppColors.primary,
),
),
);
},
),
],
);
}
}

View File

@@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import 'package:remever/common/resources.dart';
import 'package:remever/common/widgets/typography.dart';
import 'package:remever/components/extensions/context.dart';
class CrudCollectionField extends StatelessWidget {
const CrudCollectionField({
super.key,
this.height = 90,
this.width = 100,
this.onTap,
this.hint = 'Hint',
this.content,
});
final double height;
final double width;
final void Function()? onTap;
final String hint;
final String? content;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
height: height.h,
width: width.w,
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: const BorderRadius.all(Radius.circular(8)).r,
),
child: Padding(
padding: const EdgeInsets.all(12).r,
child:
content != null
? AppTypography(hint, maxLines: 99, type: Regular16px())
: AppTypography(
hint,
maxLines: 99,
type: Regular14px(),
color: AppColors.disabled,
),
),
),
);
}
}

View File

@@ -0,0 +1,211 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_keyboard_size/flutter_keyboard_size.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/screens/dialogs/alert_dialog.dart';
@RoutePage()
class CrudCollectionFullscreenField extends StatelessWidget {
CrudCollectionFullscreenField({
super.key,
this.title = '',
this.hint,
this.height = 92,
});
final String title;
final double height;
final String? hint;
final TextEditingController _controller = TextEditingController();
@override
Widget build(BuildContext context) {
return KeyboardSizeProvider(
child: Scaffold(
backgroundColor: AppColors.gray_bg,
appBar: _buildAppBar(context),
body: _buildMainBody(context),
),
);
}
/// Построение основного тела экрана
Widget _buildMainBody(BuildContext context) {
return Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16).r,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const HSpace(16),
_buildField(context),
if (hint != null) ...<Widget>[
const HSpace(16),
AppTypography(
hint!,
type: Regular14px(),
color: AppColors.disabled,
),
],
],
),
),
const Spacer(),
_buildMenu(),
],
);
}
/// Построение интерактивной плашки меню
Widget _buildMenu() {
return Consumer<ScreenHeight>(
builder: (BuildContext context, ScreenHeight res, Widget? child) {
return AnimatedOpacity(
opacity: res.isOpen ? 1 : 0,
duration: const Duration(milliseconds: 500),
child: Container(
height: 64.h,
decoration: BoxDecoration(
color: AppColors.white,
border: Border(
top: BorderSide(color: AppColors.gray, width: 1.w),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
GestureDetector(
onTap: () {},
child: Assets.icons.typePaste.image(
height: 24.h,
width: 24.w,
),
),
GestureDetector(
onTap: () {},
child: Assets.icons.typeCopy.image(height: 24.h, width: 24.w),
),
GestureDetector(
onTap: () => context.back(),
child: SizedBox.square(
dimension: 32.r,
child: DecoratedBox(
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: AppColors.primary,
),
child: Center(
child: Assets.icons.typeCheck.image(
height: 24.h,
width: 24.w,
color: AppColors.white,
),
),
),
),
),
],
),
),
);
},
);
}
/// Построение поля ввода
Widget _buildField(BuildContext context) {
return SizedBox(
height: height.h,
child: DecoratedBox(
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: const BorderRadius.all(Radius.circular(12)).r,
),
child: Padding(
padding: const EdgeInsets.all(12).r,
child: TextField(
autofocus: true,
controller: _controller,
textCapitalization: TextCapitalization.sentences,
maxLines: 99,
cursorColor: AppColors.danger,
decoration: const InputDecoration.collapsed(
hintText: 'Введите содержимое',
hintStyle: TextStyle(color: AppColors.gray),
),
),
),
),
);
}
/// Построение шапки
AppBar _buildAppBar(BuildContext context) {
return AppBar(
toolbarHeight: 56.h,
backgroundColor: AppColors.white,
shadowColor: Colors.transparent,
leading: IconButton(
onPressed: () async {
context.back();
},
icon: const Icon(CupertinoIcons.left_chevron, color: Colors.black),
),
centerTitle: true,
title: GestureDetector(
onLongPress: () async {
final bool? res = await showCuperModalBottomSheet(
context: context,
height: 262.h,
builder:
(BuildContext context) => const AlertInfoDialog(
title:
'Вы хотите выйти из режима создания описания коллекции?',
acceptTitle: 'Выйти, не сохранять',
declineTitle: 'Сохранить и выйти',
),
);
if (res != null && res) context.back();
},
child: AppTypography(
title,
type: SemiBold20px(),
color: AppColors.body_text,
),
),
actions: <Widget>[
Padding(
padding: const EdgeInsets.only(right: 16).r,
child: GestureDetector(
onTap: () {
showCuperModalBottomSheet(
context: context,
height: 262.h,
builder:
(BuildContext context) => const AlertInfoDialog(
title: 'Вы хотите сбросить все внесенные изменения?',
acceptTitle: 'Да, сбросить',
declineTitle: 'Нет, оставить',
),
);
},
child: Assets.icons.typeTrash.image(
height: 24.h,
width: 24.w,
color: AppColors.danger,
),
),
),
],
);
}
}

View File

@@ -0,0 +1,106 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';
import 'package:remever/common/functions.dart';
import 'package:remever/common/resources.dart';
import 'package:remever/common/widgets/bottom_safe_space.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/crud_collection/crud_collection.dart';
import 'package:remever/screens/dialogs/alert_dialog.dart';
import 'package:remever/screens/dialogs/dialog_header.dart';
import 'package:remever/screens/dialogs/dialog_item.dart';
class ActionDialog extends StatelessWidget {
const ActionDialog({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
const DialogHeader(title: 'Действия'),
DialogItem(
title: 'Публичная коллекция',
dimension: 36,
child: FittedBox(
fit: BoxFit.contain,
child: CupertinoSwitch(
activeTrackColor: AppColors.primary,
value: true,
onChanged: (bool value) {},
),
),
onTap: () {
showCuperModalBottomSheet(
context: context,
height: 282.h,
builder:
(BuildContext context) => const AlertInfoDialog(
title:
'Коллекция станет видна всем пользователям сервиса.\nЕё будет проще найти по тэгам ;)',
acceptTitle: 'Позже добавлю',
declineTitle: 'Добавить тэги',
),
);
},
),
DialogItem(
title: 'Исключена из тренировки',
child: Assets.icons.typeHide.image(color: AppColors.primary),
onTap: () {
Navigator.pop(context);
},
),
DialogItem(
title: 'Редактировать',
child: Assets.icons.typeEdit.image(color: AppColors.primary),
onTap: () {
// context.back();
context.pushRoute(CrudCollectionRoute(crudType: CrudType.EDIT));
},
),
DialogItem(
title: 'Статистика',
child: Assets.icons.typeStat.image(color: AppColors.primary),
onTap: () {
Navigator.pop(context);
},
),
DialogItem(
title: 'Скачать',
child: Assets.icons.typeDownload.image(color: AppColors.primary),
onTap: () {
Navigator.pop(context);
},
),
DialogItem(
title: 'Поделиться',
child: Assets.icons.typeShare.image(color: AppColors.primary),
onTap: () {
Navigator.pop(context);
},
),
DialogItem(
title: 'Удалить',
color: AppColors.danger,
onTap: () {
showCuperModalBottomSheet(
context: context,
height: 262.h,
builder:
(BuildContext context) => const AlertInfoDialog(
title:
'Вы хотите удалить коллекцию?\nЭто действие необратимо',
acceptTitle: 'Да, удалить',
declineTitle: 'Нет, оставить',
),
);
},
child: Assets.icons.typeTrash.image(color: AppColors.danger),
),
const BottomSafeSpace(),
],
);
}
}

View File

@@ -0,0 +1,110 @@
import 'package:flutter/material.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/widgets/primary_button.dart';
class AlertInfoDialog extends StatelessWidget {
const AlertInfoDialog({
super.key,
this.acceptTitle = '',
this.declineTitle = '',
this.title = '',
});
final String? title;
final String? acceptTitle;
final String? declineTitle;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16).r,
child: Stack(
children: <Widget>[
Column(
children: <Widget>[
const HSpace(32),
_buildDanger(),
const HSpace(24),
AppTypography(
title!,
type: Medium16px(),
maxLines: 3,
textAlign: TextAlign.center,
),
const HSpace(24),
Material(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
PrimaryButton(
width: 170,
height: 52,
color: AppColors.danger,
onTap: () => Navigator.pop(context, true),
child: AppTypography(
acceptTitle!,
type: Medium14px(),
color: AppColors.white,
),
),
PrimaryButton(
width: 170,
height: 52,
color: AppColors.primary,
onTap: () => Navigator.pop(context, false),
child: AppTypography(
declineTitle!,
type: Medium14px(),
color: AppColors.white,
),
),
],
),
),
],
),
Align(
alignment: Alignment.topRight,
child: Padding(
padding: const EdgeInsets.only(top: 16).r,
child: GestureDetector(
onTap: () => Navigator.pop(context),
child: SizedBox.square(
dimension: 24.r,
child: DecoratedBox(
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: AppColors.gray_bg,
),
child: Assets.icons.typeClose.image(
color: AppColors.disabled,
),
),
),
),
),
),
],
),
);
}
Widget _buildDanger() {
return SizedBox.square(
dimension: 56.r,
child: DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: const Color(0xFFFFE4E6).withOpacity(0.4),
),
child: Center(
child: Assets.icons.typeDanger.image(height: 24.h, width: 24.w),
),
),
);
}
}

View File

@@ -65,7 +65,7 @@ class DialogHeader extends StatelessWidget {
decoration: _closeButtonDecoration,
child: Center(
child: Assets.icons.typeClose.image(
color: AppColors.gray,
color: AppColors.disabled,
height: _iconSize.h,
width: _iconSize.w,
),

View File

@@ -23,7 +23,7 @@ class DialogItem extends StatelessWidget {
final double dimension;
// Константы для стилей и отступов
static final double _itemHeight = 56.h;
static final double _itemHeight = 52.h;
static const BoxDecoration _itemDecoration = BoxDecoration(
border: Border(bottom: BorderSide(color: AppColors.gray, width: 0.5)),
);
@@ -37,7 +37,7 @@ class DialogItem extends StatelessWidget {
return GestureDetector(
onTap: onTap,
child: Container(
height: _itemHeight.h,
height: _itemHeight,
decoration: _itemDecoration,
child: Padding(
padding: _itemPadding,
@@ -62,7 +62,7 @@ class DialogItem extends StatelessWidget {
Widget _buildTitle() {
return AppTypography(
title,
color: color ?? AppColors.black,
color: color ?? Colors.black,
type: _regular17Style,
);
}

View File

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

View File

@@ -0,0 +1,96 @@
import 'package:flutter/material.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/widgets/primary_button.dart';
class InfoDialog extends StatelessWidget {
const InfoDialog({super.key});
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(color: Colors.white),
constraints: BoxConstraints(
minHeight: MediaQuery.sizeOf(context).height / 6,
maxHeight: MediaQuery.sizeOf(context).height / 1.1,
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 16).r,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Row(
children: <Widget>[
_buildAvatar(),
const WSpace(5),
Flexible(child: _buildCollectionTitle()),
],
),
const HSpace(8),
_buildText(),
const HSpace(16),
PrimaryButton(
onTap: () => Navigator.of(context).pop(),
color: AppColors.primary,
child: AppTypography(
'Закрыть',
type: Medium14px(),
color: AppColors.white,
),
),
],
),
),
);
}
Widget _buildText() {
return Flexible(
child: SingleChildScrollView(
controller: ScrollController(),
child: AppTypography(
'Предварительные выводы неутешительны: убеждённость некоторых оппонентов способствует подготовке и реализации форм воздействия. Как принято считать, предприниматели в сети интернет представляют собой не что иное, как квинтэссенцию победы маркетинга над разумом и должны быть описаны максимально подробно.'
'Идейные соображения высшего порядка, а также высокотехнологичная концепция общественного уклада говорит о возможностях кластеризации усилий. Задача организации, в особенности же реализация намеченных плановых заданий выявляет срочную потребность первоочередных требований. Вот вам яркий пример современных тенденций — существующая теория обеспечивает широкому кругу (специалистов) участие в формировании поставленных обществом задач. Имеется спорная точка зрения, гласящая примерно следующее: сторонники тоталитаризма в науке освещают чрезвычайно интересные особенности картины в целом, однако конкретные выводы, разумеется, представлены в исключительно положительном свете. Лишь базовые сценарии поведения пользователей представляют собой не что иное, как квинтэссенцию победы маркетинга над разумом и должны быть функционально разнесены на независимые элементы. Каждый из нас понимает очевидную вещь: начало повседневной работы по формированию позиции способствует повышению качества распределения внутренних резервов и ресурсов.'
'В своём стремлении улучшить пользовательский опыт мы упускаем, что тщательные исследования конкурентов набирают популярность среди определенных слоев населения, а значит, должны быть представлены в исключительно положительном свете. Противоположная точка зрения подразумевает, что непосредственные участники технического прогресса преданы социально-демократической анафеме. Вот вам яркий пример современных тенденций — выбранный нами инновационный путь не оставляет шанса для поставленных обществом задач. Задача организации, в особенности же понимание сути ресурсосберегающих технологий влечет за собой процесс внедрения и модернизации новых принципов формирования материально-технической и кадровой базы. В своём стремлении улучшить пользовательский опыт мы упускаем, что непосредственные участники технического прогресса ассоциативно распределены по отраслям. Высокий уровень вовлечения представителей целевой аудитории является четким доказательством простого факта: синтетическое тестирование влечет за собой процесс внедрения и модернизации системы массового участия!'
'Сложно сказать, почему стремящиеся вытеснить традиционное производство, нанотехнологии призваны к ответу. Но предприниматели в сети интернет, инициированные исключительно синтетически, призваны к ответу. Следует отметить, что экономическая повестка сегодняшнего дня способствует подготовке и реализации глубокомысленных рассуждений. Равным образом, укрепление и развитие внутренней структуры в значительной степени обусловливает важность соответствующих условий активизации. Противоположная точка зрения подразумевает, что диаграммы связей будут призваны к ответу. И нет сомнений, что независимые государства формируют глобальную экономическую сеть и при этом — ограничены исключительно образом мышления.'
'И нет сомнений, что реплицированные с зарубежных источников, современные исследования освещают чрезвычайно интересные особенности картины в целом, однако конкретные выводы, разумеется, разоблачены! Мы вынуждены отталкиваться от того, что современная методология разработки говорит о возможностях как самодостаточных, так и внешне зависимых концептуальных решений. Повседневная практика показывает, что курс на социально-ориентированный национальный проект требует от нас анализа распределения внутренних резервов и ресурсов. Задача организации, в особенности же курс на социально-ориентированный национальный проект представляет собой интересный эксперимент проверки укрепления моральных ценностей. Значимость этих проблем настолько очевидна, что социально-экономическое развитие говорит о возможностях кластеризации усилий. Не следует, однако, забывать, что дальнейшее развитие различных форм деятельности в значительной степени обусловливает важность прогресса профессионального сообщества.'
'А ещё реплицированные с зарубежных источников, современные исследования могут быть превращены в посмешище, хотя само их существование приносит несомненную пользу обществу. Задача организации, в особенности же социально-экономическое развитие является качественно новой ступенью системы обучения кадров, соответствующей насущным потребностям. В целом, конечно, начало повседневной работы по формированию позиции требует анализа поставленных обществом задач. Но курс на социально-ориентированный национальный проект говорит о возможностях экспериментов, поражающих по своей масштабности и грандиозности. Прежде всего, существующая теория создаёт предпосылки для экономической целесообразности принимаемых решений. Внезапно, представители современных социальных резервов лишь добавляют фракционных разногласий и разоблачены.'
'В рамках спецификации современных стандартов, действия представителей оппозиции ассоциативно распределены по отраслям. Не следует, однако, забывать, что новая модель организационной деятельности позволяет оценить значение соответствующих условий активизации. Значимость этих проблем настолько очевидна, что курс на социально-ориентированный национальный проект позволяет выполнить важные задания по разработке глубокомысленных рассуждений. Современные технологии достигли такого уровня, что начало повседневной работы по формированию позиции обеспечивает широкому кругу (специалистов) участие в формировании инновационных методов управления процессами. Имеется спорная точка зрения, гласящая примерно следующее: активно развивающиеся страны третьего мира будут объявлены нарушающими общечеловеческие нормы этики и морали. Картельные сговоры не допускают ситуации, при которой диаграммы связей будут обнародованы.',
type: Regular14px(),
maxLines: 9999,
),
),
);
}
///
/// Название коллекции
///
Widget _buildCollectionTitle() {
return AppTypography(
'Астрономия и тайная комната Харли Хоттера',
type: Medium16px(),
maxLines: 2,
softWrap: true,
color: AppColors.primary,
);
}
///
/// Обложка коллекции
///
Widget _buildAvatar() {
return SizedBox.square(
dimension: 40.r,
child: DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(image: Assets.images.img.provider()),
),
),
);
}
}

View File

@@ -0,0 +1,167 @@
import 'dart:math';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.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/collections/widgets/collection_progress_bar.dart';
import 'package:remever/screens/dialogs/dialog_header.dart';
class ReplaceDialog extends StatelessWidget {
const ReplaceDialog({super.key});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const DialogHeader(title: 'Переместить карточку'),
const HSpace(16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 28).r,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
AppTypography('из коллекции', type: Medium16px()),
const HSpace(8),
_buildCollection(context),
const HSpace(16),
Center(
child: SizedBox.square(
dimension: 34.r,
child: DecoratedBox(
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: AppColors.gray,
),
child: Center(
child: Assets.icons.typeArrowDown.image(
height: 18.h,
width: 18.w,
),
),
),
),
),
const HSpace(16),
AppTypography('в коллекцию', type: Medium16px()),
const HSpace(8),
_buildCollection(context),
const HSpace(16),
_createBtn(context),
],
),
),
],
);
}
Widget _createBtn(BuildContext context) {
return GestureDetector(
onTap: () {
Navigator.of(context).pop();
// context.read<HomeCubit>().toCrudCollection(CrudType.CREATE);
context.pushRoute(CrudCollectionRoute());
},
child: Row(
children: <Widget>[
SizedBox.square(
dimension: 20.r,
child: DecoratedBox(
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: AppColors.gray,
),
child: Assets.icons.typePlus.image(color: Colors.black54),
),
),
const WSpace(4),
AppTypography('Создать и перенести в новую', type: Regular16px()),
],
),
);
}
Widget _buildCollection(BuildContext context) {
return GestureDetector(
onTap: () {},
child: Container(
constraints: BoxConstraints(minHeight: 66.h, maxHeight: 84.h),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12).r,
color: AppColors.white,
),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8).r,
child: Row(
children: <Widget>[_buildAvatar(), const WSpace(5), _buildInfo()],
),
),
);
}
///
/// Построение основной информации
///
Widget _buildInfo() {
return SizedBox(
width: 230.w,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
_buildTitle(),
const HSpace(4),
Row(
children: <Widget>[
Assets.icons.typeCards.image(
height: 18.h,
width: 18.w,
color: AppColors.disabled,
),
const WSpace(2),
AppTypography(
Random().nextInt(654).toString(),
type: Regular14px(),
color: AppColors.disabled,
),
],
),
const HSpace(6),
const CollectionProgressBar(),
],
),
);
}
///
/// Название коллекции
///
Widget _buildTitle() {
return AppTypography(
'Астрономия',
type: Medium16px(),
maxLines: 2,
softWrap: true,
);
}
///
/// Обложка коллекции
///
Widget _buildAvatar() {
return SizedBox.square(
dimension: 50.r,
child: DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(image: Assets.images.img.provider()),
),
),
);
}
}

View File

@@ -0,0 +1,103 @@
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/screens/dialogs/alert_dialog.dart';
import 'package:remever/screens/dialogs/dialog_header.dart';
class TagsDialog extends StatelessWidget {
const TagsDialog({super.key});
@override
Widget build(BuildContext context) {
return Column(children: <Widget>[_header(context), _body(context)]);
}
/// Тело диалога
Widget _body(BuildContext context) {
return Expanded(
child: Padding(
padding: const EdgeInsets.all(16).r,
child: Wrap(
runSpacing: 8.r,
spacing: 8.r,
children: List<Widget>.generate(11, (int index) {
return GestureDetector(
onTap: () {},
child: Container(
height: 30,
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(6)).r,
color: const Color(0xFFFFE4E6),
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
AppTypography(
'tag $index',
type: Regular14px(),
height: 0.95,
color: AppColors.danger,
),
const WSpace(8),
Center(
child: Icon(
Icons.close,
size: 14.r,
color: AppColors.danger,
),
),
],
),
),
),
);
}),
),
),
);
}
/// Шапка
Widget _header(BuildContext context) {
return DialogHeader(
paddingSize: 16,
title: 'Тэги',
action: GestureDetector(
onTap: () {
showCuperModalBottomSheet(
context: context,
height: 282.h,
builder:
(BuildContext context) => const AlertInfoDialog(
title: 'Удалить все тэги?\nЭто действие необратимо',
acceptTitle: 'Да, удалить все',
declineTitle: 'Нет, оставьте',
),
);
},
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
SizedBox.square(
dimension: 24.r,
child: Assets.icons.typeTrash.image(color: AppColors.danger),
),
const WSpace(4),
AppTypography(
'Удалить все',
type: Medium16px(),
color: AppColors.danger,
),
],
),
),
);
}
}

View File

@@ -26,18 +26,21 @@ class HomeScreen extends StatelessWidget {
routes: <PageRouteInfo>[
SettingsRoute(),
StatistickRoute(),
CrudCollection(),
CreateRoute(),
CollectionRoute(),
],
bottomNavigationBuilder: (_, TabsRouter tabsRouter) {
return SizedBox(
height: 73.h,
child: Stack(
alignment: Alignment.bottomCenter,
children: <Widget>[
_buildBackgroundBar(tabsRouter),
_buildCentralButton(),
],
return ColoredBox(
color: AppColors.bg,
child: SizedBox(
height: 73.h,
child: Stack(
alignment: Alignment.bottomCenter,
children: <Widget>[
_buildBackgroundBar(tabsRouter),
_buildCentralButton(),
],
),
),
);
},

View File

@@ -0,0 +1,129 @@
import 'package:auto_route/annotations.dart';
import 'package:drift/src/runtime/api/runtime_api.dart';
import 'package:drift_db_viewer/drift_db_viewer.dart';
import 'package:flutter/material.dart';
import 'package:get_it_mixin/get_it_mixin.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:remever/common/widgets/typography.dart';
import 'package:remever/common/widgets/wspace.dart';
import 'package:remever/database/database.dart';
import '../../components/notifiers/app_settings.dart';
import '../../components/env.dart';
import '../../inject.dart';
@RoutePage()
class SandboxScreen extends StatefulWidget {
const SandboxScreen({super.key});
@override
State<SandboxScreen> createState() => _SandboxScreenState();
}
class _SandboxScreenState extends State<SandboxScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const AppTypography('Песочница')),
body: ListView(
padding: const EdgeInsets.all(16),
children: <Widget>[
_buildVersion(),
const HSpace(8),
Text('${getIt<Env>().runtimeType}:${getIt<Env>().url}'),
const HSpace(8),
_debugBox(),
const HSpace(8),
],
),
);
}
Widget _debugBox() {
return DecoratedBox(
decoration: const BoxDecoration(
border: Border.fromBorderSide(
BorderSide(width: 1, color: Colors.black),
),
borderRadius: BorderRadius.all(Radius.circular(4)),
),
child: Column(
children: <Widget>[
const AppTypography('debug'),
ShowFpsSetting(),
EnableDebugSetting(),
ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute<dynamic>(
builder: (BuildContext context) {
return DriftDbViewer(getIt<AppDatabase>());
},
),
);
},
child: const Text('Open Db Viewer'),
),
],
),
);
}
Widget _buildVersion() {
return FutureBuilder<PackageInfo>(
future: PackageInfo.fromPlatform(),
builder: (BuildContext context, AsyncSnapshot<PackageInfo> snapshot) {
return AppTypography(
'Версия: ${snapshot.data?.version ?? '-'}'
'+${snapshot.data?.buildNumber ?? '-'}',
type: Bold14px(),
//color: Colors.black,
);
},
);
}
}
class ShowFpsSetting extends StatelessWidget with GetItMixin {
///
/// Виджет для контроля показа FPS
///
ShowFpsSetting({super.key});
@override
Widget build(BuildContext context) {
final bool showFPS = watchOnly<AppSettingsNotifier, bool>(
(AppSettingsNotifier e) => e.showFps,
);
return SwitchListTile.adaptive(
value: showFPS,
title: const Text('Show FPS'),
activeColor: Colors.blueAccent,
onChanged: (_) {
if (context.mounted) get<AppSettingsNotifier>().toggleFps();
},
);
}
}
class EnableDebugSetting extends StatelessWidget with GetItMixin {
///
/// Виджет для включения общего дебага
///
EnableDebugSetting({super.key});
@override
Widget build(BuildContext context) {
final bool debugMode = watchOnly<AppSettingsNotifier, bool>(
(AppSettingsNotifier e) => e.debugMode,
);
return SwitchListTile.adaptive(
value: debugMode,
title: const Text('Debug mode'),
activeColor: Colors.blueAccent,
onChanged: (_) => get<AppSettingsNotifier>().toggleDebugMode(),
);
}
}

View File

@@ -1,6 +1,5 @@
// Flutter imports:
import 'package:flutter/material.dart';
import 'package:remever/common/resources.dart';
class CustomTheme extends ValueNotifier<ThemeMode> {
/// Текущая тема
@@ -26,66 +25,13 @@ class CustomTheme extends ValueNotifier<ThemeMode> {
/// Темная тема
///
static ThemeData get darkTheme {
return ThemeData.dark(useMaterial3: true);
return ThemeData.dark(useMaterial3: false);
}
///
/// Светлая тема
///
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,
),
);
return ThemeData(useMaterial3: false);
}
}

View File

@@ -7,6 +7,8 @@ class PrimaryButton extends StatefulWidget {
const PrimaryButton({
required this.child,
required this.onTap,
this.color = AppColors.primary,
super.key,
this.height = 52,
this.width = double.infinity,
@@ -16,6 +18,7 @@ class PrimaryButton extends StatefulWidget {
final double height;
final double width;
final Function() onTap;
final Color color;
@override
State<PrimaryButton> createState() => _PrimaryButtonState();
@@ -38,7 +41,7 @@ class _PrimaryButtonState extends State<PrimaryButton> {
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(16)).r,
color: AppColors.primary,
color: widget.color,
),
child: Center(
child: