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
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user