import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; import '../core/app_controller.dart'; import '../l10n/strings.dart'; import '../models/server_profile.dart'; import '../services/net/measure.dart'; import 'common.dart'; const _mime = { ReportFormat.txt: 'text/plain', ReportFormat.csv: 'text/csv', ReportFormat.json: 'application/json', }; /// Offers a report as text on the clipboard, through the share sheet, or /// saved to a place the user picks on the device. Nothing is sent anywhere /// without that explicit choice. Future showExportSheet( BuildContext context, AppController c, { required String baseName, required String Function(ReportFormat) build, }) async { final origin = _origin(context); final choice = await showModalBottomSheet<(String, ReportFormat)>( context: context, isScrollControlled: true, builder: (ctx) => SafeArea( child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( title: Text( ctx.tr('exportResults'), style: Theme.of(ctx).textTheme.titleMedium, ), ), for (final f in ReportFormat.values) ...[ ListTile( leading: const Icon(Icons.save_alt), title: Text( '${ctx.tr('saveToDevice')} · ${f.name.toUpperCase()}', ), onTap: () => Navigator.pop(ctx, ('save', f)), ), ListTile( leading: const Icon(Icons.ios_share), title: Text('${ctx.tr('shareFile')} · ${f.name.toUpperCase()}'), onTap: () => Navigator.pop(ctx, ('share', f)), ), ], ListTile( leading: const Icon(Icons.copy), title: Text('${ctx.tr('copyResults')} · TXT'), onTap: () => Navigator.pop(ctx, ('copy', ReportFormat.txt)), ), ], ), ), ), ); if (choice == null || !context.mounted) return; final (action, format) = choice; final text = build(format); final name = '$baseName.${format.name}'; try { switch (action) { case 'copy': await Clipboard.setData(ClipboardData(text: text)); if (context.mounted) _done(context, 'copied'); case 'save': final saved = await c.withSystemDialog( () => c.documents.saveAs(name, utf8.encode(text), mime: _mime[format]), ); if (saved != null && context.mounted) _done(context, 'savedToDevice'); case 'share': await shareText(c, name, text, origin); } } catch (e) { if (context.mounted) showFailure(context, e); } } Rect _origin(BuildContext context) { final box = context.findRenderObject() as RenderBox?; final o = box?.localToGlobal(Offset.zero) ?? Offset.zero; return Rect.fromLTWH(o.dx, o.dy, 1, 1); } void _done(BuildContext context, String key) => ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(context.tr(key)))); /// Shares [text] as a file named [name] through the system share sheet. The /// temporary copy is deleted afterwards. Future shareText( AppController c, String name, String text, Rect origin, ) => shareBytes(c, name, utf8.encode(text), origin); Future shareBytes( AppController c, String name, List bytes, Rect origin, ) async { final temp = await getTemporaryDirectory(); final dir = await Directory('${temp.path}/sm-transfer-${newId()}').create(); try { final file = File('${dir.path}/$name'); await file.writeAsBytes(bytes, flush: true); await c.withSystemDialog( () => SharePlus.instance.share( ShareParams(files: [XFile(file.path)], sharePositionOrigin: origin), ), ); } finally { if (await dir.exists()) await dir.delete(recursive: true); } }