import 'dart:async'; import 'dart:convert'; import 'dart:html' as html; import 'dart:typed_data'; import '../layout/layout_controller.dart'; import '../session/log_buffer.dart'; import '../session/packet_buffer.dart'; import 'csv_exporter_base.dart'; export 'csv_exporter_base.dart'; /// Web CSV exporter: builds a Blob in memory and triggers a download. /// /// Yields between chunks so the main thread doesn't lock during large exports. class CsvExporterImpl implements CsvExporter { static const int _chunkRows = 1000; @override Future exportData({ required PacketBuffer buffer, required LayoutController layout, ProgressCallback? onProgress, }) async { final builder = CsvRowBuilder(layout); final chunks = []; chunks.add('${builder.dataHeader()}\n'); final total = buffer.length; final sb = StringBuffer(); var written = 0; for (var i = 0; i < total; i++) { sb.writeln(builder.dataRow(buffer[i])); written++; if (written % _chunkRows == 0) { chunks.add(sb.toString()); sb.clear(); onProgress?.call(written / total); await Future.delayed(Duration.zero); } } if (sb.isNotEmpty) chunks.add(sb.toString()); final ts = DateTime.now().toIso8601String().replaceAll(':', '-'); final filename = 'telemetry_data_$ts.csv'; final bytes = _toBytes(chunks); _triggerDownload(filename, bytes); onProgress?.call(1.0); return ExportResult(path: filename, bytesWritten: bytes.length); } @override Future exportLog({ required LogBuffer buffer, ProgressCallback? onProgress, }) async { final builder = CsvRowBuilder(_NoLayoutPlaceholder()); final chunks = []; chunks.add('${builder.logHeader()}\n'); final total = buffer.length; final sb = StringBuffer(); var written = 0; for (final entry in buffer.iterate()) { sb.writeln(builder.logRow(entry)); written++; if (written % _chunkRows == 0) { chunks.add(sb.toString()); sb.clear(); onProgress?.call(written / total); await Future.delayed(Duration.zero); } } if (sb.isNotEmpty) chunks.add(sb.toString()); final ts = DateTime.now().toIso8601String().replaceAll(':', '-'); final filename = 'telemetry_log_$ts.csv'; final bytes = _toBytes(chunks); _triggerDownload(filename, bytes); onProgress?.call(1.0); return ExportResult(path: filename, bytesWritten: bytes.length); } Uint8List _toBytes(List chunks) { final encoder = utf8.encoder; final encoded = chunks.map(encoder.convert).toList(); final total = encoded.fold(0, (s, c) => s + c.length); final out = Uint8List(total); var off = 0; for (final c in encoded) { out.setRange(off, off + c.length, c); off += c.length; } return out; } void _triggerDownload(String filename, Uint8List bytes) { final blob = html.Blob([bytes], 'text/csv'); final url = html.Url.createObjectUrlFromBlob(blob); final anchor = html.AnchorElement(href: url) ..download = filename ..style.display = 'none'; html.document.body!.append(anchor); anchor.click(); anchor.remove(); html.Url.revokeObjectUrl(url); } } class _NoLayoutPlaceholder implements LayoutController { @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); }