Export Movies
This commit is contained in:
@@ -15,6 +15,16 @@ import io.flutter.embedding.engine.FlutterEngine;
|
||||
public final class GeneratedPluginRegistrant {
|
||||
private static final String TAG = "GeneratedPluginRegistrant";
|
||||
public static void registerWith(@NonNull FlutterEngine flutterEngine) {
|
||||
try {
|
||||
flutterEngine.getPlugins().add(new com.mr.flutter.plugin.filepicker.FilePickerPlugin());
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error registering plugin file_picker, com.mr.flutter.plugin.filepicker.FilePickerPlugin", e);
|
||||
}
|
||||
try {
|
||||
flutterEngine.getPlugins().add(new io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin());
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error registering plugin flutter_plugin_android_lifecycle, io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin", e);
|
||||
}
|
||||
try {
|
||||
flutterEngine.getPlugins().add(new io.flutter.plugins.pathprovider.PathProviderPlugin());
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,14 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../core/status.dart';
|
||||
import '../data/movie_repository.dart';
|
||||
import '../data/movie_exporter.dart';
|
||||
import 'widgets/status_chip.dart';
|
||||
import 'movie_detail_screen.dart';
|
||||
import '../../shared/providers.dart';
|
||||
import 'movie_add_screen.dart';
|
||||
|
||||
enum _ExportFormat { csv, xlsx }
|
||||
|
||||
class MovieListScreen extends ConsumerWidget {
|
||||
const MovieListScreen({super.key});
|
||||
|
||||
@@ -31,6 +34,28 @@ class MovieListScreen extends ConsumerWidget {
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
PopupMenuButton<_ExportFormat>(
|
||||
onSelected: (fmt) => _exportMovies(context, ref, filter, fmt),
|
||||
itemBuilder: (ctx) => const [
|
||||
PopupMenuItem(
|
||||
value: _ExportFormat.csv,
|
||||
child: Text('CSV exportieren'),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: _ExportFormat.xlsx,
|
||||
child: Text('XLSX exportieren'),
|
||||
),
|
||||
],
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: const [
|
||||
Icon(Icons.download),
|
||||
SizedBox(width: 6),
|
||||
Text('Export'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Neue Filme hinzufügen'),
|
||||
@@ -269,6 +294,55 @@ class MovieListScreen extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _exportMovies(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
ItemStatus? filter,
|
||||
_ExportFormat fmt,
|
||||
) async {
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final navigator = Navigator.of(context, rootNavigator: true);
|
||||
final exporter = createMovieExporter();
|
||||
// Save-Ziel vor dem Export auswählen
|
||||
String? savePath;
|
||||
try {
|
||||
savePath = await exporter.chooseSavePath(asXlsx: fmt == _ExportFormat.xlsx, filter: filter);
|
||||
} on UnsupportedError catch (e) {
|
||||
messenger.showSnackBar(SnackBar(content: Text(e.message ?? e.toString())));
|
||||
return;
|
||||
} catch (e) {
|
||||
messenger.showSnackBar(SnackBar(content: Text('Export fehlgeschlagen: $e')));
|
||||
return;
|
||||
}
|
||||
if (savePath == null) {
|
||||
messenger.showSnackBar(const SnackBar(content: Text('Export abgebrochen')));
|
||||
return;
|
||||
}
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => const Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
|
||||
try {
|
||||
final movies = await ref.read(moviesProvider.future);
|
||||
final path = await exporter.exportMovies(
|
||||
movies,
|
||||
asXlsx: fmt == _ExportFormat.xlsx,
|
||||
filter: filter,
|
||||
path: savePath,
|
||||
);
|
||||
messenger.showSnackBar(SnackBar(content: Text('Export gespeichert: $path')));
|
||||
} on UnsupportedError catch (e) {
|
||||
messenger.showSnackBar(SnackBar(content: Text(e.message ?? e.toString())));
|
||||
} catch (e) {
|
||||
messenger.showSnackBar(SnackBar(content: Text('Export fehlgeschlagen: $e')));
|
||||
} finally {
|
||||
if (navigator.canPop()) navigator.pop();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Color? _statusBg(ItemStatus s, BuildContext context) {
|
||||
switch (s) {
|
||||
|
||||
+97
-1
@@ -1,6 +1,14 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.6.1"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -65,6 +73,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.18.0"
|
||||
cross_file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cross_file
|
||||
sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.4+2"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -113,6 +129,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
file_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: file_picker
|
||||
sha256: ab13ae8ef5580a411c458d6207b6774a6c237d77ac37011b13994879f68a8810
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.3.7"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -142,6 +166,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.0"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_plugin_android_lifecycle
|
||||
sha256: "1c2b787f99bdca1f3718543f81d38aa1b124817dfeb9fb196201bea85b6134bf"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.26"
|
||||
flutter_riverpod:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -176,6 +208,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
image:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image
|
||||
sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.3.0"
|
||||
intl:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -184,6 +224,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.19.0"
|
||||
jiffy:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: jiffy
|
||||
sha256: "1c1b86459969ff9f32dc5b0ffe392f1e08181e66396cf9dd8fa7c90552a691af"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.2"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -265,7 +313,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.9.0"
|
||||
path_provider:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||
@@ -312,6 +360,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.2"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -437,6 +493,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
syncfusion_flutter_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_flutter_core
|
||||
sha256: "6e67726b85812afc7105725a23620b876ab7f6b04b8410e211330ffb8c2cdbe8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "26.2.14"
|
||||
syncfusion_flutter_xlsio:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: syncfusion_flutter_xlsio
|
||||
sha256: "6250427a781642e22d250b87689d9a09acfd864f0bfbd95642e3e0c52b4e3b46"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "26.2.14"
|
||||
syncfusion_officecore:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_officecore
|
||||
sha256: "9ec8c9d9f66bb20440ff7e31c83fed52fb9d55bc4ba32b6cbaea70fa82ca9b41"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "26.2.14"
|
||||
synchronized:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -565,6 +645,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.10.1"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -573,6 +661,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.5.0"
|
||||
sdks:
|
||||
dart: ">=3.5.0 <4.0.0"
|
||||
flutter: ">=3.24.0"
|
||||
|
||||
@@ -14,6 +14,9 @@ dependencies:
|
||||
cached_network_image: ^3.3.1
|
||||
intl: ^0.19.0
|
||||
url_launcher: ^6.3.0
|
||||
path_provider: ^2.1.4
|
||||
file_picker: ^8.1.2
|
||||
syncfusion_flutter_xlsio: ^26.2.7
|
||||
|
||||
# Material 3 ist im Flutter SDK enthalten
|
||||
|
||||
|
||||
Reference in New Issue
Block a user