import 'dart:convert'; import 'dart:io'; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; /// Сервис для синхронизации с Nanaxe Server /// Поддерживает работу с mindmaps через REST API class NanaxeServerService { static final NanaxeServerService _instance = NanaxeServerService._internal(); factory NanaxeServerService() => _instance; NanaxeServerService._internal(); String? _baseUrl; String? _authToken; static const String _prefsUrlKey = 'nanaxe_server_url'; static const String _prefsTokenKey = 'nanaxe_server_token'; /// Инициализация сервиса Future initialize() async { final prefs = await SharedPreferences.getInstance(); _baseUrl = prefs.getString(_prefsUrlKey); _authToken = prefs.getString(_prefsTokenKey); } /// Настройка сервера Future configure(String baseUrl, {String? token}) async { _baseUrl = baseUrl.replaceAll(RegExp(r'/+$'), ''); // Remove trailing slashes _authToken = token; final prefs = await SharedPreferences.getInstance(); await prefs.setString(_prefsUrlKey, _baseUrl!); if (token != null) { await prefs.setString(_prefsTokenKey, token); } } /// Очистка конфигурации Future clearConfiguration() async { _baseUrl = null; _authToken = null; final prefs = await SharedPreferences.getInstance(); await prefs.remove(_prefsUrlKey); await prefs.remove(_prefsTokenKey); } /// Проверка, настроен ли сервер bool get isConfigured => _baseUrl != null; /// Получение базового URL String? get baseUrl => _baseUrl; // ==================== AUTH ==================== /// Регистрация нового пользователя Future> register(String email, String password, String username) async { if (_baseUrl == null) throw Exception('Server not configured'); final response = await http.post( Uri.parse('$_baseUrl/api/auth/register'), headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'email': email, 'password': password, 'username': username, }), ); final data = jsonDecode(response.body); if (response.statusCode == 201) { _authToken = data['token']; final prefs = await SharedPreferences.getInstance(); await prefs.setString(_prefsTokenKey, _authToken!); return data; } else { throw Exception(data['error'] ?? 'Registration failed'); } } /// Вход в систему Future> login(String email, String password) async { if (_baseUrl == null) throw Exception('Server not configured'); final response = await http.post( Uri.parse('$_baseUrl/api/auth/login'), headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'email': email, 'password': password, }), ); final data = jsonDecode(response.body); if (response.statusCode == 200) { _authToken = data['token']; final prefs = await SharedPreferences.getInstance(); await prefs.setString(_prefsTokenKey, _authToken!); return data; } else { throw Exception(data['error'] ?? 'Login failed'); } } /// Проверка валидности токена Future validateToken() async { if (_baseUrl == null || _authToken == null) return false; try { final response = await http.get( Uri.parse('$_baseUrl/api/auth/validate'), headers: _getHeaders(), ); final data = jsonDecode(response.body); return data['valid'] == true; } catch (e) { return false; } } // ==================== MINDMAPS ==================== /// Получение списка всех mindmaps Future>> getMindMaps() async { if (_baseUrl == null) throw Exception('Server not configured'); final response = await http.get( Uri.parse('$_baseUrl/api/mindmaps'), headers: _getHeaders(), ); if (response.statusCode == 200) { final data = jsonDecode(response.body); return List>.from(data['mindmaps'] ?? []); } else { throw Exception('Failed to load mindmaps'); } } /// Получение одной mindmap Future> getMindMap(String id) async { if (_baseUrl == null) throw Exception('Server not configured'); final response = await http.get( Uri.parse('$_baseUrl/api/mindmaps/$id'), headers: _getHeaders(), ); if (response.statusCode == 200) { return jsonDecode(response.body); } else { throw Exception('Failed to load mindmap'); } } /// Создание новой mindmap Future> createMindMap({ required String title, String? description, List>? nodes, Map? settings, }) async { if (_baseUrl == null) throw Exception('Server not configured'); final response = await http.post( Uri.parse('$_baseUrl/api/mindmaps'), headers: _getHeaders(), body: jsonEncode({ 'title': title, 'description': description ?? '', 'nodes': nodes ?? [], 'settings': settings ?? {}, }), ); if (response.statusCode == 201) { return jsonDecode(response.body); } else { final data = jsonDecode(response.body); throw Exception(data['error'] ?? 'Failed to create mindmap'); } } /// Обновление mindmap Future> updateMindMap( String id, { String? title, String? description, List>? nodes, Map? settings, }) async { if (_baseUrl == null) throw Exception('Server not configured'); final body = {}; if (title != null) body['title'] = title; if (description != null) body['description'] = description; if (nodes != null) body['nodes'] = nodes; if (settings != null) body['settings'] = settings; final response = await http.put( Uri.parse('$_baseUrl/api/mindmaps/$id'), headers: _getHeaders(), body: jsonEncode(body), ); if (response.statusCode == 200) { return jsonDecode(response.body); } else { throw Exception('Failed to update mindmap'); } } /// Удаление mindmap Future deleteMindMap(String id) async { if (_baseUrl == null) throw Exception('Server not configured'); final response = await http.delete( Uri.parse('$_baseUrl/api/mindmaps/$id'), headers: _getHeaders(), ); if (response.statusCode != 200) { throw Exception('Failed to delete mindmap'); } } /// Получение mindmap как Markdown Future getMindMapAsMarkdown(String id) async { if (_baseUrl == null) throw Exception('Server not configured'); final response = await http.get( Uri.parse('$_baseUrl/api/mindmaps/$id/markdown'), headers: _getHeaders(), ); if (response.statusCode == 200) { return response.body; } else { throw Exception('Failed to load markdown'); } } /// Сохранение mindmap из Markdown Future> saveMindMapFromMarkdown( String id, String markdown, ) async { if (_baseUrl == null) throw Exception('Server not configured'); final response = await http.post( Uri.parse('$_baseUrl/api/mindmaps/$id/markdown'), headers: _getHeaders(), body: jsonEncode({'markdown': markdown}), ); if (response.statusCode == 200) { return jsonDecode(response.body); } else { throw Exception('Failed to save markdown'); } } // ==================== SYNC ==================== /// Получение статуса синхронизации Future> getSyncStatus() async { if (_baseUrl == null) throw Exception('Server not configured'); final response = await http.get( Uri.parse('$_baseUrl/api/sync/status'), headers: _getHeaders(), ); if (response.statusCode == 200) { return jsonDecode(response.body); } else { throw Exception('Failed to get sync status'); } } /// Push изменений в Gitea Future> pushToGitea({String? commitMessage}) async { if (_baseUrl == null) throw Exception('Server not configured'); final response = await http.post( Uri.parse('$_baseUrl/api/sync/gitea/push'), headers: _getHeaders(), body: jsonEncode({ 'commitMessage': commitMessage ?? 'Sync from Nanaxe CRM', }), ); if (response.statusCode == 200) { return jsonDecode(response.body); } else { throw Exception('Failed to push to Gitea'); } } /// Pull изменений из Gitea Future> pullFromGitea() async { if (_baseUrl == null) throw Exception('Server not configured'); final response = await http.post( Uri.parse('$_baseUrl/api/sync/gitea/pull'), headers: _getHeaders(), body: jsonEncode({}), ); if (response.statusCode == 200) { return jsonDecode(response.body); } else { throw Exception('Failed to pull from Gitea'); } } // ==================== HELPERS ==================== Map _getHeaders() { final headers = { 'Content-Type': 'application/json', }; if (_authToken != null) { headers['Authorization'] = 'Bearer $_authToken'; } return headers; } /// Проверка соединения с сервером Future checkConnection() async { if (_baseUrl == null) return false; try { final response = await http .get(Uri.parse('$_baseUrl/health')) .timeout(const Duration(seconds: 5)); return response.statusCode == 200; } catch (e) { return false; } } } /// Провайдер для работы с сервером class ServerConfig { final String baseUrl; final String? token; final bool isConnected; ServerConfig({ required this.baseUrl, this.token, this.isConnected = false, }); }