import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:uuid/uuid.dart'; import '../models/mindmap_model.dart'; import '../providers/mind_maps_provider.dart'; import '../core/admin_access_service.dart'; import '../core/deepseek_service.dart'; class MindMapEditorScreen extends ConsumerStatefulWidget { final MindMapModel? mindMap; // null = создание новой const MindMapEditorScreen({super.key, this.mindMap}); @override ConsumerState createState() => _MindMapEditorScreenState(); } class _MindMapEditorScreenState extends ConsumerState { late TextEditingController _titleController; late TextEditingController _descController; late List> _nodes; late bool _isMain; bool _isSaving = false; bool _isGeneratingAI = false; @override void initState() { super.initState(); _titleController = TextEditingController(text: widget.mindMap?.title ?? ''); _descController = TextEditingController(text: widget.mindMap?.description ?? ''); _nodes = widget.mindMap?.nodes.toList() ?? []; _isMain = widget.mindMap?.isMain ?? false; } @override void dispose() { _titleController.dispose(); _descController.dispose(); super.dispose(); } Future _save() async { if (_titleController.text.trim().isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Введите название')), ); return; } setState(() => _isSaving = true); try { final notifier = ref.read(mindMapsNotifierProvider.notifier); if (widget.mindMap == null) { // Создание новой await notifier.createMindMap( title: _titleController.text.trim(), description: _descController.text.trim(), nodes: _nodes, isMain: _isMain, ); } else { // Обновление существующей final updated = widget.mindMap!.copyWith( title: _titleController.text.trim(), description: _descController.text.trim(), nodes: _nodes, isMain: _isMain, updatedAt: DateTime.now(), ); await notifier.updateMindMap(widget.mindMap!.id, updated); } if (mounted) { Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(widget.mindMap == null ? '✅ Mind map создана' : '✅ Mind map сохранена'), backgroundColor: Colors.green, ), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('❌ Ошибка: $e'), backgroundColor: Colors.red), ); } } finally { setState(() => _isSaving = false); } } Future _addNode({String? parentId}) async { final title = await _showNodeTitleDialog(); if (title == null || title.isEmpty) return; final parentNode = parentId != null ? _nodes.firstWhere((n) => n['id'] == parentId, orElse: () => {}) : null; final newNode = { 'id': const Uuid().v4(), 'title': title, 'level': parentNode != null ? (parentNode['level'] ?? 0) + 1 : 0, 'parentId': parentId, 'color': _getColorForLevel(parentNode != null ? (parentNode['level'] ?? 0) + 1 : 0), }; setState(() => _nodes.add(newNode)); } Future _addChildNode(String parentId) async { final title = await _showNodeTitleDialog(); if (title == null || title.isEmpty) return; final parentNode = _nodes.firstWhere((n) => n['id'] == parentId); final newNode = { 'id': const Uuid().v4(), 'title': title, 'level': (parentNode['level'] ?? 0) + 1, 'parentId': parentId, 'color': _getColorForLevel((parentNode['level'] ?? 0) + 1), }; setState(() => _nodes.add(newNode)); } void _deleteNode(String nodeId) { // Удаляем узел и всех его потомков final nodesToDelete = {nodeId}; _collectChildren(nodeId, nodesToDelete); setState(() { _nodes.removeWhere((n) => nodesToDelete.contains(n['id'])); }); } void _collectChildren(String parentId, Set result) { final children = _nodes.where((n) => n['parentId'] == parentId).toList(); for (final child in children) { final childId = child['id'] as String; result.add(childId); _collectChildren(childId, result); } } Future _showNodeTitleDialog() async { final controller = TextEditingController(); return showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Название узла'), content: TextField( controller: controller, decoration: const InputDecoration( labelText: 'Введите название', border: OutlineInputBorder(), ), autofocus: true, ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Отмена'), ), ElevatedButton( onPressed: () => Navigator.pop(context, controller.text), child: const Text('Добавить'), ), ], ), ); } Future _generateWithAI() async { if (_titleController.text.trim().isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Введите название проекта')), ); return; } setState(() => _isGeneratingAI = true); try { final projectName = _titleController.text.trim(); final projectDesc = _descController.text.trim(); final nodesData = await deepSeekService.generateMindMap( projectName: projectName, description: projectDesc.isNotEmpty ? projectDesc : 'Mind map для проекта', ); // Преобразуем и добавляем к существующим узлам final newNodes = >[]; for (var i = 0; i < nodesData.length; i++) { final nodeData = nodesData[i]; final level = nodeData['level'] ?? 0; final parentIdIndex = nodeData['parentId'] as int?; // Находим реальный parentId по индексу String? actualParentId; if (parentIdIndex != null && parentIdIndex < newNodes.length) { actualParentId = newNodes[parentIdIndex]['id'] as String; } newNodes.add({ 'id': const Uuid().v4(), 'title': nodeData['title'] ?? '', 'level': level, 'parentId': actualParentId, 'color': _getColorForLevel(level), }); } // Добавляем новые узлы к существующим setState(() { _nodes.addAll(newNodes); }); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('✅ Добавлено ${newNodes.length} узлов'), backgroundColor: Colors.green, ), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('❌ Ошибка: $e'), backgroundColor: Colors.red), ); } } finally { setState(() => _isGeneratingAI = false); } } String _getColorForLevel(int level) { final colors = [ '#2196F3', '#4CAF50', '#FF9800', '#E91E63', '#9C27B0', '#00BCD4' ]; return colors[level % colors.length]; } @override Widget build(BuildContext context) { final isAdmin = ref.watch(isAdminProvider); return Scaffold( appBar: AppBar( title: Text(widget.mindMap == null ? 'Новая Mind Map' : 'Редактировать'), actions: [ // Переключатель Общая/Личная (только для админов) if (isAdmin) Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.person, color: _isMain ? Colors.grey : Colors.white), Switch( value: _isMain, onChanged: (value) => setState(() => _isMain = value), ), Icon(Icons.star, color: _isMain ? Colors.amber : Colors.grey), ], ), ), // AI Генерация IconButton( icon: _isGeneratingAI ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.auto_awesome), onPressed: _isGeneratingAI ? null : _generateWithAI, tooltip: 'AI Дополнение', ), // Сохранить IconButton( icon: _isSaving ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.save), onPressed: _isSaving ? null : _save, tooltip: 'Сохранить', ), ], ), body: Column( children: [ // Заголовок и описание Padding( padding: const EdgeInsets.all(16), child: Column( children: [ TextField( controller: _titleController, decoration: const InputDecoration( labelText: 'Название', border: OutlineInputBorder(), prefixIcon: Icon(Icons.title), ), ), const SizedBox(height: 16), TextField( controller: _descController, decoration: const InputDecoration( labelText: 'Описание', border: OutlineInputBorder(), prefixIcon: Icon(Icons.description), ), maxLines: 2, ), if (isAdmin) Padding( padding: const EdgeInsets.only(top: 8), child: Row( children: [ Icon( _isMain ? Icons.star : Icons.person, color: _isMain ? Colors.amber : Colors.blue, ), const SizedBox(width: 8), Text( _isMain ? 'Основная (видна всем)' : 'Личная (только вам)', style: TextStyle( color: _isMain ? Colors.amber : Colors.blue, fontWeight: FontWeight.bold, ), ), ], ), ), ], ), ), const Divider(), // Список узлов Expanded( child: _nodes.isEmpty ? _buildEmptyNodesState() : _buildNodesTree(), ), // Кнопки добавления Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Theme.of(context).cardColor, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 4, offset: const Offset(0, -2), ), ], ), child: Row( children: [ Expanded( child: ElevatedButton.icon( onPressed: () => _addNode(), icon: const Icon(Icons.add), label: const Text('Корневой узел'), ), ), const SizedBox(width: 16), Expanded( child: FilledButton.icon( onPressed: _isGeneratingAI ? null : _generateWithAI, icon: const Icon(Icons.auto_awesome), label: const Text('AI Дополнить'), ), ), ], ), ), ], ), ); } Widget _buildEmptyNodesState() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.account_tree, size: 80, color: Colors.grey[400]), const SizedBox(height: 16), Text( 'Нет узлов', style: TextStyle(fontSize: 20, color: Colors.grey[600]), ), const SizedBox(height: 8), Text( 'Добавьте узлы вручную или с помощью AI', style: TextStyle(color: Colors.grey[500]), ), ], ), ); } Widget _buildNodesTree() { // Находим корневые узлы (без parentId) final rootNodes = _nodes.where((n) => n['parentId'] == null).toList(); return ListView.builder( padding: const EdgeInsets.all(16), itemCount: rootNodes.length, itemBuilder: (context, index) { return _buildNodeWidget(rootNodes[index]); }, ); } Widget _buildNodeWidget(Map node) { final nodeId = node['id'] as String; final title = node['title'] as String; final level = node['level'] as int; final color = _hexToColor(node['color'] as String? ?? '#2196F3'); final children = _nodes.where((n) => n['parentId'] == nodeId).toList(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( margin: EdgeInsets.only(left: level * 24.0), decoration: BoxDecoration( color: color.withOpacity(0.1), border: Border(left: BorderSide(color: color, width: 4)), borderRadius: BorderRadius.circular(8), ), child: ListTile( leading: CircleAvatar( backgroundColor: color, child: Text( '${level + 1}', style: const TextStyle(color: Colors.white, fontSize: 12), ), ), title: Text( title, style: const TextStyle(fontWeight: FontWeight.bold), ), subtitle: Text('Уровень $level'), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( icon: const Icon(Icons.add_circle, color: Colors.green), onPressed: () => _addChildNode(nodeId), tooltip: 'Добавить дочерний', ), IconButton( icon: const Icon(Icons.delete, color: Colors.red), onPressed: () => _confirmDeleteNode(nodeId, title), tooltip: 'Удалить', ), ], ), ), ), // Рекурсивно строим дочерние узлы ...children.map((child) => _buildNodeWidget(child)), ], ); } void _confirmDeleteNode(String nodeId, String title) { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Удалить узел?'), content: Text('"$title" и все дочерние узлы будут удалены'), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Отмена'), ), TextButton( onPressed: () { _deleteNode(nodeId); Navigator.pop(context); }, style: TextButton.styleFrom(foregroundColor: Colors.red), child: const Text('Удалить'), ), ], ), ); } Color _hexToColor(String hex) { hex = hex.replaceAll('#', ''); if (hex.length == 6) { return Color(int.parse('FF$hex', radix: 16)); } return const Color(0xFF2196F3); } }