Files
Avtex/lib/screens/mind_map_screen.dart
T
Alexyz f1e0f6b5ef feat: Add Nanaxe server with Docker, GitHub Actions CI/CD, and auto-deployment
- Add complete server infrastructure with Gitea, API, PostgreSQL
- Implement Obsidian-compatible markdown mindmap converter
- Add Flutter service for server synchronization
- Configure GitHub Actions for automated build and deploy
- Add production docker-compose with health checks
- Include VPS setup and deployment scripts
- Add comprehensive deployment documentation
2026-04-19 22:18:25 +05:00

381 lines
12 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:firebase_auth/firebase_auth.dart';
import '../providers/mind_maps_provider.dart';
import '../models/mindmap_model.dart';
import '../core/admin_access_service.dart';
import 'mind_map_viewer_screen.dart';
import 'mind_map_editor_screen.dart';
import 'mind_map_interactive_screen.dart';
class MindMapScreen extends ConsumerWidget {
const MindMapScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final mindMapsAsync = ref.watch(mindMapsProvider);
final isAdmin = ref.watch(isAdminProvider);
final currentUserEmail = FirebaseAuth.instance.currentUser?.email ?? '';
return Scaffold(
appBar: AppBar(
title: const Text('Mind Maps'),
actions: [
if (isAdmin)
IconButton(
icon: const Icon(Icons.admin_panel_settings),
onPressed: () => _showAdminOptions(context),
tooltip: 'Админ панель',
),
],
),
body: mindMapsAsync.when(
data: (mindMaps) =>
_buildContent(context, mindMaps, isAdmin, currentUserEmail, ref),
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 48, color: Colors.red),
const SizedBox(height: 16),
Text('Ошибка: $error'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => ref.invalidate(mindMapsProvider),
child: const Text('Повторить'),
),
],
),
),
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () => _showCreateOptions(context, ref),
icon: const Icon(Icons.add),
label: const Text('Создать'),
),
);
}
Widget _buildContent(
BuildContext context,
List<MindMapModel> mindMaps,
bool isAdmin,
String currentUserEmail,
WidgetRef ref,
) {
// Разделяем на основные и личные
final mainMaps = mindMaps.where((m) => m.isMain).toList();
final personalMaps = mindMaps.where((m) => !m.isMain).toList();
if (mindMaps.isEmpty) {
return _buildEmptyState(context, ref);
}
return ListView(
padding: const EdgeInsets.all(16),
children: [
// Основные карты
if (mainMaps.isNotEmpty) ...[
_buildSectionHeader('Основные', Icons.star, Colors.amber),
...mainMaps.map((map) =>
_buildMindMapCard(context, map, isAdmin, currentUserEmail, ref)),
const SizedBox(height: 16),
],
// Личные карты
if (personalMaps.isNotEmpty) ...[
_buildSectionHeader('Личные', Icons.person, Colors.blue),
...personalMaps.map((map) =>
_buildMindMapCard(context, map, isAdmin, currentUserEmail, ref)),
],
],
);
}
Widget _buildSectionHeader(String title, IconData icon, Color color) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Row(
children: [
Icon(icon, color: color, size: 24),
const SizedBox(width: 8),
Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
);
}
Widget _buildMindMapCard(
BuildContext context,
MindMapModel mindMap,
bool isAdmin,
String currentUserEmail,
WidgetRef ref,
) {
final isOwner = mindMap.ownerEmail == currentUserEmail;
final canDelete = isAdmin || isOwner;
final canEdit = isAdmin || isOwner;
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: ListTile(
leading: CircleAvatar(
backgroundColor: mindMap.isMain ? Colors.amber : Colors.blue,
child: Icon(
mindMap.isMain ? Icons.star : Icons.account_tree,
color: Colors.white,
),
),
title: Text(
mindMap.title,
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (mindMap.description.isNotEmpty)
Text(mindMap.description,
maxLines: 1, overflow: TextOverflow.ellipsis),
const SizedBox(height: 4),
Text('Владелец: ${mindMap.ownerEmail}'),
Text(
'${mindMap.nodeCount} узлов • ${_formatDate(mindMap.createdAt)}'),
],
),
trailing: PopupMenuButton<String>(
onSelected: (value) => _handleAction(context, value, mindMap, ref),
itemBuilder: (context) => [
PopupMenuItem(
value: 'view',
child: Row(
children: [
const Icon(Icons.visibility, size: 20),
const SizedBox(width: 12),
const Text('Открыть'),
],
),
),
if (canEdit)
PopupMenuItem(
value: 'edit',
child: Row(
children: [
const Icon(Icons.edit, size: 20),
const SizedBox(width: 12),
const Text('Редактировать'),
],
),
),
if (canDelete)
PopupMenuItem(
value: 'delete',
child: Row(
children: [
const Icon(Icons.delete, color: Colors.red, size: 20),
const SizedBox(width: 12),
const Text('Удалить', style: TextStyle(color: Colors.red)),
],
),
),
],
),
onTap: () => _openMindMap(context, mindMap),
),
);
}
Widget _buildEmptyState(BuildContext context, WidgetRef ref) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.account_tree, size: 100, color: Colors.grey[400]),
const SizedBox(height: 24),
Text(
'Нет mind карт',
style: TextStyle(fontSize: 20, color: Colors.grey[600]),
),
const SizedBox(height: 8),
Text(
'Создайте первую карту вручную или с помощью AI',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey[500]),
),
const SizedBox(height: 32),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton.icon(
onPressed: () => _showCreateManualDialog(context, ref),
icon: const Icon(Icons.add),
label: const Text('Вручную'),
),
const SizedBox(width: 16),
FilledButton.icon(
onPressed: () => _showCreateAIDialog(context, ref),
icon: const Icon(Icons.auto_awesome),
label: const Text('AI Генерация'),
),
],
),
],
),
);
}
void _showCreateOptions(BuildContext context, WidgetRef ref) {
showModalBottomSheet(
context: context,
builder: (context) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.add, color: Colors.blue),
title: const Text('Создать вручную'),
subtitle: const Text('Пустая mind map'),
onTap: () {
Navigator.pop(context);
_showCreateManualDialog(context, ref);
},
),
ListTile(
leading: const Icon(Icons.auto_awesome, color: Colors.purple),
title: const Text('AI Генерация'),
subtitle: const Text('С помощью DeepSeek'),
onTap: () {
Navigator.pop(context);
_showCreateAIDialog(context, ref);
},
),
],
),
),
);
}
void _showCreateManualDialog(BuildContext context, WidgetRef ref) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const MindMapEditorScreen(),
),
);
}
void _showCreateAIDialog(BuildContext context, WidgetRef ref) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const MindMapEditorScreen(),
),
);
}
void _handleAction(
BuildContext context,
String action,
MindMapModel mindMap,
WidgetRef ref,
) {
switch (action) {
case 'view':
_openMindMap(context, mindMap);
break;
case 'edit':
_editMindMap(context, mindMap, ref);
break;
case 'delete':
_confirmDelete(context, mindMap, ref);
break;
}
}
void _openMindMap(BuildContext context, MindMapModel mindMap) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => MindMapInteractiveScreen(mindMap: mindMap),
),
);
}
void _editMindMap(BuildContext context, MindMapModel mindMap, WidgetRef ref) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => MindMapEditorScreen(mindMap: mindMap),
),
);
}
void _confirmDelete(
BuildContext context, MindMapModel mindMap, WidgetRef ref) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Удалить mind map?'),
content: Text(
'"${mindMap.title}" будет удалена навсегда. Это действие нельзя отменить.'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Отмена'),
),
TextButton(
onPressed: () async {
try {
await ref
.read(mindMapsNotifierProvider.notifier)
.deleteMindMap(mindMap.id, mindMap.ownerEmail);
if (context.mounted) {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('✅ Mind map удалена'),
backgroundColor: Colors.green,
),
);
}
} catch (e) {
if (context.mounted) {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('❌ Ошибка: $e'),
backgroundColor: Colors.red,
),
);
}
}
},
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('Удалить'),
),
],
),
);
}
void _showAdminOptions(BuildContext context) {
// TODO: Открыть админ панель
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Админ панель в разработке')),
);
}
String _formatDate(DateTime date) {
return '${date.day.toString().padLeft(2, '0')}.'
'${date.month.toString().padLeft(2, '0')}.'
'${date.year}';
}
}