import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../core/gitea_sync_service.dart'; import '../core/offline_mindmap_repository.dart'; import '../core/markdown_mindmap_parser.dart'; import '../models/mind_map_model.dart'; /// Provider для GiteaSyncService final giteaSyncServiceProvider = Provider((ref) { return GiteaSyncService(); }); /// Provider для OfflineMindmapRepository final offlineMindmapRepoProvider = Provider((ref) { return OfflineMindmapRepository(); }); /// State для mindmap синхронизации class MindmapSyncState { final List files; final List cachedMindmaps; final bool isLoading; final bool isSyncing; final String? error; final bool isConfigured; MindmapSyncState({ this.files = const [], this.cachedMindmaps = const [], this.isLoading = false, this.isSyncing = false, this.error, this.isConfigured = false, }); MindmapSyncState copyWith({ List? files, List? cachedMindmaps, bool? isLoading, bool? isSyncing, String? error, bool? isConfigured, }) { return MindmapSyncState( files: files ?? this.files, cachedMindmaps: cachedMindmaps ?? this.cachedMindmaps, isLoading: isLoading ?? this.isLoading, isSyncing: isSyncing ?? this.isSyncing, error: error ?? this.error, isConfigured: isConfigured ?? this.isConfigured, ); } } /// Notifier для управления синхронизацией mindmaps class MindmapSyncNotifier extends StateNotifier { final GiteaSyncService _gitea; final OfflineMindmapRepository _repo; MindmapSyncNotifier(this._gitea, this._repo) : super(MindmapSyncState()); /// Инициализация сервиса Future initialize() async { await _repo.initialize(); await _gitea.initialize(); state = state.copyWith(isConfigured: _gitea.isConfigured); if (state.isConfigured) { await loadCachedMindmaps(); } } /// Настройка Gitea Future configure({ required String baseUrl, required String token, required String owner, required String repo, }) async { state = state.copyWith(isLoading: true, error: null); try { await _gitea.configure( baseUrl: baseUrl, token: token, owner: owner, repo: repo, ); final connected = await _gitea.testConnection(); if (!connected) { throw Exception('Failed to connect to Gitea. Check token and URL.'); } state = state.copyWith(isConfigured: true, isLoading: false); await syncFromGitea(); } catch (e) { state = state.copyWith(isLoading: false, error: e.toString()); } } /// Загрузить mindmaps из кэша Future loadCachedMindmaps() async { final cached = _repo.getAllMindmaps(); state = state.copyWith(cachedMindmaps: cached); } /// Полная синхронизация с Gitea (download) Future syncFromGitea({String branch = 'main'}) async { if (!state.isConfigured) return; state = state.copyWith(isSyncing: true, error: null); try { // Получаем дерево файлов final files = await _gitea.getTree(branch: branch); final mindmapFiles = files.where((f) => f.path.endsWith('.md') || f.path.endsWith('.canvas')).toList(); // Загружаем каждый файл for (final file in mindmapFiles) { try { final giteaFile = await _gitea.getFile(path: file.path, branch: branch); final content = giteaFile.decodedContent; if (content != null) { await _repo.cacheMindmap(CachedMindmap( path: file.path, content: content, sha: giteaFile.sha, lastSync: DateTime.now(), lastModified: DateTime.now(), branch: branch, )); } } catch (e) { // Логируем ошибку но продолжаем print('Failed to sync ${file.path}: $e'); } } await loadCachedMindmaps(); state = state.copyWith(isSyncing: false); } catch (e) { state = state.copyWith(isSyncing: false, error: e.toString()); } } /// Парсить markdown-кэш в MindMapNode List? parseMindmap(String path) { final cached = _repo.getMindmap(path); if (cached == null) return null; return MarkdownMindmapParser.parse( cached.content, path.replaceAll('/', '_'), ); } /// Сохранить изменения mindmap и запушить в Gitea Future saveAndPush({ required String path, required List nodes, String? title, String branch = 'main', }) async { if (!state.isConfigured) { throw Exception('Gitea not configured'); } final markdown = MarkdownMindmapParser.toMarkdown(nodes, title: title); // Сохраняем локально await _repo.cacheMindmap(CachedMindmap( path: path, content: markdown, lastSync: DateTime.now(), lastModified: DateTime.now(), branch: branch, )); // Пушим в Gitea try { await _gitea.updateFile( path: path, content: markdown, commitMessage: 'mobile: update $path', branch: branch, authorName: 'NANAXE Mobile', authorEmail: 'mobile@nanaxe.local', ); } catch (e) { // Если нет сети — кладём в очередь await _repo.queueChange( path: path, content: markdown, operation: 'update', commitMessage: 'mobile: update $path', ); } await loadCachedMindmaps(); } /// Отправить накопленные offline-изменения Future pushPendingChanges({String branch = 'main'}) async { if (!state.isConfigured) return; final pending = _repo.getPendingChanges(); for (final change in pending) { try { await _gitea.updateFile( path: change['path'], content: change['content'], commitMessage: change['commitMessage'] ?? 'Mobile sync', branch: branch, ); // Удаляем из очереди по timestamp_path final key = '${change['timestamp']}_${change['path']}'; await _repo.removeQueuedChange(key); } catch (e) { print('Failed to push queued change: $e'); } } } } /// StateNotifierProvider для mindmap синхронизации final mindmapSyncProvider = StateNotifierProvider((ref) { final gitea = ref.watch(giteaSyncServiceProvider); final repo = ref.watch(offlineMindmapRepoProvider); return MindmapSyncNotifier(gitea, repo); });