Export Movies
parent
2628ae138a
commit
4777fe49f9
@ -0,0 +1,22 @@
|
|||||||
|
import '../../../core/status.dart';
|
||||||
|
import 'movie_model.dart';
|
||||||
|
import 'movie_exporter_stub.dart'
|
||||||
|
if (dart.library.html) 'movie_exporter_web.dart'
|
||||||
|
if (dart.library.io) 'movie_exporter_io.dart';
|
||||||
|
|
||||||
|
/// Factory to obtain the platform-specific exporter implementation.
|
||||||
|
MovieExporter createMovieExporter() => getMovieExporter();
|
||||||
|
|
||||||
|
abstract class MovieExporter {
|
||||||
|
/// Opens a save dialog and returns the chosen path, or null if cancelled.
|
||||||
|
Future<String?> chooseSavePath({required bool asXlsx, ItemStatus? filter});
|
||||||
|
|
||||||
|
/// Exports movies to disk and returns the written file path.
|
||||||
|
/// Returns `null` if the user cancels the dialog.
|
||||||
|
Future<String?> exportMovies(
|
||||||
|
List<Movie> movies, {
|
||||||
|
required bool asXlsx,
|
||||||
|
ItemStatus? filter,
|
||||||
|
String? path,
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -0,0 +1,127 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:syncfusion_flutter_xlsio/xlsio.dart' as xlsio;
|
||||||
|
|
||||||
|
import '../../../core/status.dart';
|
||||||
|
import 'movie_model.dart';
|
||||||
|
import 'movie_exporter.dart';
|
||||||
|
|
||||||
|
MovieExporter getMovieExporter() => _IoMovieExporter();
|
||||||
|
|
||||||
|
class _IoMovieExporter implements MovieExporter {
|
||||||
|
@override
|
||||||
|
Future<String?> exportMovies(
|
||||||
|
List<Movie> movies, {
|
||||||
|
required bool asXlsx,
|
||||||
|
ItemStatus? filter,
|
||||||
|
String? path,
|
||||||
|
}) async {
|
||||||
|
final savePath = path ?? await chooseSavePath(asXlsx: asXlsx, filter: filter);
|
||||||
|
if (savePath == null) return null;
|
||||||
|
if (asXlsx) {
|
||||||
|
await _writeXlsx(savePath, movies);
|
||||||
|
} else {
|
||||||
|
await _writeCsv(savePath, movies);
|
||||||
|
}
|
||||||
|
|
||||||
|
return savePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> chooseSavePath({required bool asXlsx, ItemStatus? filter}) async {
|
||||||
|
final dir = await getApplicationDocumentsDirectory();
|
||||||
|
await dir.create(recursive: true);
|
||||||
|
final safeFilter = _sanitize(filter?.name ?? 'alle');
|
||||||
|
final ts = DateTime.now();
|
||||||
|
final name = 'movies_${safeFilter}_${_ts(ts)}.${asXlsx ? 'xlsx' : 'csv'}';
|
||||||
|
|
||||||
|
String? savePath;
|
||||||
|
try {
|
||||||
|
savePath = await FilePicker.platform.saveFile(
|
||||||
|
dialogTitle: 'Export speichern',
|
||||||
|
fileName: name,
|
||||||
|
type: FileType.custom,
|
||||||
|
allowedExtensions: [asXlsx ? 'xlsx' : 'csv'],
|
||||||
|
initialDirectory: dir.path,
|
||||||
|
);
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
// propagate to trigger UI fallback
|
||||||
|
throw Exception('Speicherdialog fehlgeschlagen: ${e.code}');
|
||||||
|
}
|
||||||
|
if (savePath == null) return null; // user cancelled
|
||||||
|
return savePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _writeCsv(String path, List<Movie> movies) async {
|
||||||
|
final rows = <List<String>>[
|
||||||
|
['Titel', 'Jahr', 'Status', 'Aufloesung', 'TMDB-ID', 'Beschreibung'],
|
||||||
|
for (final m in movies)
|
||||||
|
[
|
||||||
|
m.title,
|
||||||
|
m.releaseYear?.toString() ?? '',
|
||||||
|
m.status.name,
|
||||||
|
m.resolution ?? '',
|
||||||
|
m.tmdbId.toString(),
|
||||||
|
m.overview ?? '',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
final content = _toCsv(rows);
|
||||||
|
final file = File(path);
|
||||||
|
await file.writeAsString(content, flush: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _writeXlsx(String path, List<Movie> movies) async {
|
||||||
|
final wb = xlsio.Workbook();
|
||||||
|
final sheet = wb.worksheets[0];
|
||||||
|
sheet.name = 'Filme';
|
||||||
|
// Header
|
||||||
|
final headers = ['Titel', 'Jahr', 'Status', 'Aufloesung', 'TMDB-ID', 'Beschreibung'];
|
||||||
|
for (var c = 0; c < headers.length; c++) {
|
||||||
|
sheet.getRangeByIndex(1, c + 1).setText(headers[c]);
|
||||||
|
}
|
||||||
|
// Data
|
||||||
|
for (var i = 0; i < movies.length; i++) {
|
||||||
|
final m = movies[i];
|
||||||
|
final row = i + 2;
|
||||||
|
sheet.getRangeByIndex(row, 1).setText(m.title);
|
||||||
|
sheet.getRangeByIndex(row, 2).setNumber((m.releaseYear ?? 0).toDouble());
|
||||||
|
sheet.getRangeByIndex(row, 3).setText(m.status.name);
|
||||||
|
sheet.getRangeByIndex(row, 4).setText(m.resolution ?? '');
|
||||||
|
sheet.getRangeByIndex(row, 5).setNumber(m.tmdbId.toDouble());
|
||||||
|
sheet.getRangeByIndex(row, 6).setText(m.overview ?? '');
|
||||||
|
}
|
||||||
|
final bytes = wb.saveAsStream();
|
||||||
|
wb.dispose();
|
||||||
|
final file = File(path);
|
||||||
|
await file.writeAsBytes(bytes, flush: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _ts(DateTime dt) {
|
||||||
|
final two = (int v) => v.toString().padLeft(2, '0');
|
||||||
|
return '${dt.year}${two(dt.month)}${two(dt.day)}_${two(dt.hour)}${two(dt.minute)}${two(dt.second)}';
|
||||||
|
}
|
||||||
|
|
||||||
|
String _sanitize(String input) => input.replaceAll(RegExp(r'[^A-Za-z0-9_-]'), '_');
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
import '../../../core/status.dart';
|
||||||
|
import 'movie_model.dart';
|
||||||
|
import 'movie_exporter.dart';
|
||||||
|
|
||||||
|
MovieExporter getMovieExporter() => _WebMovieExporter();
|
||||||
|
|
||||||
|
class _WebMovieExporter implements MovieExporter {
|
||||||
|
@override
|
||||||
|
Future<String?> chooseSavePath({required bool asXlsx, ItemStatus? filter}) {
|
||||||
|
return Future.error(
|
||||||
|
UnsupportedError('Export wird auf dieser Plattform nicht unterstützt.'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> exportMovies(
|
||||||
|
List<Movie> movies, {
|
||||||
|
required bool asXlsx,
|
||||||
|
ItemStatus? filter,
|
||||||
|
String? path,
|
||||||
|
}) {
|
||||||
|
return Future.error(
|
||||||
|
UnsupportedError('Export wird auf dieser Plattform nicht unterstützt.'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,111 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'dart:html' as html;
|
||||||
|
import 'package:syncfusion_flutter_xlsio/xlsio.dart' as xlsio;
|
||||||
|
|
||||||
|
import '../../../core/status.dart';
|
||||||
|
import 'movie_exporter.dart';
|
||||||
|
import 'movie_model.dart';
|
||||||
|
|
||||||
|
MovieExporter getMovieExporter() => _WebMovieExporter();
|
||||||
|
|
||||||
|
class _WebMovieExporter implements MovieExporter {
|
||||||
|
@override
|
||||||
|
Future<String?> chooseSavePath({required bool asXlsx, ItemStatus? filter}) async {
|
||||||
|
// On web, saveFile shows the browser download dialog when bytes are provided.
|
||||||
|
// We return only the filename here; actual save happens in exportMovies.
|
||||||
|
return _defaultFilename(filter, asXlsx);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> exportMovies(
|
||||||
|
List<Movie> movies, {
|
||||||
|
required bool asXlsx,
|
||||||
|
ItemStatus? filter,
|
||||||
|
String? path,
|
||||||
|
}) async {
|
||||||
|
final filename = path ?? _defaultFilename(filter, asXlsx);
|
||||||
|
final bytes = asXlsx ? _buildXlsxBytes(movies) : _buildCsvBytes(movies);
|
||||||
|
_downloadBytes(bytes, filename);
|
||||||
|
return filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint8List _buildCsvBytes(List<Movie> movies) {
|
||||||
|
final rows = <List<String>>[
|
||||||
|
['Titel', 'Jahr', 'Status', 'Aufloesung', 'TMDB-ID', 'Beschreibung'],
|
||||||
|
for (final m in movies)
|
||||||
|
[
|
||||||
|
m.title,
|
||||||
|
m.releaseYear?.toString() ?? '',
|
||||||
|
m.status.name,
|
||||||
|
m.resolution ?? '',
|
||||||
|
m.tmdbId.toString(),
|
||||||
|
m.overview ?? '',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
final csv = _toCsv(rows);
|
||||||
|
return Uint8List.fromList(utf8.encode(csv));
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint8List _buildXlsxBytes(List<Movie> movies) {
|
||||||
|
final wb = xlsio.Workbook();
|
||||||
|
final sheet = wb.worksheets[0];
|
||||||
|
sheet.name = 'Filme';
|
||||||
|
final headers = ['Titel', 'Jahr', 'Status', 'Aufloesung', 'TMDB-ID', 'Beschreibung'];
|
||||||
|
for (var c = 0; c < headers.length; c++) {
|
||||||
|
sheet.getRangeByIndex(1, c + 1).setText(headers[c]);
|
||||||
|
}
|
||||||
|
for (var i = 0; i < movies.length; i++) {
|
||||||
|
final m = movies[i];
|
||||||
|
final row = i + 2;
|
||||||
|
sheet.getRangeByIndex(row, 1).setText(m.title);
|
||||||
|
sheet.getRangeByIndex(row, 2).setNumber((m.releaseYear ?? 0).toDouble());
|
||||||
|
sheet.getRangeByIndex(row, 3).setText(m.status.name);
|
||||||
|
sheet.getRangeByIndex(row, 4).setText(m.resolution ?? '');
|
||||||
|
sheet.getRangeByIndex(row, 5).setNumber(m.tmdbId.toDouble());
|
||||||
|
sheet.getRangeByIndex(row, 6).setText(m.overview ?? '');
|
||||||
|
}
|
||||||
|
final bytes = wb.saveAsStream();
|
||||||
|
wb.dispose();
|
||||||
|
return Uint8List.fromList(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _downloadBytes(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 'movies_${safeFilter}_$stamp.${asXlsx ? 'xlsx' : 'csv'}';
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue