[auth] Add feature to export code as a HTML file
This commit is contained in:
@@ -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 = "";
|
||||
|
||||
Reference in New Issue
Block a user