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:path_provider/path_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/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'; import 'package:path/path.dart' as path; @RoutePage() class CrudCollectionScreen extends StatefulWidget { const CrudCollectionScreen({super.key, this.editedCollection}); final Collection? editedCollection; @override State createState() => _CrudCollectionScreenState(); } class _CrudCollectionScreenState extends State { 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 _pickImage() async { final result = await FilePicker.platform.pickFiles(); if (result?.files.single.path case final String? originPath?) { try { // Получаем директорию документов final Directory directory = await getApplicationDocumentsDirectory(); final String collectionsDirPath = path.join( directory.path, 'collections', ); final Directory collectionsDir = Directory(collectionsDirPath); // Создаём директорию рекурсивно if (!(await collectionsDir.exists())) { await collectionsDir.create(recursive: true); } final String fileName = path.basename(originPath!); final String destinationPath = path.join(collectionsDirPath, fileName); await File(originPath).copy(destinationPath); _updateCollection(avatar: destinationPath); } catch (e) { showErrorToast('Не удалось загрузить изображение'); } } else { showErrorToast('Файл не выбран'); } } void _updateCollection({ String? title, String? desc, bool? isPublic, String? 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 _handleCreateOrUpdate() async { if (!_isCollectionValid()) return; if (!_hasChanges()) { context.back(); } try { final collectionService = getIt(); 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 _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 _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.file( File(_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 _handleBackPress() async { if (widget.editedCollection != null) { final shouldExit = await _showExitDialog(); if (shouldExit == true) context.back(); } else { context.back(); } } Future _showExitDialog() async { // Показываем диалог только если есть редактируемая коллекция и есть изменения if (widget.editedCollection != null && _hasChanges()) { return showCuperModalBottomSheet( 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(() {}); } }); } }