111 lines
3.4 KiB
Dart
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);
|
|
} |