241 lines
7.7 KiB
Dart
241 lines
7.7 KiB
Dart
import 'dart:convert';
|
|
import 'dart:typed_data';
|
|
import 'dart:html' as html;
|
|
|
|
import '../../../core/status.dart';
|
|
import 'series_exporter.dart';
|
|
import 'package:syncfusion_flutter_xlsio/xlsio.dart' as xlsio;
|
|
|
|
SeriesExporter getSeriesExporter() => _SeriesExporterWeb();
|
|
|
|
class _SeriesExporterWeb implements SeriesExporter {
|
|
@override
|
|
Future<String?> chooseSavePath({required bool asXlsx, ItemStatus? filter}) async {
|
|
// Browser decides; we only provide filename.
|
|
return _defaultFilename(filter, asXlsx);
|
|
}
|
|
|
|
@override
|
|
Future<String?> exportSeries(
|
|
List<SeriesExportRow> rows, {
|
|
required bool asXlsx,
|
|
ItemStatus? filter,
|
|
String? path,
|
|
}) async {
|
|
final filename = path ?? _defaultFilename(filter, asXlsx);
|
|
final bytes = asXlsx ? _buildXlsxBytes(rows) : _buildCsvBytes(rows);
|
|
_download(bytes, filename);
|
|
return filename;
|
|
}
|
|
|
|
Uint8List _buildCsvBytes(List<SeriesExportRow> rows) {
|
|
final rowsWithEpisodes = rows.where((r) => r.episodesBySeason != null).toList();
|
|
final hasEpisodes = rowsWithEpisodes.isNotEmpty;
|
|
final episodeSeasons = <int>{};
|
|
if (hasEpisodes) {
|
|
for (final r in rowsWithEpisodes) {
|
|
episodeSeasons.addAll(r.episodesBySeason!.keys);
|
|
}
|
|
}
|
|
final seasonList = episodeSeasons.toList()..sort();
|
|
|
|
final csvRows = <List<String>>[];
|
|
// Header rows
|
|
final baseHeader = ['Titel', 'Jahr', 'Serienstatus', 'Cliffhanger', 'Aufloesung', 'DownloadPfad', 'Staffeln', 'Init', 'Progress', 'Done', 'Episoden Gesamt'];
|
|
final seasonHeader = <String>[]..addAll(baseHeader);
|
|
final epHeader = <String>[]..addAll(baseHeader);
|
|
if (hasEpisodes) {
|
|
for (final s in seasonList) {
|
|
final maxEp = _maxEpForSeason(rowsWithEpisodes, s);
|
|
seasonHeader.add('Season $s');
|
|
for (int ep = 1; ep <= maxEp; ep++) {
|
|
seasonHeader.add('');
|
|
epHeader.add('E$ep');
|
|
}
|
|
}
|
|
}
|
|
csvRows.add(seasonHeader);
|
|
if (hasEpisodes) {
|
|
csvRows.add(epHeader);
|
|
}
|
|
|
|
for (final r in rows) {
|
|
final row = [
|
|
r.title,
|
|
r.year?.toString() ?? '',
|
|
r.seriesStatus ?? '',
|
|
r.cliffhanger == null ? '' : (r.cliffhanger! ? 'ja' : 'nein'),
|
|
r.resolution ?? '',
|
|
r.downloadPath ?? '',
|
|
r.seasons.toString(),
|
|
r.episodesInit.toString(),
|
|
r.episodesProgress.toString(),
|
|
r.episodesDone.toString(),
|
|
r.episodesTotal.toString(),
|
|
];
|
|
if (hasEpisodes) {
|
|
for (final s in seasonList) {
|
|
final eps = r.episodesBySeason?[s] ?? const {};
|
|
final maxEp = _maxEpForSeason(rowsWithEpisodes, s);
|
|
row.add('S$s');
|
|
for (int ep = 1; ep <= maxEp; ep++) {
|
|
row.add(_statusCode(eps[ep]));
|
|
}
|
|
}
|
|
}
|
|
csvRows.add(row);
|
|
}
|
|
final csv = _toCsv(csvRows);
|
|
return Uint8List.fromList(utf8.encode(csv));
|
|
}
|
|
|
|
Uint8List _buildXlsxBytes(List<SeriesExportRow> rows) {
|
|
final rowsWithEpisodes = rows.where((r) => r.episodesBySeason != null).toList();
|
|
final hasEpisodes = rowsWithEpisodes.isNotEmpty;
|
|
final episodeSeasons = <int>{};
|
|
if (hasEpisodes) {
|
|
for (final r in rowsWithEpisodes) {
|
|
episodeSeasons.addAll(r.episodesBySeason!.keys);
|
|
}
|
|
}
|
|
final seasonList = episodeSeasons.toList()..sort();
|
|
|
|
final wb = xlsio.Workbook();
|
|
final sheet = wb.worksheets[0];
|
|
sheet.name = 'Serien';
|
|
|
|
final headers = [
|
|
'Titel',
|
|
'Jahr',
|
|
'Serienstatus',
|
|
'Cliffhanger',
|
|
'Aufloesung',
|
|
'DownloadPfad',
|
|
'Staffeln',
|
|
'Init',
|
|
'Progress',
|
|
'Done',
|
|
'Episoden Gesamt',
|
|
];
|
|
var col = 1;
|
|
for (final h in headers) {
|
|
sheet.getRangeByIndex(1, col).setText(h);
|
|
col++;
|
|
}
|
|
int headerRowSpan = 1;
|
|
if (hasEpisodes) {
|
|
headerRowSpan = 2;
|
|
int rowSeason = 1;
|
|
int rowEpisode = 2;
|
|
col = headers.length + 1;
|
|
for (final s in seasonList) {
|
|
final maxEp = _maxEpForSeason(rowsWithEpisodes, s);
|
|
sheet.getRangeByIndex(rowSeason, col).setText('Season $s');
|
|
if (maxEp > 1) {
|
|
sheet.getRangeByIndex(rowSeason, col, rowSeason, col + maxEp - 1).merge();
|
|
}
|
|
for (int ep = 1; ep <= maxEp; ep++) {
|
|
sheet.getRangeByIndex(rowEpisode, col + ep - 1).setText('E$ep');
|
|
}
|
|
col += maxEp;
|
|
}
|
|
}
|
|
|
|
for (var i = 0; i < rows.length; i++) {
|
|
final r = rows[i];
|
|
final row = i + headerRowSpan + 1;
|
|
sheet.getRangeByIndex(row, 1).setText(r.title);
|
|
if (r.year != null) sheet.getRangeByIndex(row, 2).setNumber(r.year!.toDouble());
|
|
sheet.getRangeByIndex(row, 3).setText(r.seriesStatus ?? '');
|
|
sheet.getRangeByIndex(row, 4).setText(r.cliffhanger == null ? '' : (r.cliffhanger! ? 'ja' : 'nein'));
|
|
sheet.getRangeByIndex(row, 5).setText(r.resolution ?? '');
|
|
sheet.getRangeByIndex(row, 6).setText(r.downloadPath ?? '');
|
|
sheet.getRangeByIndex(row, 7).setNumber(r.seasons.toDouble());
|
|
sheet.getRangeByIndex(row, 8).setNumber(r.episodesInit.toDouble());
|
|
sheet.getRangeByIndex(row, 9).setNumber(r.episodesProgress.toDouble());
|
|
sheet.getRangeByIndex(row, 10).setNumber(r.episodesDone.toDouble());
|
|
sheet.getRangeByIndex(row, 11).setNumber(r.episodesTotal.toDouble());
|
|
if (hasEpisodes) {
|
|
var colOffset = headers.length + 1;
|
|
final epsBySeason = r.episodesBySeason ?? const {};
|
|
for (final s in seasonList) {
|
|
final eps = epsBySeason[s] ?? const {};
|
|
final maxEp = _maxEpForSeason(rowsWithEpisodes, s);
|
|
for (int ep = 1; ep <= maxEp; ep++) {
|
|
sheet.getRangeByIndex(row, colOffset + ep - 1).setText(_statusCode(eps[ep]));
|
|
}
|
|
colOffset += maxEp;
|
|
}
|
|
}
|
|
}
|
|
|
|
final bytes = wb.saveAsStream();
|
|
wb.dispose();
|
|
return Uint8List.fromList(bytes);
|
|
}
|
|
|
|
void _download(Uint8List bytes, String filename) {
|
|
final blob = html.Blob([bytes]);
|
|
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);
|
|
}
|
|
|
|
String _defaultFilename(ItemStatus? filter, bool asXlsx) {
|
|
final safeFilter = (filter?.name ?? 'alle').replaceAll(RegExp(r'[^A-Za-z0-9_-]'), '_');
|
|
final ts = DateTime.now();
|
|
String two(int v) => v.toString().padLeft(2, '0');
|
|
final stamp = '${ts.year}${two(ts.month)}${two(ts.day)}_${two(ts.hour)}${two(ts.minute)}${two(ts.second)}';
|
|
return 'series_${safeFilter}_$stamp.${asXlsx ? 'xlsx' : 'csv'}';
|
|
}
|
|
|
|
int _maxEpForSeason(List<SeriesExportRow> rows, int season) {
|
|
var maxEp = 0;
|
|
for (final r in rows) {
|
|
final eps = r.episodesBySeason?[season];
|
|
if (eps != null && eps.isNotEmpty) {
|
|
final m = eps.keys.reduce((a, b) => a > b ? a : b);
|
|
if (m > maxEp) maxEp = m;
|
|
}
|
|
}
|
|
return maxEp;
|
|
}
|
|
|
|
String _statusCode(ItemStatus? st) {
|
|
switch (st) {
|
|
case ItemStatus.Init:
|
|
return 'I';
|
|
case ItemStatus.Progress:
|
|
return 'P';
|
|
case ItemStatus.Done:
|
|
return 'D';
|
|
default:
|
|
return '';
|
|
}
|
|
}
|
|
|
|
String _toCsv(List<List<String>> rows) {
|
|
final buffer = StringBuffer();
|
|
for (var i = 0; i < rows.length; i++) {
|
|
final row = rows[i];
|
|
for (var j = 0; j < row.length; j++) {
|
|
buffer.write(_escape(row[j]));
|
|
if (j < row.length - 1) buffer.write(',');
|
|
}
|
|
if (i < rows.length - 1) buffer.writeln();
|
|
}
|
|
return buffer.toString();
|
|
}
|
|
|
|
String _escape(String v) {
|
|
final needsQuotes = v.contains(',') || v.contains('"') || v.contains('\n') || v.contains('\r');
|
|
final escaped = v.replaceAll('"', '""');
|
|
return needsQuotes ? '"$escaped"' : escaped;
|
|
}
|
|
}
|