diff --git a/mobile/apps/auth/flutter b/mobile/apps/auth/flutter index edada7c56e..2663184aa7 160000 --- a/mobile/apps/auth/flutter +++ b/mobile/apps/auth/flutter @@ -1 +1 @@ -Subproject commit edada7c56edf4a183c1735310e123c7f923584f1 +Subproject commit 2663184aa79047d0a33a14a3b607954f8fdd8730 diff --git a/mobile/apps/auth/lib/l10n/arb/app_en.arb b/mobile/apps/auth/lib/l10n/arb/app_en.arb index 6fc8f8ad55..a0a94c5045 100644 --- a/mobile/apps/auth/lib/l10n/arb/app_en.arb +++ b/mobile/apps/auth/lib/l10n/arb/app_en.arb @@ -519,5 +519,12 @@ "algorithm": "Algorithm", "type": "Type", "period": "Period", - "digits": "Digits" + "digits": "Digits", + "importFromGallery": "Import from gallery", + "errorCouldNotReadImage": "Could not read the selected image file.", + "errorInvalidQRCode": "Invalid QR Code", + "errorInvalidQRCodeBody": "The scanned QR code is not a valid 2FA account.", + "errorNoQRCode": "No QR code found", + "errorGenericTitle": "An Error Occurred", + "errorGenericBody": "An unexpected error occurred while importing." } \ No newline at end of file diff --git a/mobile/apps/auth/lib/ui/home_page.dart b/mobile/apps/auth/lib/ui/home_page.dart index 32c8c2683f..ed4f9c6094 100644 --- a/mobile/apps/auth/lib/ui/home_page.dart +++ b/mobile/apps/auth/lib/ui/home_page.dart @@ -35,18 +35,22 @@ import 'package:ente_auth/ui/settings_page.dart'; import 'package:ente_auth/ui/sort_option_menu.dart'; import 'package:ente_auth/utils/dialog_util.dart'; import 'package:ente_auth/utils/platform_util.dart'; +import 'package:ente_auth/utils/toast_util.dart'; import 'package:ente_auth/utils/totp_util.dart'; import 'package:ente_events/event_bus.dart'; import 'package:ente_lock_screen/lock_screen_settings.dart'; import 'package:ente_lock_screen/ui/app_lock.dart'; import 'package:ente_ui/pages/base_home_page.dart'; +import 'package:file_picker/file_picker.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_speed_dial/flutter_speed_dial.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; +import 'package:image/image.dart' as img; import 'package:logging/logging.dart'; import 'package:move_to_background/move_to_background.dart'; +import 'package:zxing2/qrcode.dart'; class HomePage extends BaseHomePage { const HomePage({super.key}); @@ -62,6 +66,7 @@ class _HomePageState extends State { ); bool _hasLoaded = false; bool _isSettingsOpen = false; + bool _isImportingFromGallery = false; final Logger _logger = Logger("HomePage"); final scaffoldKey = GlobalKey(); @@ -288,6 +293,70 @@ class _HomePageState extends State { } } + Future _importFromGallery() async { + + final l10n = AppLocalizations.of(context); + + if (_isImportingFromGallery) { + return; + } + + setState(() { + _isImportingFromGallery = true; + }); + + try { + final FilePickerResult? result = await FilePicker.platform.pickFiles( + type: FileType.image, + ); + + if (result == null || result.files.single.path == null) { + return; + } + + final String imagePath = result.files.single.path!; + final rawImage = await File(imagePath).readAsBytes(); + final image = img.decodeImage(rawImage); + + if (image == null) { + await showErrorDialog(context, l10n.error, l10n.errorCouldNotReadImage); + return; + } + + final source = RGBLuminanceSource( + image.width, image.height, + image.getBytes(order: img.ChannelOrder.rgba).buffer.asInt32List(), + ); + final bitmap = BinaryBitmap(HybridBinarizer(source)); + final reader = QRCodeReader(); + final Result decodeResult = reader.decode(bitmap); + final String code = decodeResult.text; + try{ + final newCode = Code.fromOTPAuthUrl(code); + await CodeStore.instance.addCode(newCode, shouldSync: false); + } + catch (e){ + await showErrorDialog( + context, l10n.errorInvalidQRCode, l10n.errorInvalidQRCodeBody, + ); + } + } + on ReaderException { + showToast(context, l10n.errorNoQRCode); + } + + catch (e) { + await showErrorDialog( + context, l10n.errorGenericTitle, l10n.errorGenericBody, + ); + } + finally { + setState(() { + _isImportingFromGallery = false; + }); + } +} + Future _redirectToScannerPage() async { final Code? code = await Navigator.of(context).push( MaterialPageRoute( @@ -745,6 +814,13 @@ class _HomePageState extends State { labelWidget: SpeedDialLabelWidget(context.l10n.enterDetailsManually), onTap: _redirectToManualEntryPage, ), + SpeedDialChild( + child: const Icon(Icons.image), + backgroundColor: Theme.of(context).colorScheme.fabBackgroundColor, + foregroundColor: Theme.of(context).colorScheme.fabForegroundColor, + labelWidget: SpeedDialLabelWidget(context.l10n.importFromGallery), + onTap: _importFromGallery, + ), ], ); } diff --git a/mobile/apps/auth/pubspec.lock b/mobile/apps/auth/pubspec.lock index 8a8b4fd6b1..631eaf0802 100644 --- a/mobile/apps/auth/pubspec.lock +++ b/mobile/apps/auth/pubspec.lock @@ -201,6 +201,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a + url: "https://pub.dev" + source: hosted + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -951,7 +959,7 @@ packages: source: hosted version: "0.1.0" image: - dependency: transitive + dependency: "direct main" description: name: image sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" @@ -2021,6 +2029,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.3" + zxing2: + dependency: "direct main" + description: + name: zxing2 + sha256: "2677c49a3b9ca9457cb1d294fd4bd5041cac6aab8cdb07b216ba4e98945c684f" + url: "https://pub.dev" + source: hosted + version: "0.2.4" sdks: dart: ">=3.7.2 <4.0.0" flutter: ">=3.29.0" diff --git a/mobile/apps/auth/pubspec.yaml b/mobile/apps/auth/pubspec.yaml index 8145cada7d..74dd6ffe01 100644 --- a/mobile/apps/auth/pubspec.yaml +++ b/mobile/apps/auth/pubspec.yaml @@ -84,6 +84,7 @@ dependencies: google_nav_bar: ^5.0.5 #supported gradient_borders: ^1.0.0 http: ^1.1.0 + image: ^4.5.4 intl: ^0.20.2 io: ^1.0.4 json_annotation: ^4.5.0 @@ -130,6 +131,7 @@ dependencies: win32: ^5.1.1 window_manager: ^0.5.0 xdg_directories: ^1.0.4 + zxing2: ^0.2.4 dev_dependencies: build_runner: ^2.1.11