import 'dart:convert'; import 'package:hive/hive.dart'; /// Hive модель для кэшированного mindmap-файла class CachedMindmap { final String path; final String content; final String? sha; final DateTime lastSync; final DateTime lastModified; final String branch; CachedMindmap({ required this.path, required this.content, this.sha, required this.lastSync, required this.lastModified, this.branch = 'main', }); Map toJson() => { 'path': path, 'content': content, 'sha': sha, 'lastSync': lastSync.toIso8601String(), 'lastModified': lastModified.toIso8601String(), 'branch': branch, }; factory CachedMindmap.fromJson(Map json) { return CachedMindmap( path: json['path'] ?? '', content: json['content'] ?? '', sha: json['sha'], lastSync: DateTime.parse(json['lastSync'] ?? DateTime.now().toIso8601String()), lastModified: DateTime.parse( json['lastModified'] ?? DateTime.now().toIso8601String()), branch: json['branch'] ?? 'main', ); } } /// Оффлайн репозиторий для mindmaps /// Хранит кэш файлов из Gitea и очередь изменений для синхронизации class OfflineMindmapRepository { static const String _boxName = 'mindmaps_cache'; static const String _queueBoxName = 'mindmaps_queue'; static const String _metaKey = '_meta_sync_state'; Box? _box; Box? _queueBox; Future initialize() async { _box = await Hive.openBox(_boxName); _queueBox = await Hive.openBox(_queueBoxName); } bool get isInitialized => _box != null && _box!.isOpen; // ==================== CACHE ==================== /// Сохранить файл в кэш Future cacheMindmap(CachedMindmap mindmap) async { if (!isInitialized) throw Exception('Repository not initialized'); await _box!.put(mindmap.path, jsonEncode(mindmap.toJson())); } /// Получить файл из кэша CachedMindmap? getMindmap(String path) { if (!isInitialized) return null; final raw = _box!.get(path); if (raw == null) return null; try { return CachedMindmap.fromJson(jsonDecode(raw)); } catch (_) { return null; } } /// Получить все кэшированные mindmaps List getAllMindmaps() { if (!isInitialized) return []; return _box!.values .map((raw) { try { return CachedMindmap.fromJson(jsonDecode(raw)); } catch (_) { return null; } }) .whereType() .toList(); } /// Удалить файл из кэша Future deleteMindmap(String path) async { if (!isInitialized) return; await _box!.delete(path); } /// Очистить весь кэш Future clearCache() async { if (!isInitialized) return; await _box!.clear(); } // ==================== SYNC QUEUE ==================== /// Добавить изменение в очередь (для offline → online sync) Future queueChange({ required String path, required String content, required String operation, // 'create' | 'update' | 'delete' String? commitMessage, }) async { if (!isInitialized) throw Exception('Repository not initialized'); final entry = { 'path': path, 'content': content, 'operation': operation, 'commitMessage': commitMessage ?? 'Mobile sync: $path', 'timestamp': DateTime.now().toIso8601String(), }; await _queueBox!.put( '${DateTime.now().millisecondsSinceEpoch}_$path', jsonEncode(entry)); } /// Получить все изменения из очереди List> getPendingChanges() { if (!isInitialized) return []; return _queueBox!.values .map((raw) { try { return jsonDecode(raw) as Map; } catch (_) { return null; } }) .whereType>() .toList() ..sort((a, b) => (a['timestamp'] ?? '').compareTo(b['timestamp'] ?? '')); } /// Удалить выполненное изменение из очереди Future removeQueuedChange(String key) async { if (!isInitialized) return; await _queueBox!.delete(key); } /// Очистить очередь Future clearQueue() async { if (!isInitialized) return; await _queueBox!.clear(); } bool get hasPendingChanges => (_queueBox?.length ?? 0) > 0; // ==================== SYNC STATE ==================== /// Сохранить время последней успешной синхронизации Future setLastSync(DateTime time) async { if (!isInitialized) return; await _box!.put(_metaKey, jsonEncode({'lastSync': time.toIso8601String()})); } /// Получить время последней синхронизации DateTime? getLastSync() { if (!isInitialized) return null; final raw = _box!.get(_metaKey); if (raw == null) return null; try { final data = jsonDecode(raw) as Map; return DateTime.tryParse(data['lastSync'] ?? ''); } catch (_) { return null; } } }