Files
TelemetryMonitor/lib/export/csv_exporter_web.dart
2026-04-21 14:40:09 -03:00

111 lines
3.4 KiB
Dart

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<ExportResult> exportData({
required PacketBuffer buffer,
required LayoutController layout,
ProgressCallback? onProgress,
}) async {
final builder = CsvRowBuilder(layout);
final chunks = <String>[];
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<void>.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<ExportResult> exportLog({
required LogBuffer buffer,
ProgressCallback? onProgress,
}) async {
final builder = CsvRowBuilder(_NoLayoutPlaceholder());
final chunks = <String>[];
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<void>.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<String> chunks) {
final encoder = utf8.encoder;
final encoded = chunks.map(encoder.convert).toList();
final total = encoded.fold<int>(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);
}