229 lines
6.8 KiB
Dart
229 lines
6.8 KiB
Dart
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<GiteaSyncService>((ref) {
|
|
return GiteaSyncService();
|
|
});
|
|
|
|
/// Provider для OfflineMindmapRepository
|
|
final offlineMindmapRepoProvider = Provider<OfflineMindmapRepository>((ref) {
|
|
return OfflineMindmapRepository();
|
|
});
|
|
|
|
/// State для mindmap синхронизации
|
|
class MindmapSyncState {
|
|
final List<GiteaFile> files;
|
|
final List<CachedMindmap> 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<GiteaFile>? files,
|
|
List<CachedMindmap>? 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<MindmapSyncState> {
|
|
final GiteaSyncService _gitea;
|
|
final OfflineMindmapRepository _repo;
|
|
|
|
MindmapSyncNotifier(this._gitea, this._repo) : super(MindmapSyncState());
|
|
|
|
/// Инициализация сервиса
|
|
Future<void> initialize() async {
|
|
await _repo.initialize();
|
|
await _gitea.initialize();
|
|
state = state.copyWith(isConfigured: _gitea.isConfigured);
|
|
|
|
if (state.isConfigured) {
|
|
await loadCachedMindmaps();
|
|
}
|
|
}
|
|
|
|
/// Настройка Gitea
|
|
Future<void> 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<void> loadCachedMindmaps() async {
|
|
final cached = _repo.getAllMindmaps();
|
|
state = state.copyWith(cachedMindmaps: cached);
|
|
}
|
|
|
|
/// Полная синхронизация с Gitea (download)
|
|
Future<void> 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<MindMapNode>? parseMindmap(String path) {
|
|
final cached = _repo.getMindmap(path);
|
|
if (cached == null) return null;
|
|
|
|
return MarkdownMindmapParser.parse(
|
|
cached.content,
|
|
path.replaceAll('/', '_'),
|
|
);
|
|
}
|
|
|
|
/// Сохранить изменения mindmap и запушить в Gitea
|
|
Future<void> saveAndPush({
|
|
required String path,
|
|
required List<MindMapNode> 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<void> 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<MindmapSyncNotifier, MindmapSyncState>((ref) {
|
|
final gitea = ref.watch(giteaSyncServiceProvider);
|
|
final repo = ref.watch(offlineMindmapRepoProvider);
|
|
return MindmapSyncNotifier(gitea, repo);
|
|
});
|