[auth] Add feature to export code as a HTML file

This commit is contained in:
Aman Raj
2024-11-25 12:26:35 +05:30
parent 5786a3cb79
commit f439f2fcec

View File

@@ -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<void> handleExportClick(BuildContext context) async {
@@ -41,13 +41,22 @@ Future<void> 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<void> _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<void> _requestForEncryptionPassword(
);
}
Future<void> _showExportWarningDialog(BuildContext context) async {
Future<void> _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<void> _exportCodes(BuildContext context, String fileContent) async {
Future<void> _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<void> _exportCodes(BuildContext context, String fileContent) async {
);
}
Future<String> generateOTPEntryHtml(
Code code,
BuildContext context,
) async {
final qrBase64 = await generateQRImageBase64(
code.rawData,
);
return '''
<div class="otp-entry">
<p class="details">
<p><span class="label">Account:</span> ${code.account}</p>
<p><span class="label">Issuer:</span> ${code.issuer}</p>
<p><span class="label">Type:</span> ${code.type.toString()}</p>
<p><span class="label">Algorithm:</span> ${code.algorithm.toString()}</p>
<p><span class="label">Digits:</span> ${code.digits}</p>
</p>
<p><span class="qr-code">
<img src="data:image/png;base64,$qrBase64" alt="QR Code">
</span></p>
</div>
''';
}
Future<String> 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<String> 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<String> enteries = [];
for (final code in allCodes) {
if (code.hasError) continue;
final entry = await generateOTPEntryHtml(code, context);
enteries.add(entry);
}
return '''
<!DOCTYPE html>
<html>
<head>
<title>OTP Data Export</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
html {
font-size: 62.5%;
box-sizing: border-box;
}
*, *:before, *:after {
box-sizing: inherit;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 1.6rem;
line-height: 1.5;
max-width: 80rem;
margin: 0 auto;
padding: 2rem;
background-color: #f8f9fa;
color: #212529;
}
.otp-entry {
background: white;
border-radius: 1.2rem;
margin: 2.4rem 0;
padding: 2.4rem;
box-shadow: 0 0.2rem 1rem rgba(0, 0, 0, 0.08);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.otp-entry:hover {
transform: translateY(-0.2rem);
box-shadow: 0 0.4rem 1.5rem rgba(0, 0, 0, 0.12);
}
h1 {
font-size: 3.2rem;
font-weight: 700;
color: #1a1a1a;
text-align: center;
margin: 3.2rem 0;
letter-spacing: -0.02em;
}
h2 {
font-size: 2.4rem;
font-weight: 600;
color: #2d2d2d;
margin: 0 0 1.6rem 0;
letter-spacing: -0.01em;
}
.details {
margin: 1.6rem 0;
font-size: 1.4rem;
}
.label {
color: #6c757d;
font-weight: 500;
margin-right: 1.2rem;
display: inline-block;
min-width: 12rem;
}
.qr-code {
margin-top: 2.4rem;
text-align: center;
}
.qr-code img {
max-width: 30rem;
height: auto;
border-radius: 0.8rem;
box-shadow: 0 0.2rem 0.8rem rgba(0, 0, 0, 0.06);
}
p {
margin: 0.8rem 0;
line-height: 1.5;
}
.timestamp {
text-align: center;
color: #6c757d;
margin: 2.4rem 0 4rem;
font-size: 1.4rem;
font-weight: 500;
}
@media (max-width: 600px) {
html {
font-size: 56.25%;
}
body {
padding: 1.6rem;
}
.otp-entry {
padding: 1.6rem;
margin: 1.6rem 0;
}
.label {
min-width: 9rem;
}
}
</style>
</head>
<body>
<h1>Ente OTP Codes Export</h1>
<p class="timestamp">Export Date: $formattedDate</p>
${enteries.join('\n')}
</body>
</html>
''';
}
Future<String> _getAuthDataForExport() async {
final allCodes = await CodeStore.instance.getAllCodes();
String data = "";