diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 03b2c845..e06a3133 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -7,6 +7,9 @@
+
@@ -15,6 +18,7 @@
+
Permission.camera.request());
+
+class QRCodeScannerScreen extends StatefulHookConsumerWidget {
const QRCodeScannerScreen({super.key});
Future open(BuildContext context) async {
@@ -20,110 +23,194 @@ class QRCodeScannerScreen extends HookConsumerWidget with PresLogger {
}
@override
- Widget build(BuildContext context, WidgetRef ref) {
+ ConsumerState createState() =>
+ _QRCodeScannerScreenState();
+}
+
+class _QRCodeScannerScreenState extends ConsumerState
+ with WidgetsBindingObserver, PresLogger {
+ final controller =
+ MobileScannerController(detectionTimeoutMs: 500, autoStart: false);
+ bool started = false;
+ bool settingsOpened = false;
+
+ @override
+ void initState() {
+ super.initState();
+ WidgetsBinding.instance.addObserver(this);
+ }
+
+ @override
+ void dispose() {
+ controller.stop();
+ WidgetsBinding.instance.removeObserver(this);
+ super.dispose();
+ }
+
+ @override
+ void didChangeAppLifecycleState(AppLifecycleState state) {
+ if (state == AppLifecycleState.resumed && settingsOpened) {
+ loggy.debug("resumed");
+ ref.invalidate(_cameraPermissionProvider);
+ settingsOpened = false;
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
final t = ref.watch(translationsProvider);
- final controller = useMemoized(
- () => MobileScannerController(detectionTimeoutMs: 500),
+ ref.listen(
+ _cameraPermissionProvider,
+ (previous, next) async {
+ if (next case AsyncData(:final value)
+ when value == PermissionStatus.granted) {
+ try {
+ final result = await controller.start();
+ if (result != null) {
+ setState(() {
+ started = true;
+ });
+ }
+ } catch (error) {
+ loggy.warning("error starting scanner: $error", error);
+ }
+ }
+ },
);
- useEffect(() => controller.dispose, []);
+ switch (ref.watch(_cameraPermissionProvider)) {
+ case AsyncData(value: final status)
+ when status == PermissionStatus.granted:
+ final size = MediaQuery.sizeOf(context);
+ final overlaySize = (size.shortestSide - 12).coerceAtMost(248);
- final size = MediaQuery.sizeOf(context);
- final overlaySize = (size.shortestSide - 12).coerceAtMost(248);
-
- return Scaffold(
- extendBodyBehindAppBar: true,
- appBar: AppBar(
- backgroundColor: Colors.transparent,
- iconTheme: Theme.of(context).iconTheme.copyWith(
- color: Colors.white,
- size: 32,
- ),
- actions: [
- IconButton(
- icon: ValueListenableBuilder(
- valueListenable: controller.torchState,
- builder: (context, state, child) {
- switch (state) {
- case TorchState.off:
- return const Icon(
- FluentIcons.flash_off_24_regular,
- color: Colors.grey,
- );
- case TorchState.on:
- return const Icon(
- FluentIcons.flash_24_regular,
- color: Colors.yellow,
- );
- }
- },
- ),
- tooltip: t.profile.add.qrScanner.torchSemanticLabel,
- onPressed: () => controller.toggleTorch(),
- ),
- IconButton(
- icon: const Icon(FluentIcons.camera_switch_24_regular),
- tooltip: t.profile.add.qrScanner.facingSemanticLabel,
- onPressed: () => controller.switchCamera(),
- ),
- ],
- ),
- body: Stack(
- children: [
- MobileScanner(
- controller: controller,
- onDetect: (capture) {
- final rawData = capture.barcodes.first.rawValue;
- loggy.debug('captured raw: [$rawData]');
- if (rawData != null) {
- final uri = Uri.tryParse(rawData);
- if (context.mounted && uri != null) {
- loggy.debug('captured url: [$uri]');
- Navigator.of(context, rootNavigator: true)
- .pop(uri.toString());
- }
- } else {
- loggy.warning("unable to capture");
- }
- },
- errorBuilder: (_, error, __) {
- final message = switch (error.errorCode) {
- MobileScannerErrorCode.permissionDenied =>
- t.profile.add.qrScanner.permissionDeniedError,
- _ => t.profile.add.qrScanner.unexpectedError,
- };
-
- return Center(
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- const Padding(
- padding: EdgeInsets.only(bottom: 8),
- child: Icon(
- FluentIcons.error_circle_24_regular,
- color: Colors.white,
- ),
- ),
- Text(message),
- Text(error.errorDetails?.message ?? ''),
- ],
+ return Scaffold(
+ extendBodyBehindAppBar: true,
+ appBar: AppBar(
+ backgroundColor: Colors.transparent,
+ iconTheme: Theme.of(context).iconTheme.copyWith(
+ color: Colors.white,
+ size: 32,
),
- );
- },
- ),
- CustomPaint(
- painter: ScannerOverlay(
- Rect.fromCenter(
- center: size.center(Offset.zero),
- width: overlaySize,
- height: overlaySize,
+ actions: [
+ IconButton(
+ icon: ValueListenableBuilder(
+ valueListenable: controller.torchState,
+ builder: (context, state, child) {
+ switch (state) {
+ case TorchState.off:
+ return const Icon(
+ FluentIcons.flash_off_24_regular,
+ color: Colors.grey,
+ );
+ case TorchState.on:
+ return const Icon(
+ FluentIcons.flash_24_regular,
+ color: Colors.yellow,
+ );
+ }
+ },
+ ),
+ tooltip: t.profile.add.qrScanner.torchSemanticLabel,
+ onPressed: () => controller.toggleTorch(),
),
+ IconButton(
+ icon: const Icon(FluentIcons.camera_switch_24_regular),
+ tooltip: t.profile.add.qrScanner.facingSemanticLabel,
+ onPressed: () => controller.switchCamera(),
+ ),
+ ],
+ ),
+ body: Stack(
+ children: [
+ MobileScanner(
+ controller: controller,
+ onDetect: (capture) {
+ final rawData = capture.barcodes.first.rawValue;
+ loggy.debug('captured raw: [$rawData]');
+ if (rawData != null) {
+ final uri = Uri.tryParse(rawData);
+ if (context.mounted && uri != null) {
+ loggy.debug('captured url: [$uri]');
+ Navigator.of(context, rootNavigator: true)
+ .pop(uri.toString());
+ }
+ } else {
+ loggy.warning("unable to capture");
+ }
+ },
+ errorBuilder: (_, error, __) {
+ final message = switch (error.errorCode) {
+ MobileScannerErrorCode.permissionDenied =>
+ t.profile.add.qrScanner.permissionDeniedError,
+ _ => t.profile.add.qrScanner.unexpectedError,
+ };
+
+ return Center(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const Padding(
+ padding: EdgeInsets.only(bottom: 8),
+ child: Icon(
+ FluentIcons.error_circle_24_regular,
+ color: Colors.white,
+ ),
+ ),
+ Text(message),
+ Text(error.errorDetails?.message ?? ''),
+ ],
+ ),
+ );
+ },
+ ),
+ if (started)
+ CustomPaint(
+ painter: ScannerOverlay(
+ Rect.fromCenter(
+ center: size.center(Offset.zero),
+ width: overlaySize,
+ height: overlaySize,
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+
+ case AsyncData(value: final status):
+ return Scaffold(
+ appBar: AppBar(),
+ body: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text(t.profile.add.qrScanner.permissionDeniedError),
+ if (status == PermissionStatus.permanentlyDenied)
+ TextButton(
+ onPressed: () async {
+ settingsOpened = await openAppSettings();
+ },
+ child: Text(t.general.openAppSettings),
+ )
+ else
+ TextButton(
+ onPressed: () {
+ ref.invalidate(_cameraPermissionProvider);
+ },
+ child: Text(t.general.grantPermission),
+ ),
+ ],
),
),
- ],
- ),
- );
+ );
+
+ default:
+ return Scaffold(
+ appBar: AppBar(),
+ );
+ }
}
}
diff --git a/pubspec.lock b/pubspec.lock
index 706d4bb8..ff1ae15f 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -1102,6 +1102,54 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.2.3"
+ permission_handler:
+ dependency: "direct main"
+ description:
+ name: permission_handler
+ sha256: "74e962b7fad7ff75959161bb2c0ad8fe7f2568ee82621c9c2660b751146bfe44"
+ url: "https://pub.dev"
+ source: hosted
+ version: "11.3.0"
+ permission_handler_android:
+ dependency: transitive
+ description:
+ name: permission_handler_android
+ sha256: "1acac6bae58144b442f11e66621c062aead9c99841093c38f5bcdcc24c1c3474"
+ url: "https://pub.dev"
+ source: hosted
+ version: "12.0.5"
+ permission_handler_apple:
+ dependency: transitive
+ description:
+ name: permission_handler_apple
+ sha256: bdafc6db74253abb63907f4e357302e6bb786ab41465e8635f362ee71fd8707b
+ url: "https://pub.dev"
+ source: hosted
+ version: "9.4.0"
+ permission_handler_html:
+ dependency: transitive
+ description:
+ name: permission_handler_html
+ sha256: "54bf176b90f6eddd4ece307e2c06cf977fb3973719c35a93b85cc7093eb6070d"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.1.1"
+ permission_handler_platform_interface:
+ dependency: transitive
+ description:
+ name: permission_handler_platform_interface
+ sha256: "23dfba8447c076ab5be3dee9ceb66aad345c4a648f0cac292c77b1eb0e800b78"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.2.0"
+ permission_handler_windows:
+ dependency: transitive
+ description:
+ name: permission_handler_windows
+ sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.1"
petitparser:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index efccc5e6..ca3fa051 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -81,6 +81,7 @@ dependencies:
http: ^1.2.0
timezone_to_country: ^2.1.0
json_path: ^0.7.1
+ permission_handler: ^11.3.0
dev_dependencies:
flutter_test:
diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc
index c9dae969..47f071f8 100644
--- a/windows/flutter/generated_plugin_registrant.cc
+++ b/windows/flutter/generated_plugin_registrant.cc
@@ -6,6 +6,7 @@
#include "generated_plugin_registrant.h"
+#include
#include
#include
#include
@@ -17,6 +18,8 @@
#include
void RegisterPlugins(flutter::PluginRegistry* registry) {
+ PermissionHandlerWindowsPluginRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
ProtocolHandlerWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ProtocolHandlerWindowsPluginCApi"));
ScreenRetrieverPluginRegisterWithRegistrar(
diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake
index 9642d1c9..58127d66 100644
--- a/windows/flutter/generated_plugins.cmake
+++ b/windows/flutter/generated_plugins.cmake
@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
+ permission_handler_windows
protocol_handler_windows
screen_retriever
sentry_flutter