diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2dc7cf59..3f6f3bb5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -149,13 +149,13 @@ jobs: [IO.File]::WriteAllBytes("windows\sign.pfx", [Convert]::FromBase64String("${{ secrets.WINDOWS_SIGNING_KEY }}")) (Get-Content "windows\packaging\msix\make_config.yaml") -replace '^certificate_password:.*$', 'certificate_password: ${{ secrets.WINDOWS_SIGNING_PASSWORD }}' | Set-Content "windows\packaging\msix\make_config.yaml" - - name: Temporary disable Permission Handler for windows due to its issue in permission - if: ${{ startsWith(matrix.platform,'windows') }} - run: | - (Get-Content -Path "pubspec.yaml") -notmatch "permission_handler" | Set-Content -Path "pubspec.yaml" - (Get-Content -Path "lib\features\profile\add\add_profile_modal.dart") -notmatch "qr_code_scanner_screen" | Set-Content -Path "lib\features\profile\add\add_profile_modal.dart" - (Get-Content -Path lib\features\profile\add\add_profile_modal.dart) -replace 'await QRCodeScannerScreen\(\).open\(context\);', 'null;' | Set-Content -Path lib\features\profile\add\add_profile_modal.dart - Remove-Item -Path "lib\features\common\qr_code_scanner_screen.dart" + # - name: Temporary disable Permission Handler for windows due to its issue in permission + # if: ${{ startsWith(matrix.platform,'windows') }} + # run: | + # (Get-Content -Path "pubspec.yaml") -notmatch "permission_handler" | Set-Content -Path "pubspec.yaml" + # (Get-Content -Path "lib\features\profile\add\add_profile_modal.dart") -notmatch "qr_code_scanner_screen" | Set-Content -Path "lib\features\profile\add\add_profile_modal.dart" + # (Get-Content -Path lib\features\profile\add\add_profile_modal.dart) -replace 'await QRCodeScannerScreen\(\).open\(context\);', 'null;' | Set-Content -Path lib\features\profile\add\add_profile_modal.dart + # Remove-Item -Path "lib\features\common\qr_code_scanner_screen.dart" diff --git a/assets/translations/strings_en.i18n.json b/assets/translations/strings_en.i18n.json index b09df18c..5ead6e5b 100644 --- a/assets/translations/strings_en.i18n.json +++ b/assets/translations/strings_en.i18n.json @@ -74,7 +74,8 @@ "permissionDeniedError": "Permission Denied", "unexpectedError": "Something Went Wrong", "torchSemanticLabel": "Flash Light", - "facingSemanticLabel": "Camera Facing" + "facingSemanticLabel": "Camera Facing", + "permissionRequest":"Permission to camera to scan QR Code" }, "manually": "Manual Entry", "addingProfileMsg": "Adding Profile", diff --git a/ios/Podfile b/ios/Podfile index c461cbd0..8e07cf3d 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -32,6 +32,8 @@ target 'Runner' do use_modular_headers! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + pod 'EasyPermissionX/Camera' + target 'RunnerTests' do inherit! :search_paths end diff --git a/lib/features/common/qr_code_scanner_screen.dart b/lib/features/common/qr_code_scanner_screen.dart index d534279f..cfb3f679 100644 --- a/lib/features/common/qr_code_scanner_screen.dart +++ b/lib/features/common/qr_code_scanner_screen.dart @@ -1,14 +1,15 @@ import 'package:dartx/dartx.dart'; import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_easy_permission/easy_permissions.dart'; import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; -import 'package:permission_handler/permission_handler.dart'; +// import 'package:permission_handler/permission_handler.dart'; -final _cameraPermissionProvider = - FutureProvider.autoDispose((ref) => Permission.camera.request()); +const permissions = [Permissions.CAMERA]; +const permissionGroup = [PermissionGroup.Camera]; class QRCodeScannerScreen extends StatefulHookConsumerWidget { const QRCodeScannerScreen({super.key}); @@ -32,185 +33,157 @@ class _QRCodeScannerScreenState extends ConsumerState final controller = MobileScannerController(detectionTimeoutMs: 500, autoStart: false); bool started = false; - bool settingsOpened = false; + late FlutterEasyPermission _easyPermission; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); + + _easyPermission = FlutterEasyPermission() + ..addPermissionCallback(onGranted: (requestCode, androidPerms, iosPerm) { + debugPrint("android:$androidPerms"); + debugPrint("iOS:$iosPerm"); + startQrScannerIfPermissionGranted(); + }, onDenied: (requestCode, androidPerms, iosPerm, isPermanent) { + if (isPermanent) { + FlutterEasyPermission.showAppSettingsDialog(title: "Camera"); + } else { + debugPrint("android:$androidPerms"); + debugPrint("iOS:$iosPerm"); + } + }, onSettingsReturned: () { + startQrScannerIfPermissionGranted(); + }); } @override void dispose() { controller.stop(); + _easyPermission.dispose(); WidgetsBinding.instance.removeObserver(this); super.dispose(); } - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - if (state == AppLifecycleState.resumed && settingsOpened) { - loggy.debug("resumed"); - ref.invalidate(_cameraPermissionProvider); - settingsOpened = false; - } + void startQrScannerIfPermissionGranted() { + FlutterEasyPermission.has(perms: permissions, permsGroup: permissionGroup) + .then((value) { + if (value) { + controller.start().then((result) { + if (result != null) { + setState(() { + started = true; + }); + } + }).catchError((error) { + loggy.warning("Error starting scanner: $error"); + }); + } else {} + }); } @override Widget build(BuildContext context) { final t = ref.watch(translationsProvider); - 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); - } - } - }, - ); + startQrScannerIfPermissionGranted(); - 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 ?? ''), - ], - ), - ); - }, - ), - 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), - ), - ], + 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, + }; - default: - return Scaffold( - appBar: AppBar(), - ); - } + 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, + ), + ), + ), + ], + ), + ); } } diff --git a/pubspec.lock b/pubspec.lock index fe3121a1..ba8bc660 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -503,6 +503,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.0" + flutter_easy_permission: + dependency: "direct main" + description: + name: flutter_easy_permission + sha256: "05eb1b561c894adef28b3ae38d8087fc2635f1047c5e18cf2698fb42b6ccc132" + url: "https://pub.dev" + source: hosted + version: "1.1.2" flutter_gen_core: dependency: transitive description: @@ -1134,54 +1142,6 @@ 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 5d139c70..8182c8d7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -76,7 +76,8 @@ dependencies: http: ^1.2.0 timezone_to_country: ^2.1.0 json_path: ^0.7.1 - permission_handler: ^11.3.0 + # permission_handler: ^11.3.0 # is not compatible with windows + flutter_easy_permission: ^1.1.2 # circle_flags: ^4.0.2 circle_flags: git: https://github.com/hiddify-com/flutter_circle_flags.git diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 47f071f8..c9dae969 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,7 +6,6 @@ #include "generated_plugin_registrant.h" -#include #include #include #include @@ -18,8 +17,6 @@ #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 58127d66..9642d1c9 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,7 +3,6 @@ # list(APPEND FLUTTER_PLUGIN_LIST - permission_handler_windows protocol_handler_windows screen_retriever sentry_flutter