f1e0f6b5ef
- 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
381 lines
12 KiB
Dart
381 lines
12 KiB
Dart
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}';
|
||
}
|
||
}
|