Browse Source

Add export accounts, and qr generation

Sasan Salamzadeh 1 year ago
parent
commit
7f69b24f92

+ 1 - 1
android/app/src/main/AndroidManifest.xml

@@ -1,6 +1,6 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android">
     <application
-        android:label="PB Authenticator"
+        android:label="Authenticator"
         android:name="${applicationName}"
         android:icon="@mipmap/ic_launcher">
         <activity

+ 4 - 1
lib/l10n/app_en.arb

@@ -50,5 +50,8 @@
     "exportAccounts": "Export accounts",
     "importAccounts": "Import accounts",
     "importSuccess": "Accounts imported successfully!",
-    "importDescription": "Import your accounts from Google Authenticator or PB Authenticator by scanning a QR code."
+    "importDescription": "Import your accounts from Google Authenticator or PB Authenticator by scanning a QR code.",
+    "exportDescription": "Select which accounts to export and generate a QR code to transfer them to another device.",
+    "generateQr": "Generate QR",
+    "noAccountsSelected": "No accounts selected."
 }

+ 18 - 0
lib/l10n/app_localizations.dart

@@ -405,6 +405,24 @@ abstract class AppLocalizations {
   /// In en, this message translates to:
   /// **'Import your accounts from Google Authenticator or PB Authenticator by scanning a QR code.'**
   String get importDescription;
+
+  /// No description provided for @exportDescription.
+  ///
+  /// In en, this message translates to:
+  /// **'Select which accounts to export and generate a QR code to transfer them to another device.'**
+  String get exportDescription;
+
+  /// No description provided for @generateQr.
+  ///
+  /// In en, this message translates to:
+  /// **'Generate QR'**
+  String get generateQr;
+
+  /// No description provided for @noAccountsSelected.
+  ///
+  /// In en, this message translates to:
+  /// **'No accounts selected.'**
+  String get noAccountsSelected;
 }
 
 class _AppLocalizationsDelegate

+ 10 - 0
lib/l10n/app_localizations_en.dart

@@ -172,4 +172,14 @@ class AppLocalizationsEn extends AppLocalizations {
   @override
   String get importDescription =>
       'Import your accounts from Google Authenticator or PB Authenticator by scanning a QR code.';
+
+  @override
+  String get exportDescription =>
+      'Select which accounts to export and generate a QR code to transfer them to another device.';
+
+  @override
+  String get generateQr => 'Generate QR';
+
+  @override
+  String get noAccountsSelected => 'No accounts selected.';
 }

+ 145 - 66
lib/pages/export_accounts.dart

@@ -3,8 +3,8 @@ import 'package:provider/provider.dart';
 import '../state/app_state.dart';
 import 'package:pb_authenticator_totp/totp_item.dart';
 import 'package:pb_authenticator_totp/otpauth_migration.dart';
-import 'package:barcode_scan2/barcode_scan2.dart';
 import '../l10n/app_localizations.dart';
