import 'package:flutter/material.dart';
import 'package:qr/qr.dart';
import '../l10n/app_localizations.dart';
import '../models/two_factor_account.dart';
import '../services/storage_service.dart';
import '../utils/qr_utils.dart';
import '../utils/style_utils.dart';
class Export2FaScreen extends StatefulWidget {
const Export2FaScreen({super.key});
@override
State<Export2FaScreen> createState() => _Export2FaScreenState();
}
class _Export2FaScreenState extends State<Export2FaScreen> {
final StorageService _storageService = StorageService();
List<TwoFactorAccount> _accounts = [];
List<bool> _selectedAccounts = [];
String? _qrcodeData;
@override
void initState() {
super.initState();
_loadAccounts();
}
Future<void> _loadAccounts() async {
try {
final accounts = await _storageService.getAllAccounts();
if (!mounted) return;
setState(() {
_accounts = accounts;
_selectedAccounts = List.filled(accounts.length, true);
});
} catch (e) {
if (mounted) {
StyleUtils.errorSnackBar(context, AppLocalizations.of(context).snackLoadFailed(e.toString()));
}
}
}
void _generateQrcode() {
final l10n = AppLocalizations.of(context);
try {
final selectedAccounts = _accounts.where((account) {
final index = _accounts.indexOf(account);
return _selectedAccounts[index];
}).toList();
if (selectedAccounts.isEmpty) {
StyleUtils.errorSnackBar(context, l10n.snackSelectAtLeastOne);
return;
}
final migrationData = _generateMigrationData(selectedAccounts);
setState(() {
_qrcodeData = migrationData;
});
} catch (e) {
StyleUtils.errorSnackBar(context, l10n.snackQrCodeGenerateError);
}
}
String _generateMigrationData(List<TwoFactorAccount> accounts) {
try {
final otps = accounts
.map((account) {
try {
final issuer = account.issuer ?? '';
final name = account.name ?? '';
final fullName = issuer.isNotEmpty && name.isNotEmpty
? '$issuer:$name'
: (name.isNotEmpty ? name : issuer);
return {
'secret': account.secret,
'name': fullName,
'issuer': issuer,
'algorithm': account.algorithm,
'digits': 6,
'type': account.type,
'counter': account.counter,
'period': account.period,
};
} catch (e) {
return null;
}
})
.where((item) => item != null)
.toList();
return QrUtils.generateMigrationData(otps.cast<Map<String, dynamic>>());
} catch (e) {
return QrUtils.generateMigrationData([]);
}
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return Scaffold(
appBar: AppBar(title: Text(l10n.exportTitle)),
body: _qrcodeData == null
? Column(
children: [
Expanded(
child: ListView.builder(
itemCount: _accounts.length,
itemBuilder: (context, index) {
final account = _accounts[index];
return CheckboxListTile(
value: _selectedAccounts[index],
onChanged: (value) {
setState(() {
_selectedAccounts[index] = value ?? false;
});
},
title: Text(
account.displayName,
style: StyleUtils.bodyTextStyle(context),
),
subtitle: Text(
account.issuer ?? '',
style: StyleUtils.subtitleTextStyle(context),
),
);
},
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _generateQrcode,
style: StyleUtils.primaryButtonStyle(context),
child: Text(l10n.buttonExport),
),
),
),
],
)
: Center(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_QrCodeWidget(data: _qrcodeData!, size: 260),
const SizedBox(height: 24),
Text(
l10n.exportScanPrompt,
style: const TextStyle(fontSize: 16),
),
],
),
),
),
),
);
}
}
class _QrCodeWidget extends StatelessWidget {
final String data;
final double size;
const _QrCodeWidget({required this.data, required this.size});
@override
Widget build(BuildContext context) {
final qrCode = QrCode(
payload: QrPayload.fromString(data),
errorCorrectLevel: QrErrorCorrectLevel.medium,
);
final qrImage = QrImage(qrCode);
return CustomPaint(
size: Size(size, size),
painter: _QrPainter(qrImage: qrImage),
);
}
}
class _QrPainter extends CustomPainter {
final QrImage qrImage;
_QrPainter({required this.qrImage});
@override
void paint(Canvas canvas, Size size) {
final moduleCount = qrImage.moduleCount;
final moduleSize = size.width / moduleCount;
final paint = Paint()
..style = PaintingStyle.fill
..color = const Color(0xFF000000);
for (int x = 0; x < moduleCount; x++) {
for (int y = 0; y < moduleCount; y++) {
if (qrImage.isDark(y, x)) {
canvas.drawRect(
Rect.fromLTWH(
x * moduleSize,
y * moduleSize,
moduleSize,
moduleSize,
),
paint,
);
}
}
}
}
@override
bool shouldRepaint(covariant _QrPainter oldDelegate) {
return oldDelegate.qrImage != qrImage;
}
}