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,178 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import '../models/mindmap_model.dart';
|
||||
import '../core/admin_access_service.dart';
|
||||
import '../core/deepseek_service.dart';
|
||||
|
||||
// Provider для получения текущего email пользователя
|
||||
final currentUserEmailProvider = Provider<String?>((ref) {
|
||||
return FirebaseAuth.instance.currentUser?.email;
|
||||
});
|
||||
|
||||
// Provider проверки является ли пользователь админом
|
||||
final isAdminProvider = Provider<bool>((ref) {
|
||||
final email = ref.watch(currentUserEmailProvider);
|
||||
if (email == null) return false;
|
||||
return AdminAccessService.adminEmails.contains(email.toLowerCase());
|
||||
});
|
||||
|
||||
// Provider списка mind maps
|
||||
final mindMapsProvider = StreamProvider<List<MindMapModel>>((ref) {
|
||||
return FirebaseFirestore.instance
|
||||
.collection('mind_maps')
|
||||
.orderBy('createdAt', descending: true)
|
||||
.snapshots()
|
||||
.map((snapshot) {
|
||||
return snapshot.docs.map((doc) {
|
||||
return MindMapModel.fromFirestore(doc);
|
||||
}).toList();
|
||||
});
|
||||
});
|
||||
|
||||
// Notifier для управления mind maps
|
||||
class MindMapsNotifier extends StateNotifier<AsyncValue<List<MindMapModel>>> {
|
||||
final Ref ref;
|
||||
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
||||
final FirebaseAuth _auth = FirebaseAuth.instance;
|
||||
|
||||
MindMapsNotifier(this.ref) : super(const AsyncValue.loading()) {
|
||||
_listenToMindMaps();
|
||||
}
|
||||
|
||||
void _listenToMindMaps() {
|
||||
_firestore
|
||||
.collection('mind_maps')
|
||||
.orderBy('createdAt', descending: true)
|
||||
.snapshots()
|
||||
.listen(
|
||||
(snapshot) {
|
||||
final mindMaps = snapshot.docs.map((doc) {
|
||||
return MindMapModel.fromFirestore(doc);
|
||||
}).toList();
|
||||
state = AsyncValue.data(mindMaps);
|
||||
},
|
||||
onError: (error, st) {
|
||||
state = AsyncValue.error(error, st);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Создание новой mind map
|
||||
Future<String> createMindMap({
|
||||
required String title,
|
||||
String description = '',
|
||||
required List<Map<String, dynamic>> nodes,
|
||||
bool isMain = false,
|
||||
}) async {
|
||||
try {
|
||||
final userEmail = _auth.currentUser?.email ?? 'unknown';
|
||||
final now = DateTime.now();
|
||||
final id = const Uuid().v4();
|
||||
|
||||
final mindMap = MindMapModel(
|
||||
id: id,
|
||||
title: title,
|
||||
description: description,
|
||||
ownerEmail: userEmail,
|
||||
isMain: isMain,
|
||||
nodes: nodes,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
);
|
||||
|
||||
await _firestore.collection('mind_maps').doc(id).set(mindMap.toFirestore());
|
||||
|
||||
return id;
|
||||
} catch (e, st) {
|
||||
state = AsyncValue.error(e, st);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// Удаление mind map (админы могут удалять любые, обычные - только свои)
|
||||
Future<void> deleteMindMap(String id, String ownerEmail) async {
|
||||
try {
|
||||
final isAdmin = ref.read(isAdminProvider);
|
||||
final currentUserEmail = _auth.currentUser?.email ?? '';
|
||||
|
||||
// Проверка прав
|
||||
if (!isAdmin && ownerEmail != currentUserEmail) {
|
||||
throw Exception('Нет прав для удаления этой mind map');
|
||||
}
|
||||
|
||||
await _firestore.collection('mind_maps').doc(id).delete();
|
||||
} catch (e, st) {
|
||||
state = AsyncValue.error(e, st);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// Обновление mind map
|
||||
Future<void> updateMindMap(String id, MindMapModel mindMap) async {
|
||||
try {
|
||||
final isAdmin = ref.read(isAdminProvider);
|
||||
final currentUserEmail = _auth.currentUser?.email ?? '';
|
||||
|
||||
// Проверка прав
|
||||
if (!isAdmin && mindMap.ownerEmail != currentUserEmail) {
|
||||
throw Exception('Нет прав для редактирования этой mind map');
|
||||
}
|
||||
|
||||
await _firestore
|
||||
.collection('mind_maps')
|
||||
.doc(id)
|
||||
.update(mindMap.copyWith(updatedAt: DateTime.now()).toFirestore());
|
||||
} catch (e, st) {
|
||||
state = AsyncValue.error(e, st);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// AI генерация mind map
|
||||
Future<String> generateWithAI({
|
||||
required String title,
|
||||
required String description,
|
||||
}) async {
|
||||
try {
|
||||
final nodesData = await deepSeekService.generateMindMap(
|
||||
projectName: title,
|
||||
description: description,
|
||||
);
|
||||
|
||||
final nodes = <Map<String, dynamic>>[];
|
||||
for (var i = 0; i < nodesData.length; i++) {
|
||||
final nodeData = nodesData[i];
|
||||
nodes.add({
|
||||
'id': 'node_$i',
|
||||
'level': nodeData['level'] ?? 0,
|
||||
'parentId': nodeData['parentId'],
|
||||
'title': nodeData['title'] ?? '',
|
||||
'color': _getColorForLevel(nodeData['level'] ?? 0),
|
||||
});
|
||||
}
|
||||
|
||||
return await createMindMap(
|
||||
title: title,
|
||||
description: description,
|
||||
nodes: nodes,
|
||||
);
|
||||
} catch (e, st) {
|
||||
state = AsyncValue.error(e, st);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
String _getColorForLevel(int level) {
|
||||
final colors = [
|
||||
'#2196F3', '#4CAF50', '#FF9800', '#E91E63', '#9C27B0', '#00BCD4'
|
||||
];
|
||||
return colors[level % colors.length];
|
||||
}
|
||||
}
|
||||
|
||||
final mindMapsNotifierProvider =
|
||||
StateNotifierProvider<MindMapsNotifier, AsyncValue<List<MindMapModel>>>(
|
||||
(ref) => MindMapsNotifier(ref),
|
||||
);
|
||||
@@ -1,13 +1,15 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:reactive_mind_map/reactive_mind_map.dart' as rm;
|
||||
import '../models/mind_map_model.dart';
|
||||
import '../models/task_model.dart';
|
||||
import '../core/firestore_service.dart';
|
||||
import '../core/local_storage_service.dart';
|
||||
import '../core/deepseek_service.dart';
|
||||
import '../core/mind_map_adapter.dart';
|
||||
|
||||
// Provider для mind-карты проекта
|
||||
final mindMapProvider = StateNotifierProvider.family<MindMapNotifier, AsyncValue<MindMapData>, String>((ref, projectId) {
|
||||
final mindMapProvider = StateNotifierProvider.family<MindMapNotifier,
|
||||
AsyncValue<MindMapData>, String>((ref, projectId) {
|
||||
return MindMapNotifier(projectId);
|
||||
});
|
||||
|
||||
@@ -31,9 +33,16 @@ class MindMapData {
|
||||
|
||||
// Получить родительский узел
|
||||
MindMapNode? getParent(String nodeId) {
|
||||
final node = nodes.firstWhere((n) => n.id == nodeId, orElse: () => throw Exception('Node not found'));
|
||||
final node = nodes.firstWhere((n) => n.id == nodeId,
|
||||
orElse: () => throw Exception('Node not found'));
|
||||
if (node.parentId == null) return null;
|
||||
return nodes.firstWhere((n) => n.id == node.parentId, orElse: () => throw Exception('Parent not found'));
|
||||
return nodes.firstWhere((n) => n.id == node.parentId,
|
||||
orElse: () => throw Exception('Parent not found'));
|
||||
}
|
||||
|
||||
// Конвертировать в reactive_mind_map формат
|
||||
rm.MindMapData toReactiveMindMapData() {
|
||||
return MindMapAdapter.nodesToMindMapData(nodes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,8 +58,9 @@ class MindMapNotifier extends StateNotifier<AsyncValue<MindMapData>> {
|
||||
try {
|
||||
// Загружаем из локального хранилища
|
||||
final localNodes = localStorageService.getMindMapNodes();
|
||||
final projectNodes = localNodes.where((n) => n.projectId == projectId).toList();
|
||||
|
||||
final projectNodes =
|
||||
localNodes.where((n) => n.projectId == projectId).toList();
|
||||
|
||||
state = AsyncValue.data(_createMindMapData(projectNodes));
|
||||
|
||||
// TODO: Загрузить из Firestore и синхронизировать
|
||||
@@ -90,7 +100,7 @@ class MindMapNotifier extends StateNotifier<AsyncValue<MindMapData>> {
|
||||
String icon = 'folder',
|
||||
}) async {
|
||||
try {
|
||||
final parentNode = parentId != null
|
||||
final parentNode = parentId != null
|
||||
? state.value?.nodes.firstWhere((n) => n.id == parentId)
|
||||
: null;
|
||||
|
||||
@@ -123,11 +133,11 @@ class MindMapNotifier extends StateNotifier<AsyncValue<MindMapData>> {
|
||||
final currentData = state.value;
|
||||
if (currentData != null) {
|
||||
final updatedNodes = [...currentData.nodes, node];
|
||||
|
||||
|
||||
if (parentId != null) {
|
||||
currentData.childrenMap.putIfAbsent(parentId, () => []).add(node.id);
|
||||
}
|
||||
|
||||
|
||||
state = AsyncValue.data(MindMapData(
|
||||
nodes: updatedNodes,
|
||||
childrenMap: currentData.childrenMap,
|
||||
@@ -143,7 +153,8 @@ class MindMapNotifier extends StateNotifier<AsyncValue<MindMapData>> {
|
||||
}
|
||||
|
||||
// Обновление узла
|
||||
Future<void> updateNode(String nodeId, {
|
||||
Future<void> updateNode(
|
||||
String nodeId, {
|
||||
String? title,
|
||||
String? color,
|
||||
String? icon,
|
||||
@@ -203,7 +214,9 @@ class MindMapNotifier extends StateNotifier<AsyncValue<MindMapData>> {
|
||||
_collectChildNodes(nodeId, currentData, nodesToDelete);
|
||||
|
||||
// Сохраняем для удаления
|
||||
final nodes = currentData.nodes.where((n) => !nodesToDelete.contains(n.id)).toList();
|
||||
final nodes = currentData.nodes
|
||||
.where((n) => !nodesToDelete.contains(n.id))
|
||||
.toList();
|
||||
|
||||
// Сохраняем локально (удаляем)
|
||||
for (final id in nodesToDelete) {
|
||||
@@ -234,7 +247,8 @@ class MindMapNotifier extends StateNotifier<AsyncValue<MindMapData>> {
|
||||
}
|
||||
|
||||
// Рекурсивный сбор дочерних узлов
|
||||
void _collectChildNodes(String parentId, MindMapData data, Set<String> result) {
|
||||
void _collectChildNodes(
|
||||
String parentId, MindMapData data, Set<String> result) {
|
||||
final childIds = data.childrenMap[parentId] ?? [];
|
||||
for (final childId in childIds) {
|
||||
result.add(childId);
|
||||
@@ -260,7 +274,7 @@ class MindMapNotifier extends StateNotifier<AsyncValue<MindMapData>> {
|
||||
final nodeData = nodesData[i];
|
||||
final level = nodeData['level'] as int;
|
||||
final parentId = nodeData['parentId'] as int?;
|
||||
|
||||
|
||||
String? actualParentId;
|
||||
if (parentId != null && parentId < nodes.length) {
|
||||
actualParentId = nodes[parentId].id;
|
||||
@@ -272,16 +286,16 @@ class MindMapNotifier extends StateNotifier<AsyncValue<MindMapData>> {
|
||||
x: (level * 200).toDouble(), // Автоматическое позиционирование
|
||||
y: (i * 50).toDouble(),
|
||||
);
|
||||
|
||||
|
||||
nodes.add(node);
|
||||
|
||||
|
||||
if (level == 0) {
|
||||
rootNode = node;
|
||||
}
|
||||
}
|
||||
|
||||
return nodes;
|
||||
} catch (e, st) {
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@@ -300,16 +314,18 @@ class MindMapNotifier extends StateNotifier<AsyncValue<MindMapData>> {
|
||||
return {
|
||||
'projectId': projectId,
|
||||
'rootNodeId': data.rootNodeId,
|
||||
'nodes': data.nodes.map((n) => {
|
||||
'id': n.id,
|
||||
'parentId': n.parentId,
|
||||
'level': n.level,
|
||||
'title': n.title,
|
||||
'x': n.x,
|
||||
'y': n.y,
|
||||
'color': n.color,
|
||||
'icon': n.icon,
|
||||
}).toList(),
|
||||
'nodes': data.nodes
|
||||
.map((n) => {
|
||||
'id': n.id,
|
||||
'parentId': n.parentId,
|
||||
'level': n.level,
|
||||
'title': n.title,
|
||||
'x': n.x,
|
||||
'y': n.y,
|
||||
'color': n.color,
|
||||
'icon': n.icon,
|
||||
})
|
||||
.toList(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'dart:async';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import '../models/task_model.dart';
|
||||
import '../models/project_model.dart';
|
||||
import '../core/firestore_service.dart';
|
||||
import '../core/local_storage_service.dart';
|
||||
import '../core/deepseek_service.dart';
|
||||
@@ -253,7 +252,7 @@ class TasksNotifier extends StateNotifier<AsyncValue<List<TaskModel>>> {
|
||||
}
|
||||
|
||||
return subtasks;
|
||||
} catch (e, st) {
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user