+import 'package:qr_flutter/qr_flutter.dart';
 
 class ExportAccountsPage extends StatefulWidget {
   const ExportAccountsPage({super.key});
@@ -17,24 +17,116 @@ class _ExportAccountsPageState extends State<ExportAccountsPage> {
   static const int maxAccountsPerBatch = 10; // Google Authenticator uses 10
   List<String> _qrUris = [];
   int _currentIndex = 0;
+  List<bool> _selected = [];
+  bool _qrGenerated = false;
 
   @override
   void initState() {
     super.initState();
-    WidgetsBinding.instance.addPostFrameCallback((_) => _generateQrUris());
+    WidgetsBinding.instance.addPostFrameCallback((_) => _initSelection());
+  }
+
+  void _initSelection() {
+    final state = Provider.of<AppState>(context, listen: false);
+    final items = state.items ?? [];
+    setState(() {
+      _selected = List.generate(items.length, (_) => true);
+      _qrUris = [];
+      _qrGenerated = false;
+      _currentIndex = 0;
+    });
   }
 
   void _generateQrUris() {
     final state = Provider.of<AppState>(context, listen: false);
     final items = state.items ?? [];
-    final otpAuths = items.map((item) => _toOtpAuthUri(item.totp)).toList();
-    _qrUris = [];
+    final selectedItems = <TotpItem>[];
+    for (int i = 0; i < items.length; i++) {
+      if (_selected[i]) {
+        selectedItems.add(items[i].totp);
+      }
+    }
+    if (selectedItems.isEmpty) {
+      setState(() {
+        _qrUris = [];
+        _qrGenerated = true;
+      });
+      return;
+    }
+    final otpAuths = selectedItems.map((item) => _toOtpAuthUri(item)).toList();
+    final qrUris = <String>[];
     for (int i = 0; i < otpAuths.length; i += maxAccountsPerBatch) {
       final batch = otpAuths.sublist(i, (i + maxAccountsPerBatch > otpAuths.length) ? otpAuths.length : i + maxAccountsPerBatch);
       final uri = OtpAuthMigration().encode(batch, batchSize: otpAuths.length, batchIndex: (i ~/ maxAccountsPerBatch), batchId: 1);
-      _qrUris.add(uri);
+      qrUris.add(uri);
+    }
+    setState(() {
+      _qrUris = qrUris;
+      _qrGenerated = true;
+      _currentIndex = 0;
+    });
+    if (qrUris.isNotEmpty) {
+      _showQrDialog(qrUris);
     }
-    setState(() {});
+  }
+
+  void _showQrDialog(List<String> qrUris) {
+    int dialogIndex = 0;
+    showDialog(
+      context: context,
+      builder: (context) {
+        return StatefulBuilder(
+          builder: (context, setState) {
+            return AlertDialog(
+              contentPadding: const EdgeInsets.all(16),
+              content: SizedBox(
+                width: 360,
+                child: Column(
+                  mainAxisSize: MainAxisSize.min,
+                  children: [
+                    QrImageView(
+                      data: qrUris[dialogIndex],
+                      version: QrVersions.auto,
+                      size: 320,
+                      backgroundColor: Colors.white,
+                      errorStateBuilder: (cxt, err) => Center(child: Text('QR error', style: TextStyle(color: Colors.red))),
+                    ),
+                    if (qrUris.length > 1)
+                      Padding(
+                        padding: const EdgeInsets.only(top: 16),
+                        child: Row(
+                          mainAxisAlignment: MainAxisAlignment.center,
+                          children: [
+                            IconButton(
+                              icon: const Icon(Icons.arrow_back),
+                              onPressed: dialogIndex > 0
+                                  ? () => setState(() => dialogIndex--)
+                                  : null,
+                            ),
+                            Text('${dialogIndex + 1} / ${qrUris.length}'),
+                            IconButton(
+                              icon: const Icon(Icons.arrow_forward),
+                              onPressed: dialogIndex < qrUris.length - 1
+                                  ? () => setState(() => dialogIndex++)
+                                  : null,
+                            ),
+                          ],
+                        ),
+                      ),
+                  ],
+                ),
+              ),
+              actions: [
+                TextButton(
+                  onPressed: () => Navigator.of(context).pop(),
+                  child: const Text('Close'),
+                ),
+              ],
+            );
+          },
+        );
+      },
+    );
   }
 
   String _toOtpAuthUri(TotpItem item) {
@@ -44,61 +136,53 @@ class _ExportAccountsPageState extends State<ExportAccountsPage> {
 
   @override
   Widget build(BuildContext context) {
-    if (_qrUris.isEmpty) {
-      final state = Provider.of<AppState>(context, listen: false);
-      final items = state.items ?? [];
-      if (items.isEmpty) {
-        return Scaffold(
-          appBar: AppBar(title: Text(AppLocalizations.of(context)?.exportAccounts ?? 'Export accounts')),
-          body: Center(child: Text(AppLocalizations.of(context)?.noAccounts ?? 'No accounts to export.')),
-        );
-      }
-      // Still loading QR codes
-      return Scaffold(
-        appBar: AppBar(title: Text(AppLocalizations.of(context)?.exportAccounts ?? 'Export accounts')),
-        body: const Center(child: CircularProgressIndicator()),
-      );
-    }
+    final state = Provider.of<AppState>(context);
+    final items = state.items ?? [];
+    final loc = AppLocalizations.of(context);
     return Scaffold(
-      appBar: AppBar(title: Text(AppLocalizations.of(context)?.exportAccounts ?? 'Export accounts')),
-      body: Column(
-        mainAxisAlignment: MainAxisAlignment.center,
-        children: [
-          Text(
-            AppLocalizations.of(context)?.exportAccounts ?? 'Export accounts',
-            style: Theme.of(context).textTheme.titleLarge,
-          ),
-          const SizedBox(height: 16),
-          Expanded(
-            child: Center(
-              child: BarcodePainterWidget(data: _qrUris[_currentIndex]),
-            ),
-          ),
-          if (_qrUris.length > 1)
-            Padding(
-              padding: const EdgeInsets.symmetric(vertical: 8.0),
-              child: Row(
-                mainAxisAlignment: MainAxisAlignment.center,
+      appBar: AppBar(title: Text(loc?.exportAccounts ?? 'Export accounts')),
+      body: items.isEmpty
+          ? Center(child: Text(loc?.noAccounts ?? 'No accounts to export.'))
+          : Padding(
+              padding: const EdgeInsets.all(16.0),
+              child: Column(
                 children: [
-                  IconButton(
-                    icon: const Icon(Icons.arrow_back),
-                    onPressed: _currentIndex > 0
-                        ? () => setState(() => _currentIndex--)
-                        : null,
+                  Icon(Icons.qr_code, size: 48, color: Colors.blue),
+                  const SizedBox(height: 8),
+                  Text(
+                    loc?.exportDescription ?? 'Select which accounts to export and generate a QR code to transfer them to another device.',
+                    textAlign: TextAlign.center,
+                    style: Theme.of(context).textTheme.bodyMedium,
+                  ),
+                  const SizedBox(height: 16),
+                  Expanded(
+                    child: ListView.builder(
+                      itemCount: items.length,
+                      itemBuilder: (context, i) {
+                        final item = items[i];
+                        return CheckboxListTile(
+                          value: _selected[i],
+                          onChanged: (val) {
+                            setState(() {
+                              _selected[i] = val ?? false;
+                            });
+                          },
+                          title: Text('${item.totp.issuer}: ${item.totp.accountName}'),
+                        );
+                      },
+                    ),
                   ),
-                  Text('${_currentIndex + 1} / ${_qrUris.length}'),
-                  IconButton(
-                    icon: const Icon(Icons.arrow_forward),
-                    onPressed: _currentIndex < _qrUris.length - 1
-                        ? () => setState(() => _currentIndex++)
-                        : null,
+                  ElevatedButton.icon(
+                    icon: const Icon(Icons.qr_code),
+                    label: Text(loc?.generateQr ?? 'Generate QR'),
+                    onPressed: _selected.any((s) => s) ? _generateQrUris : null,
                   ),
+                  const SizedBox(height: 16),
+                  if (_qrGenerated && _qrUris.isEmpty)
+                    Text(loc?.noAccountsSelected ?? 'No accounts selected.'),
                 ],
               ),
             ),
-          const SizedBox(height: 16),
-        ],
-      ),
     );
   }
 }
@@ -109,17 +193,12 @@ class BarcodePainterWidget extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    // barcode_scan2 does not provide a direct widget, so we use a placeholder for now.
-    // You should replace this with a QR code widget compatible with your requirements.
-    return Container(
-      color: Colors.white,
-      width: 240,
-      height: 240,
-      alignment: Alignment.center,
-      child: Text(
-        'QR code here',
-        style: TextStyle(color: Colors.black),
-      ),
+    return QrImageView(
+      data: data,
+      version: QrVersions.auto,
+      size: 240,
+      backgroundColor: Colors.white,
+      errorStateBuilder: (cxt, err) => Center(child: Text('QR error', style: TextStyle(color: Colors.red))),
     );
   }
-} 
+} 

+ 16 - 0
pubspec.lock

@@ -387,6 +387,22 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "6.1.2"
+  qr:
+    dependency: transitive
+    description:
+      name: qr
+      sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.0.2"
+  qr_flutter:
+    dependency: "direct main"
+    description:
+      name: qr_flutter
+      sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097"
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.1.0"
   sky_engine:
     dependency: transitive
     description: flutter

+ 2 - 0
pubspec.yaml

@@ -31,6 +31,8 @@ dependencies:
   barcode_scan2: 4.5.1
   provider: ^6.1.2
   introduction_screen: ^3.1.11
+  qr_flutter: ^4.1.0
+
 
 dev_dependencies:
   flutter_test: