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
194 lines
5.9 KiB
Dart
194 lines
5.9 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:reactive_mind_map/reactive_mind_map.dart' as rm;
|
|
import '../models/mindmap_model.dart';
|
|
|
|
class MindMapViewerScreen extends StatefulWidget {
|
|
final MindMapModel mindMap;
|
|
|
|
const MindMapViewerScreen({super.key, required this.mindMap});
|
|
|
|
@override
|
|
State<MindMapViewerScreen> createState() => _MindMapViewerScreenState();
|
|
}
|
|
|
|
class _MindMapViewerScreenState extends State<MindMapViewerScreen> {
|
|
rm.MindMapLayout _currentLayout = rm.MindMapLayout.radial;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text(widget.mindMap.title),
|
|
actions: [
|
|
PopupMenuButton<rm.MindMapLayout>(
|
|
icon: const Icon(Icons.account_tree),
|
|
tooltip: 'Тип layout',
|
|
onSelected: (layout) => setState(() => _currentLayout = layout),
|
|
itemBuilder: (context) => [
|
|
_popupMenuItem(
|
|
rm.MindMapLayout.radial, 'Радиальный', Icons.circle_outlined),
|
|
_popupMenuItem(
|
|
rm.MindMapLayout.right, 'Вправо', Icons.arrow_forward),
|
|
_popupMenuItem(rm.MindMapLayout.left, 'Влево', Icons.arrow_back),
|
|
_popupMenuItem(rm.MindMapLayout.horizontal, 'Горизонтальный',
|
|
Icons.swap_horiz),
|
|
_popupMenuItem(
|
|
rm.MindMapLayout.vertical, 'Вертикальный', Icons.swap_vert),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
body: widget.mindMap.nodes.isEmpty
|
|
? _buildEmptyState()
|
|
: rm.MindMapWidget(
|
|
data: _buildMindMapData(),
|
|
style: rm.MindMapStyle(
|
|
layout: _currentLayout,
|
|
nodeShape: rm.NodeShape.roundedRectangle,
|
|
// Используем стандартные узлы (без кастомизации)
|
|
connectionColor: Colors.blue.shade100,
|
|
connectionWidth: 2.0,
|
|
),
|
|
cameraFocus: rm.CameraFocus.fitAll,
|
|
onNodeTap: (node) {
|
|
// TODO: Редактирование узла
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
rm.MindMapData _buildMindMapData() {
|
|
// Находим корневой узел (level 0)
|
|
final rootNodes =
|
|
widget.mindMap.nodes.where((n) => n['level'] == 0).toList();
|
|
|
|
if (rootNodes.isEmpty) {
|
|
return rm.MindMapData(
|
|
id: 'empty',
|
|
title: widget.mindMap.title,
|
|
children: [],
|
|
);
|
|
}
|
|
|
|
final root = rootNodes.first;
|
|
return rm.MindMapData(
|
|
id: root['id'] as String,
|
|
title: root['title'] as String,
|
|
color: _hexToColor(root['color'] as String? ?? '#2196F3'),
|
|
children: _buildChildren(root['id'] as String),
|
|
);
|
|
}
|
|
|
|
List<rm.MindMapData> _buildChildren(String parentId) {
|
|
final children =
|
|
widget.mindMap.nodes.where((n) => n['parentId'] == parentId).toList();
|
|
|
|
return children.map((node) {
|
|
return rm.MindMapData(
|
|
id: node['id'] as String,
|
|
title: node['title'] as String,
|
|
color: _hexToColor(node['color'] as String? ?? '#4CAF50'),
|
|
children: _buildChildren(node['id'] as String),
|
|
);
|
|
}).toList();
|
|
}
|
|
|
|
Widget _buildCustomNode(
|
|
rm.MindMapData node,
|
|
VoidCallback onTap,
|
|
VoidCallback onLongPress,
|
|
bool isSelected,
|
|
) {
|
|
return GestureDetector(
|
|
onTap: onTap,
|
|
onLongPress: onLongPress,
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 200),
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
|
decoration: BoxDecoration(
|
|
color: node.color ?? const Color(0xFF2196F3),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: isSelected ? Border.all(color: Colors.white, width: 3) : null,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.15),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(Icons.circle, color: Colors.white70, size: 12),
|
|
const SizedBox(width: 8),
|
|
ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 200),
|
|
child: Text(
|
|
node.title,
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 14,
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
maxLines: 2,
|
|
),
|
|
),
|
|
if (node.children.isNotEmpty) ...[
|
|
const SizedBox(width: 8),
|
|
Icon(Icons.keyboard_arrow_down, color: Colors.white70, size: 16),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildEmptyState() {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.account_tree, size: 100, color: Colors.grey[400]),
|
|
const SizedBox(height: 24),
|
|
Text(
|
|
'Mind map пуста',
|
|
style: TextStyle(fontSize: 20, color: Colors.grey[600]),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Добавьте узлы для начала работы',
|
|
style: TextStyle(color: Colors.grey[500]),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
PopupMenuItem<rm.MindMapLayout> _popupMenuItem(
|
|
rm.MindMapLayout layout,
|
|
String title,
|
|
IconData icon,
|
|
) {
|
|
return PopupMenuItem(
|
|
value: layout,
|
|
child: Row(
|
|
children: [
|
|
Icon(icon, size: 20),
|
|
const SizedBox(width: 12),
|
|
Text(title),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Color _hexToColor(String hex) {
|
|
hex = hex.replaceAll('#', '');
|
|
if (hex.length == 6) {
|
|
return Color(int.parse('FF$hex', radix: 16));
|
|
}
|
|
return const Color(0xFF2196F3);
|
|
}
|
|
}
|