diff --git a/auth/lib/ui/settings/data/export_widget.dart b/auth/lib/ui/settings/data/export_widget.dart index 0df7482898..efb70ecb0b 100644 --- a/auth/lib/ui/settings/data/export_widget.dart +++ b/auth/lib/ui/settings/data/export_widget.dart @@ -1,8 +1,9 @@ import 'dart:convert'; import 'dart:io'; - +import 'dart:ui' as ui; import 'package:ente_auth/core/configuration.dart'; import 'package:ente_auth/l10n/l10n.dart'; +import 'package:ente_auth/models/code.dart'; import 'package:ente_auth/models/export/ente.dart'; import 'package:ente_auth/services/local_authentication_service.dart'; import 'package:ente_auth/store/code_store.dart'; @@ -15,10 +16,9 @@ import 'package:ente_auth/utils/share_utils.dart'; import 'package:ente_auth/utils/toast_util.dart'; import 'package:ente_crypto_dart/ente_crypto_dart.dart'; import 'package:file_saver/file_saver.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -import 'package:logging/logging.dart'; +import 'package:qr_flutter/qr_flutter.dart'; import 'package:share_plus/share_plus.dart'; Future handleExportClick(BuildContext context) async { @@ -41,13 +41,22 @@ Future handleExportClick(BuildContext context) async { isInAlert: true, buttonAction: ButtonAction.second, ), + const ButtonWidget( + buttonType: ButtonType.secondary, + labelText: "HTML", + buttonSize: ButtonSize.large, + isInAlert: true, + buttonAction: ButtonAction.third, + ), ], ); if (result?.action != null && result!.action != ButtonAction.cancel) { if (result.action == ButtonAction.first) { await _requestForEncryptionPassword(context); - } else { - await _showExportWarningDialog(context); + } else if (result.action == ButtonAction.second) { + await _showExportWarningDialog(context, "txt"); + } else if (result.action == ButtonAction.third) { + await _showExportWarningDialog(context, "html"); } } } @@ -98,9 +107,8 @@ Future _requestForEncryptionPassword( ), ); // get json value of data - await _exportCodes(context, jsonEncode(data.toJson())); - } catch (e, s) { - Logger("ExportWidget").severe(e, s); + await _exportCodes(context, jsonEncode(data.toJson()), "txt"); + } catch (e) { showToast(context, "Error while exporting codes."); } } @@ -108,26 +116,35 @@ Future _requestForEncryptionPassword( ); } -Future _showExportWarningDialog(BuildContext context) async { +Future _showExportWarningDialog(BuildContext context, String type) async { await showChoiceActionSheet( context, title: context.l10n.warning, body: context.l10n.exportWarningDesc, isCritical: true, firstButtonOnTap: () async { - final data = await _getAuthDataForExport(); - await _exportCodes(context, data); + if (type == "html") { + final data = await generateHtml(context); + await _exportCodes(context, data, type); + } else { + final data = await _getAuthDataForExport(); + await _exportCodes(context, data, type); + } }, secondButtonLabel: context.l10n.cancel, firstButtonLabel: context.l10n.iUnderStand, ); } -Future _exportCodes(BuildContext context, String fileContent) async { +Future _exportCodes( + BuildContext context, + String fileContent, + String extension, +) async { DateTime now = DateTime.now().toUtc(); String formattedDate = DateFormat('yyyy-MM-dd').format(now); String exportFileName = 'ente-auth-codes-$formattedDate'; - String exportFileExtension = 'txt'; + String exportFileExtension = extension; final hasAuthenticated = await LocalAuthenticationService.instance .requestLocalAuthentication(context, context.l10n.authToExportCodes); await PlatformUtil.refocusWindows(); @@ -170,6 +187,193 @@ Future _exportCodes(BuildContext context, String fileContent) async { ); } +Future generateOTPEntryHtml( + Code code, + BuildContext context, +) async { + final qrBase64 = await generateQRImageBase64( + code.rawData, + ); + return ''' +
+

+

Account: ${code.account}

+

Issuer: ${code.issuer}

+

Type: ${code.type.toString()}

+

Algorithm: ${code.algorithm.toString()}

+

Digits: ${code.digits}

+

+

+ QR Code +

+
+ '''; +} + +Future generateQRImageBase64(String data) async { + final qrPainter = QrPainter( + data: data, + version: QrVersions.auto, + eyeStyle: const QrEyeStyle( + eyeShape: QrEyeShape.square, + color: Colors.black, + ), + dataModuleStyle: const QrDataModuleStyle( + dataModuleShape: QrDataModuleShape.square, + color: Colors.black, + ), + ); + + const size = 250.0; + final recorder = ui.PictureRecorder(); + final canvas = Canvas(recorder); + qrPainter.paint(canvas, const Size(size, size)); + final picture = recorder.endRecording(); + final img = await picture.toImage(size.toInt(), size.toInt()); + final byteData = await img.toByteData(format: ui.ImageByteFormat.png); + final pngBytes = byteData!.buffer.asUint8List(); + + return base64Encode(pngBytes); +} + +Future generateHtml(BuildContext context) async { + DateTime now = DateTime.now().toUtc(); + String formattedDate = DateFormat('yyyy-MM-dd').format(now); + final allCodes = await CodeStore.instance.getAllCodes(); + final List enteries = []; + + for (final code in allCodes) { + if (code.hasError) continue; + final entry = await generateOTPEntryHtml(code, context); + enteries.add(entry); + } + + return ''' + + + + OTP Data Export + + + + + +

Ente OTP Codes Export

+

Export Date: $formattedDate

+ ${enteries.join('\n')} + + + '''; +} + Future _getAuthDataForExport() async { final allCodes = await CodeStore.instance.getAllCodes(); String data = "";