import 'dart:convert'; import '../../contracts/contracts.dart'; import '../errors.dart'; import '../ports/danmaku_sources_store.dart'; import '../ports/script_widget_repository.dart'; import '../ports/server_repository.dart'; import '../ports/session_repository.dart'; const int kBackupSchemaVersion = 1; class BackupExportResult { final String json; final int serverCount; final int sessionCount; final int danmakuSourceCount; final int scriptWidgetCount; final int scriptWidgetSubscriptionCount; const BackupExportResult({ required this.json, required this.serverCount, required this.sessionCount, required this.danmakuSourceCount, required this.scriptWidgetCount, required this.scriptWidgetSubscriptionCount, }); } class BackupImportResult { final int serverCount; final int sessionCount; final int danmakuSourceCount; final int scriptWidgetCount; final int scriptWidgetSubscriptionCount; const BackupImportResult({ required this.serverCount, required this.sessionCount, required this.danmakuSourceCount, required this.scriptWidgetCount, required this.scriptWidgetSubscriptionCount, }); } class ExportBackupUseCase { final ServerRepository serverRepository; final SessionRepository sessionRepository; final DanmakuSourcesStore danmakuSourcesStore; final ScriptWidgetRepository scriptWidgetRepository; final DateTime Function() now; ExportBackupUseCase({ required this.serverRepository, required this.sessionRepository, required this.danmakuSourcesStore, required this.scriptWidgetRepository, DateTime Function()? now, }) : now = (now ?? DateTime.now); Future execute() async { final servers = await serverRepository.list(); final sessions = await sessionRepository.listAll(); final danmakuSources = await danmakuSourcesStore.list(); final scriptWidgets = await scriptWidgetRepository.listWidgets(); final scriptWidgetSubscriptions = await scriptWidgetRepository.listSubscriptions(); final bundle = BackupBundle( version: kBackupSchemaVersion, exportedAt: now().toUtc().toIso8601String(), servers: servers, sessions: sessions, danmakuSources: danmakuSources, scriptWidgets: scriptWidgets, scriptWidgetSubscriptions: scriptWidgetSubscriptions, ); final json = const JsonEncoder.withIndent(' ').convert(bundle.toJson()); return BackupExportResult( json: json, serverCount: servers.length, sessionCount: sessions.length, danmakuSourceCount: danmakuSources.length, scriptWidgetCount: scriptWidgets.length, scriptWidgetSubscriptionCount: scriptWidgetSubscriptions.length, ); } } class ImportBackupUseCase { final ServerRepository serverRepository; final SessionRepository sessionRepository; final DanmakuSourcesStore danmakuSourcesStore; final ScriptWidgetRepository scriptWidgetRepository; ImportBackupUseCase({ required this.serverRepository, required this.sessionRepository, required this.danmakuSourcesStore, required this.scriptWidgetRepository, }); Future execute(String rawJson) async { final BackupBundle bundle; try { final decoded = jsonDecode(rawJson); if (decoded is! Map) { throw DomainError(ErrorCode.invalidParams, '备份文件格式不正确'); } bundle = BackupBundle.fromJson(decoded); } on DomainError { rethrow; } catch (e) { throw DomainError( ErrorCode.invalidParams, '备份文件解析失败', details: {'reason': e.toString()}, ); } if (bundle.version != kBackupSchemaVersion) { throw DomainError( ErrorCode.invalidParams, '不支持的备份文件版本:${bundle.version}', details: {'expected': kBackupSchemaVersion, 'got': bundle.version}, ); } for (final server in bundle.servers) { await serverRepository.update(server); } await sessionRepository.setMany(bundle.sessions); final mergedSources = mergeDanmakuSourcesByUrl( local: await danmakuSourcesStore.list(), remote: bundle.danmakuSources, ); await danmakuSourcesStore.replaceAll(mergedSources); for (final scriptWidget in bundle.scriptWidgets) { await scriptWidgetRepository.upsertWidget(scriptWidget); } for (final subscription in bundle.scriptWidgetSubscriptions) { await scriptWidgetRepository.upsertSubscription(subscription); } return BackupImportResult( serverCount: bundle.servers.length, sessionCount: bundle.sessions.length, danmakuSourceCount: mergedSources.length, scriptWidgetCount: bundle.scriptWidgets.length, scriptWidgetSubscriptionCount: bundle.scriptWidgetSubscriptions.length, ); } } List mergeDanmakuSourcesByUrl({ required List local, required List remote, }) { final localByUrl = { for (final s in local) s.url.trim(): s, }; final out = []; final seen = {}; for (final r in remote) { final url = r.url.trim(); if (url.isEmpty || seen.contains(url)) continue; seen.add(url); final hit = localByUrl[url]; if (hit != null) { out.add(hit.copyWith(name: hit.name.trim().isEmpty ? r.name : hit.name)); } else { out.add( DanmakuSource( id: r.id.trim().isEmpty ? 'src_${url.hashCode.toUnsigned(32).toRadixString(16)}' : r.id, name: r.name, url: url, ), ); } } return out; }