501 lines
14 KiB
Dart
501 lines
14 KiB
Dart
import 'dart:io';
|
||
import 'dart:typed_data';
|
||
|
||
import 'package:auto_route/auto_route.dart';
|
||
import 'package:file_picker/file_picker.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/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/database/database.dart';
|
||
import 'package:remever/gen/assets.gen.dart';
|
||
import 'package:remever/inject.dart';
|
||
import 'package:remever/models/crud_collection_dto.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/services/collection/collections_interface.dart';
|
||
import 'package:remever/widgets/primary_button.dart';
|
||
|
||
import '../../../components/extensions/state.dart';
|
||
|
||
@RoutePage()
|
||
class CrudCollectionScreen extends StatefulWidget {
|
||
const CrudCollectionScreen({super.key, this.editedCollection});
|
||
|
||
final Collection? editedCollection;
|
||
|
||
@override
|
||
State<CrudCollectionScreen> createState() => _CrudCollectionScreenState();
|
||
}
|
||
|
||
class _CrudCollectionScreenState extends State<CrudCollectionScreen> {
|
||
late CrudCollectionDto _collection;
|
||
bool _isPublic = false;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_initializeCollection();
|
||
}
|
||
|
||
void _initializeCollection() {
|
||
_collection = CrudCollectionDto(
|
||
desc: widget.editedCollection?.desc ?? '',
|
||
title: widget.editedCollection?.title ?? '',
|
||
isPublic: widget.editedCollection?.isPublic ?? false,
|
||
avatar: widget.editedCollection?.image,
|
||
);
|
||
_isPublic = _collection.isPublic;
|
||
}
|
||
|
||
Future<void> _pickImage() async {
|
||
final result = await FilePicker.platform.pickFiles();
|
||
|
||
if (result?.files.single.path case final String? path?) {
|
||
try {
|
||
final bytes = await File(path!).readAsBytes();
|
||
_updateCollection(avatar: bytes);
|
||
} catch (e) {
|
||
showErrorToast('Не удалось загрузить изображение');
|
||
}
|
||
} else {
|
||
showErrorToast('Файл не выбран');
|
||
}
|
||
}
|
||
|
||
void _updateCollection({
|
||
String? title,
|
||
String? desc,
|
||
bool? isPublic,
|
||
Uint8List? avatar,
|
||
}) {
|
||
_collection = _collection.copyWith(
|
||
title: title ?? _collection.title,
|
||
desc: desc ?? _collection.desc,
|
||
isPublic: isPublic ?? _collection.isPublic,
|
||
avatar: avatar ?? _collection.avatar,
|
||
);
|
||
safeSetState(() {});
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
backgroundColor: AppColors.gray_bg,
|
||
appBar: _buildAppBar(),
|
||
body: _buildMainBody(),
|
||
);
|
||
}
|
||
|
||
Widget _buildMainBody() {
|
||
return Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 16).r,
|
||
child: SingleChildScrollView(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
const HSpace(16),
|
||
_buildPhotoAndTitle(),
|
||
const HSpace(16),
|
||
..._buildDescription(),
|
||
const HSpace(16),
|
||
// _buildPublicSwitch(),
|
||
const HSpace(16),
|
||
AnimatedOpacity(
|
||
// opacity: _isPublic ? 1 : 0,
|
||
opacity: 0,
|
||
duration: const Duration(milliseconds: 300),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
..._buildTagButton(),
|
||
const HSpace(16),
|
||
_buildTagsList(),
|
||
const HSpace(47),
|
||
],
|
||
),
|
||
),
|
||
_buildCreateBtn(),
|
||
const BottomSafeSpace(),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildCreateBtn() {
|
||
return PrimaryButton(
|
||
height: 52,
|
||
onTap: _handleCreateOrUpdate,
|
||
color: AppColors.primary,
|
||
child: AppTypography(
|
||
widget.editedCollection == null
|
||
? 'Создать коллекцию'
|
||
: 'Сохранить изменения',
|
||
type: Regular14px(),
|
||
color: Colors.white,
|
||
),
|
||
);
|
||
}
|
||
|
||
Future<void> _handleCreateOrUpdate() async {
|
||
if (!_isCollectionValid()) return;
|
||
|
||
if (!_hasChanges()) {
|
||
context.back();
|
||
}
|
||
|
||
try {
|
||
final collectionService = getIt<CollectionsInterface>();
|
||
|
||
widget.editedCollection != null
|
||
? await collectionService.updateCollection(
|
||
_collection,
|
||
widget.editedCollection!.id,
|
||
)
|
||
: await collectionService.createCollection(_collection);
|
||
|
||
context.back();
|
||
} catch (e) {
|
||
showErrorToast(
|
||
'Ошибка при ${widget.editedCollection != null ? 'обновлении' : 'создании'} коллекции',
|
||
);
|
||
}
|
||
}
|
||
|
||
bool _isCollectionValid() {
|
||
if (_collection.title.isEmpty && _collection.desc.isEmpty) {
|
||
showErrorToast('Для создания коллекции добавьте название и описание');
|
||
return false;
|
||
}
|
||
|
||
if (_isPublic && _collection.desc.isEmpty) {
|
||
showErrorToast(
|
||
'Для создания публичной коллекции добавьте описание и тэги',
|
||
);
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
Widget _buildTagsList() {
|
||
return SizedBox(
|
||
height: 68.h,
|
||
child: Row(
|
||
children: [
|
||
Expanded(
|
||
child: Wrap(
|
||
runSpacing: 8.r,
|
||
spacing: 8.r,
|
||
children: List.generate(6, _buildTagItem),
|
||
),
|
||
),
|
||
const WSpace(9),
|
||
_buildAddTagButton(),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildTagItem(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: [
|
||
AppTypography(
|
||
'tag $index',
|
||
type: Regular14px(),
|
||
height: 0.95,
|
||
color: AppColors.danger,
|
||
),
|
||
const WSpace(8),
|
||
Icon(Icons.close, size: 14.r, color: AppColors.danger),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildAddTagButton() {
|
||
return GestureDetector(
|
||
onTap: _showTagsDialog,
|
||
child: AppTypography('+13', type: Medium16px(), color: AppColors.primary),
|
||
);
|
||
}
|
||
|
||
void _showTagsDialog() {
|
||
showCuperModalBottomSheet(
|
||
context: context,
|
||
height: 270.h,
|
||
builder: (_) => const TagsDialog(),
|
||
);
|
||
}
|
||
|
||
List<Widget> _buildTagButton() {
|
||
return [
|
||
AppTypography('Тэги', type: SemiBold14px()),
|
||
const HSpace(4),
|
||
CrudCollectionField(height: 42, width: 348, hint: 'Добавить тэг'),
|
||
];
|
||
}
|
||
|
||
Widget _buildPublicSwitch() {
|
||
return GestureDetector(
|
||
onTap: _togglePublic,
|
||
child: Row(
|
||
children: [
|
||
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,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
void _togglePublic() => _setPublic(!_isPublic);
|
||
|
||
void _setPublic(bool isPublic) {
|
||
_updateCollection(isPublic: isPublic);
|
||
safeSetState(() => _isPublic = isPublic);
|
||
}
|
||
|
||
List<Widget> _buildDescription() {
|
||
return [
|
||
AppTypography('Описание', type: SemiBold14px()),
|
||
const HSpace(4),
|
||
CrudCollectionField(
|
||
height: 110,
|
||
width: 348,
|
||
hint: 'Добавить описание',
|
||
content: _collection.desc,
|
||
onTap:
|
||
() => _navigateToFullscreenField(
|
||
title: 'Описание',
|
||
height: 333,
|
||
content: _collection.desc,
|
||
onResult: (result) => _updateCollection(desc: result ?? ''),
|
||
),
|
||
),
|
||
];
|
||
}
|
||
|
||
Widget _buildTitle() {
|
||
return Column(
|
||
children: [
|
||
AppTypography('Название', type: SemiBold14px()),
|
||
const HSpace(4),
|
||
CrudCollectionField(
|
||
height: 91,
|
||
width: 225,
|
||
hint: 'Добавить название',
|
||
content: _collection.title,
|
||
onTap:
|
||
() => _navigateToFullscreenField(
|
||
title: 'Название',
|
||
hint: 'Максимальное количество символов - 250',
|
||
content: _collection.title,
|
||
onResult: (result) => _updateCollection(title: result ?? ''),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
void _navigateToFullscreenField({
|
||
required String title,
|
||
String? hint,
|
||
String? content,
|
||
required Function(String?) onResult,
|
||
double height = 91,
|
||
}) {
|
||
context.pushRoute(
|
||
CrudCollectionFullscreenField(
|
||
title: title,
|
||
hint: hint,
|
||
height: height,
|
||
content: content,
|
||
onEditingComplete: onResult,
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildPhotoAndTitle() {
|
||
return Row(
|
||
children: [
|
||
_buildPhoto(),
|
||
const WSpace(8),
|
||
Expanded(child: _buildTitle()),
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget _buildPhoto() {
|
||
return GestureDetector(
|
||
onTap: _pickImage,
|
||
child: SizedBox.square(
|
||
dimension: 115.r,
|
||
child: DecoratedBox(
|
||
decoration: const BoxDecoration(
|
||
shape: BoxShape.circle,
|
||
gradient: LinearGradient(
|
||
colors: [Color(0xFFB6AAFE), Color(0xFFDBD7F4)],
|
||
begin: Alignment.bottomLeft,
|
||
end: Alignment.topRight,
|
||
),
|
||
),
|
||
child: Wif(
|
||
condition: _collection.avatar != null,
|
||
builder:
|
||
(_) => ClipOval(
|
||
child: Image.memory(
|
||
_collection.avatar!,
|
||
fit: BoxFit.cover,
|
||
errorBuilder: (_, __, ___) => _buildPhotoPlaceholder(),
|
||
),
|
||
),
|
||
fallback: (_) => _buildPhotoPlaceholder(),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildPhotoPlaceholder() {
|
||
return SizedBox.square(
|
||
dimension: 32.r,
|
||
child: Center(
|
||
child: Assets.icons.typePhoto.image(
|
||
height: 32.h,
|
||
width: 32.w,
|
||
color: AppColors.primary,
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
AppBar _buildAppBar() {
|
||
return AppBar(
|
||
toolbarHeight: 56.h,
|
||
backgroundColor: AppColors.white,
|
||
shadowColor: Colors.transparent,
|
||
leading: IconButton(
|
||
onPressed: _handleBackPress,
|
||
icon: const Icon(CupertinoIcons.left_chevron, color: Colors.black),
|
||
),
|
||
centerTitle: true,
|
||
title: GestureDetector(
|
||
onLongPress: () => context.pushRoute(const SandboxRoute()),
|
||
child: AppTypography(
|
||
widget.editedCollection == null
|
||
? 'Создать коллекцию'
|
||
: 'Редактировать',
|
||
type: SemiBold20px(),
|
||
color: AppColors.body_text,
|
||
),
|
||
),
|
||
actions: [
|
||
if (widget.editedCollection != null && _hasChanges())
|
||
Padding(
|
||
padding: const EdgeInsets.only(right: 16).r,
|
||
child: GestureDetector(
|
||
onTap: _showResetDialog,
|
||
child: Assets.icons.typeTrash.image(
|
||
height: 24.h,
|
||
width: 24.w,
|
||
color: AppColors.danger,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
Future<void> _handleBackPress() async {
|
||
if (widget.editedCollection != null) {
|
||
final shouldExit = await _showExitDialog();
|
||
if (shouldExit == true) context.back();
|
||
} else {
|
||
context.back();
|
||
}
|
||
}
|
||
|
||
Future<bool?> _showExitDialog() async {
|
||
// Показываем диалог только если есть редактируемая коллекция и есть изменения
|
||
if (widget.editedCollection != null && _hasChanges()) {
|
||
return showCuperModalBottomSheet<bool>(
|
||
context: context,
|
||
height: 262.h,
|
||
builder:
|
||
(_) => const AlertInfoDialog(
|
||
title: 'Вы хотите сбросить все внесенные изменения?',
|
||
acceptTitle: 'Да, сбросить',
|
||
declineTitle: 'Нет, оставить',
|
||
),
|
||
);
|
||
}
|
||
return true;
|
||
}
|
||
|
||
bool _hasChanges() {
|
||
// Если нет редактируемой коллекции, значит это создание новой
|
||
if (widget.editedCollection == null) return false;
|
||
|
||
// Сравниваем все поля
|
||
return _collection.title != widget.editedCollection!.title ||
|
||
_collection.desc != widget.editedCollection!.desc ||
|
||
_collection.isPublic != widget.editedCollection!.isPublic ||
|
||
_collection.avatar != widget.editedCollection!.image;
|
||
}
|
||
|
||
void _showResetDialog() {
|
||
_showExitDialog().then((result) {
|
||
if (result == true) {
|
||
_initializeCollection();
|
||
safeSetState(() {});
|
||
}
|
||
});
|
||
}
|
||
}
|