export_accounts.dart 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. import 'package:flutter/material.dart';
  2. import 'package:provider/provider.dart';
  3. import '../state/app_state.dart';
  4. import 'package:pb_authenticator_totp/totp_item.dart';
  5. import 'package:pb_authenticator_totp/otpauth_migration.dart';
  6. import 'package:barcode_scan2/barcode_scan2.dart';
  7. import '../l10n/app_localizations.dart';
  8. class ExportAccountsPage extends StatefulWidget {
  9. const ExportAccountsPage({super.key});
  10. @override
  11. State<ExportAccountsPage> createState() => _ExportAccountsPageState();
  12. }
  13. class _ExportAccountsPageState extends State<ExportAccountsPage> {
  14. static const int maxAccountsPerBatch = 10; // Google Authenticator uses 10
  15. List<String> _qrUris = [];
  16. int _currentIndex = 0;
  17. @override
  18. void initState() {
  19. super.initState();
  20. WidgetsBinding.instance.addPostFrameCallback((_) => _generateQrUris());
  21. }
  22. void _generateQrUris() {
  23. final state = Provider.of<AppState>(context, listen: false);
  24. final items = state.items ?? [];
  25. final otpAuths = items.map((item) => _toOtpAuthUri(item.totp)).toList();
  26. _qrUris = [];
  27. for (int i = 0; i < otpAuths.length; i += maxAccountsPerBatch) {
  28. final batch = otpAuths.sublist(i, (i + maxAccountsPerBatch > otpAuths.length) ? otpAuths.length : i + maxAccountsPerBatch);
  29. final uri = OtpAuthMigration().encode(batch, batchSize: otpAuths.length, batchIndex: (i ~/ maxAccountsPerBatch), batchId: 1);
  30. _qrUris.add(uri);
  31. }
  32. setState(() {});
  33. }
  34. String _toOtpAuthUri(TotpItem item) {
  35. final algorithm = item.algorithm.name.toUpperCase();
  36. return 'otpauth://totp/${Uri.encodeComponent(item.issuer)}:${Uri.encodeComponent(item.accountName)}?secret=${item.secret}&issuer=${Uri.encodeComponent(item.issuer)}&algorithm=$algorithm&digits=${item.digits}&period=${item.period}';
  37. }
  38. @override
  39. Widget build(BuildContext context) {
  40. if (_qrUris.isEmpty) {
  41. final state = Provider.of<AppState>(context, listen: false);
  42. final items = state.items ?? [];
  43. if (items.isEmpty) {
  44. return Scaffold(
  45. appBar: AppBar(title: Text(AppLocalizations.of(context)?.exportAccounts ?? 'Export accounts')),
  46. body: Center(child: Text(AppLocalizations.of(context)?.noAccounts ?? 'No accounts to export.')),
  47. );
  48. }
  49. // Still loading QR codes
  50. return Scaffold(
  51. appBar: AppBar(title: Text(AppLocalizations.of(context)?.exportAccounts ?? 'Export accounts')),
  52. body: const Center(child: CircularProgressIndicator()),
  53. );
  54. }
  55. return Scaffold(
  56. appBar: AppBar(title: Text(AppLocalizations.of(context)?.exportAccounts ?? 'Export accounts')),
  57. body: Column(
  58. mainAxisAlignment: MainAxisAlignment.center,
  59. children: [
  60. Text(
  61. AppLocalizations.of(context)?.exportAccounts ?? 'Export accounts',
  62. style: Theme.of(context).textTheme.titleLarge,
  63. ),
  64. const SizedBox(height: 16),
  65. Expanded(
  66. child: Center(
  67. child: BarcodePainterWidget(data: _qrUris[_currentIndex]),
  68. ),
  69. ),
  70. if (_qrUris.length > 1)
  71. Padding(
  72. padding: const EdgeInsets.symmetric(vertical: 8.0),
  73. child: Row(
  74. mainAxisAlignment: MainAxisAlignment.center,
  75. children: [
  76. IconButton(
  77. icon: const Icon(Icons.arrow_back),
  78. onPressed: _currentIndex > 0
  79. ? () => setState(() => _currentIndex--)
  80. : null,
  81. ),
  82. Text('${_currentIndex + 1} / ${_qrUris.length}'),
  83. IconButton(
  84. icon: const Icon(Icons.arrow_forward),
  85. onPressed: _currentIndex < _qrUris.length - 1
  86. ? () => setState(() => _currentIndex++)
  87. : null,
  88. ),
  89. ],
  90. ),
  91. ),
  92. const SizedBox(height: 16),
  93. ],
  94. ),
  95. );
  96. }
  97. }
  98. class BarcodePainterWidget extends StatelessWidget {
  99. final String data;
  100. const BarcodePainterWidget({super.key, required this.data});
  101. @override
  102. Widget build(BuildContext context) {
  103. // barcode_scan2 does not provide a direct widget, so we use a placeholder for now.
  104. // You should replace this with a QR code widget compatible with your requirements.
  105. return Container(
  106. color: Colors.white,
  107. width: 240,
  108. height: 240,
  109. alignment: Alignment.center,
  110. child: Text(
  111. 'QR code here',
  112. style: TextStyle(color: Colors.black),
  113. ),
  114. );
  115. }
  116. }