From 78640437f11200229af5f1b15f845fc5dba2801d Mon Sep 17 00:00:00 2001 From: Ian VanSchooten Date: Tue, 24 Sep 2024 07:25:09 -0400 Subject: [PATCH] Update Gradle, replace QR code reader library (#162) I ran the Gradle Upgrade Assistant to get us on the latest version of gradle, but two of our dependencies didn't support it. - https://pub.dev/documentation/package_info/latest/ - https://github.com/AmolGangadhare/flutter_barcode_scanner `pacakge_info` is officially deprecated and replaced by `package_info_plus`, which is what I've swapped to here. A bigger change was switching to https://github.com/juliansteenbakker/mobile_scanner. It does seem to work a bit better than the other one, and does not throw an error now when cancelling the QR code collection, as it did before. I've tested on android in the simulator, and iOS with an actual device. To test adding a cert: 1. Create a CA on your computer with `nebula-cert ca -name test-mobile`. This will create a `ca.crt` and `ca.key` 2. Tap the + button in the mobile app to add a site 3. Tap the "Certificate" row 4. Copy the public key to a file on your computer like `test.pub` 5. Create a signed cert with `nebula-cert sign -name test-mobile -ip 192.168.0.20/24 -in-pub test.pub` 6. Create a QR code for it: `nebula-cert print -out-qr "qr.png" -path ./test-mobile.crt` 7. In Android studio, in the "Running devices" tab, open the simulator's extended controls: Android Studio 2024-09-23 13 08 08 8. Choose the Camera option, and add the qr code image to the wall of the virtual scene image 9. Back in the app, when you choose QR Code and "Scan a QR code`, the virtual scene should open. Hold shift, then move your mouse to look around. Turn around 180 degrees, and walk forward into the other room (can go through the walls) using the `w` key. When you get the QR code into the white border, the scanner should close and apply the certificate settings. If you use a nonsense QR code, or a QR code with a non-matching key, or a CA QR code, you should get an error message when it scans. The process for scanning a CA qr code is similar. 1. Run `nebula-cert print -out-qr "qr-ca.png" -path ./ca.crt` 2. Replace the QR in the extended controls 3. Tap CA when adding a site 4. The rest of the process is the same as above. iOS is similar, except you'll need to use a real device, as the simulator does not include a virtual scene like Android does. --- android/gradle.properties | 3 + .../gradle/wrapper/gradle-wrapper.properties | 2 +- android/settings.gradle | 2 +- ios/Podfile.lock | 95 +++++++- ios/Runner.xcodeproj/project.pbxproj | 37 ++- .../siteConfig/AddCertificateScreen.dart | 28 ++- lib/screens/siteConfig/CAListScreen.dart | 38 +-- lib/screens/siteConfig/ScanQRScreen.dart | 229 ++++++++++++++++++ nebula/go.mod | 1 + nebula/go.sum | 2 + pubspec.lock | 16 +- pubspec.yaml | 2 +- 12 files changed, 401 insertions(+), 54 deletions(-) create mode 100644 lib/screens/siteConfig/ScanQRScreen.dart diff --git a/android/gradle.properties b/android/gradle.properties index 94adc3a..b9a9a24 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,3 +1,6 @@ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true +android.defaults.buildfeatures.buildconfig=true +android.nonTransitiveRClass=false +android.nonFinalResIds=false diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 3c472b9..3c85cfe 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip diff --git a/android/settings.gradle b/android/settings.gradle index 4806268..1684940 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -18,7 +18,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "7.4.2" apply false + id "com.android.application" version '8.5.2' apply false id "org.jetbrains.kotlin.android" version "1.7.20" apply false } diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 010bdfd..6865187 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -34,13 +34,64 @@ PODS: - DKImagePickerController/PhotoGallery - Flutter - Flutter (1.0.0) - - flutter_barcode_scanner (2.0.0): + - GoogleDataTransport (9.4.1): + - GoogleUtilities/Environment (~> 7.7) + - nanopb (< 2.30911.0, >= 2.30908.0) + - PromisesObjC (< 3.0, >= 1.2) + - GoogleMLKit/BarcodeScanning (6.0.0): + - GoogleMLKit/MLKitCore + - MLKitBarcodeScanning (~> 5.0.0) + - GoogleMLKit/MLKitCore (6.0.0): + - MLKitCommon (~> 11.0.0) + - GoogleToolboxForMac/Defines (4.2.1) + - GoogleToolboxForMac/Logger (4.2.1): + - GoogleToolboxForMac/Defines (= 4.2.1) + - "GoogleToolboxForMac/NSData+zlib (4.2.1)": + - GoogleToolboxForMac/Defines (= 4.2.1) + - GoogleUtilities/Environment (7.13.3): + - GoogleUtilities/Privacy + - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities/Logger (7.13.3): + - GoogleUtilities/Environment + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (7.13.3) + - GoogleUtilities/UserDefaults (7.13.3): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GoogleUtilitiesComponents (1.1.0): + - GoogleUtilities/Logger + - GTMSessionFetcher/Core (3.5.0) + - MLImage (1.0.0-beta5) + - MLKitBarcodeScanning (5.0.0): + - MLKitCommon (~> 11.0) + - MLKitVision (~> 7.0) + - MLKitCommon (11.0.0): + - GoogleDataTransport (< 10.0, >= 9.4.1) + - GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1) + - "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)" + - GoogleUtilities/UserDefaults (< 8.0, >= 7.13.0) + - GoogleUtilitiesComponents (~> 1.0) + - GTMSessionFetcher/Core (< 4.0, >= 3.3.2) + - MLKitVision (7.0.0): + - GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1) + - "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)" + - GTMSessionFetcher/Core (< 4.0, >= 3.3.2) + - MLImage (= 1.0.0-beta5) + - MLKitCommon (~> 11.0) + - mobile_scanner (5.2.2): - Flutter - - package_info (0.0.1): + - GoogleMLKit/BarcodeScanning (~> 6.0.0) + - nanopb (2.30910.0): + - nanopb/decode (= 2.30910.0) + - nanopb/encode (= 2.30910.0) + - nanopb/decode (2.30910.0) + - nanopb/encode (2.30910.0) + - package_info_plus (0.4.5): - Flutter - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - PromisesObjC (2.4.0) - SDWebImage (5.15.5): - SDWebImage/Core (= 5.15.5) - SDWebImage/Core (5.15.5) @@ -54,8 +105,8 @@ PODS: DEPENDENCIES: - file_picker (from `.symlinks/plugins/file_picker/ios`) - Flutter (from `Flutter`) - - flutter_barcode_scanner (from `.symlinks/plugins/flutter_barcode_scanner/ios`) - - package_info (from `.symlinks/plugins/package_info/ios`) + - mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`) + - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - SwiftyJSON (~> 5.0) @@ -65,6 +116,18 @@ SPEC REPOS: trunk: - DKImagePickerController - DKPhotoGallery + - GoogleDataTransport + - GoogleMLKit + - GoogleToolboxForMac + - GoogleUtilities + - GoogleUtilitiesComponents + - GTMSessionFetcher + - MLImage + - MLKitBarcodeScanning + - MLKitCommon + - MLKitVision + - nanopb + - PromisesObjC - SDWebImage - SwiftyGif - SwiftyJSON @@ -74,10 +137,10 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/file_picker/ios" Flutter: :path: Flutter - flutter_barcode_scanner: - :path: ".symlinks/plugins/flutter_barcode_scanner/ios" - package_info: - :path: ".symlinks/plugins/package_info/ios" + mobile_scanner: + :path: ".symlinks/plugins/mobile_scanner/ios" + package_info_plus: + :path: ".symlinks/plugins/package_info_plus/ios" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" share_plus: @@ -90,9 +153,21 @@ SPEC CHECKSUMS: DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_barcode_scanner: 7a1144744c28dc0c57a8de7218ffe5ec59a9e4bf - package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62 + GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a + GoogleMLKit: 97ac7af399057e99182ee8edfa8249e3226a4065 + GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8 + GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 + GoogleUtilitiesComponents: 679b2c881db3b615a2777504623df6122dd20afe + GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 + MLImage: 1824212150da33ef225fbd3dc49f184cf611046c + MLKitBarcodeScanning: 10ca0845a6d15f2f6e911f682a1998b68b973e8b + MLKitCommon: afec63980417d29ffbb4790529a1b0a2291699e1 + MLKitVision: e858c5f125ecc288e4a31127928301eaba9ae0c1 + mobile_scanner: 83ad7d6c1c04303f3277387ab9f71840070d0e9a + nanopb: 438bc412db1928dac798aa6fd75726007be04262 + package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 SDWebImage: fd7e1a22f00303e058058278639bf6196ee431fe share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index cf4a186..de698b3 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -269,6 +269,7 @@ 43AA89612444DA6500EDC39C /* Embed App Extensions */, 00C7A79AE88792090BDAC68B /* [CP] Embed Pods Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + C6F39268CE91654BF2E0E591 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -352,12 +353,17 @@ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", "${BUILT_PRODUCTS_DIR}/DKImagePickerController/DKImagePickerController.framework", "${BUILT_PRODUCTS_DIR}/DKPhotoGallery/DKPhotoGallery.framework", + "${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework", + "${BUILT_PRODUCTS_DIR}/GoogleDataTransport/GoogleDataTransport.framework", + "${BUILT_PRODUCTS_DIR}/GoogleToolboxForMac/GoogleToolboxForMac.framework", + "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", + "${BUILT_PRODUCTS_DIR}/PromisesObjC/FBLPromises.framework", "${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework", "${BUILT_PRODUCTS_DIR}/SwiftyGif/SwiftyGif.framework", "${BUILT_PRODUCTS_DIR}/SwiftyJSON/SwiftyJSON.framework", "${BUILT_PRODUCTS_DIR}/file_picker/file_picker.framework", - "${BUILT_PRODUCTS_DIR}/flutter_barcode_scanner/flutter_barcode_scanner.framework", - "${BUILT_PRODUCTS_DIR}/package_info/package_info.framework", + "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", + "${BUILT_PRODUCTS_DIR}/package_info_plus/package_info_plus.framework", "${BUILT_PRODUCTS_DIR}/path_provider_foundation/path_provider_foundation.framework", "${BUILT_PRODUCTS_DIR}/share_plus/share_plus.framework", "${BUILT_PRODUCTS_DIR}/url_launcher_ios/url_launcher_ios.framework", @@ -366,12 +372,17 @@ outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DKImagePickerController.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DKPhotoGallery.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMSessionFetcher.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleDataTransport.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleToolboxForMac.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBLPromises.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyGif.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyJSON.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_picker.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_barcode_scanner.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info_plus.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_foundation.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/share_plus.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher_ios.framework", @@ -434,6 +445,24 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; }; + C6F39268CE91654BF2E0E591 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/mobile_scanner/mobile_scanner_privacy.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/mobile_scanner_privacy.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; FF0E0EB9A684F086443A8FBA /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; diff --git a/lib/screens/siteConfig/AddCertificateScreen.dart b/lib/screens/siteConfig/AddCertificateScreen.dart index af567d3..01cfd56 100644 --- a/lib/screens/siteConfig/AddCertificateScreen.dart +++ b/lib/screens/siteConfig/AddCertificateScreen.dart @@ -1,9 +1,9 @@ +import 'dart:async'; import 'dart:convert'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_barcode_scanner/flutter_barcode_scanner.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:mobile_nebula/components/SimplePage.dart'; import 'package:mobile_nebula/components/config/ConfigButtonItem.dart'; @@ -11,6 +11,7 @@ import 'package:mobile_nebula/components/config/ConfigItem.dart'; import 'package:mobile_nebula/components/config/ConfigSection.dart'; import 'package:mobile_nebula/components/config/ConfigTextItem.dart'; import 'package:mobile_nebula/models/Certificate.dart'; +import 'package:mobile_nebula/screens/siteConfig/ScanQRScreen.dart'; import 'package:mobile_nebula/services/share.dart'; import 'package:mobile_nebula/services/utils.dart'; @@ -215,17 +216,20 @@ class _AddCertificateScreenState extends State { ConfigSection( children: [ ConfigButtonItem( - content: Text('Scan a QR code'), - onPressed: () async { - try { - var result = await FlutterBarcodeScanner.scanBarcode('#ff6666', 'Cancel', true, ScanMode.QR); - if (result != "") { - _addCertEntry(result); - } - } catch (err) { - return Utils.popError(context, 'Error scanning QR code', err.toString()); - } - }), + content: Text('Scan a QR code'), + onPressed: () async { + var result = await Navigator.push( + context, + platformPageRoute( + context: context, + builder: (context) => new ScanQRScreen(), + ), + ); + if (result != null) { + _addCertEntry(result); + } + }, + ), ], ) ]; diff --git a/lib/screens/siteConfig/CAListScreen.dart b/lib/screens/siteConfig/CAListScreen.dart index df2958f..5cd0bf6 100644 --- a/lib/screens/siteConfig/CAListScreen.dart +++ b/lib/screens/siteConfig/CAListScreen.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_barcode_scanner/flutter_barcode_scanner.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:mobile_nebula/components/FormPage.dart'; import 'package:mobile_nebula/components/config/ConfigButtonItem.dart'; import 'package:mobile_nebula/components/config/ConfigPageItem.dart'; @@ -10,6 +10,7 @@ import 'package:mobile_nebula/components/config/ConfigSection.dart'; import 'package:mobile_nebula/components/config/ConfigTextItem.dart'; import 'package:mobile_nebula/models/Certificate.dart'; import 'package:mobile_nebula/screens/siteConfig/CertificateDetailsScreen.dart'; +import 'package:mobile_nebula/screens/siteConfig/ScanQRScreen.dart'; import 'package:mobile_nebula/services/utils.dart'; //TODO: wire up the focus nodes, add a done/next/prev to the keyboard @@ -233,23 +234,26 @@ class _CAListScreenState extends State { ConfigSection( children: [ ConfigButtonItem( - content: Text('Scan a QR code'), - onPressed: () async { - try { - var result = await FlutterBarcodeScanner.scanBarcode('#ff6666', 'Cancel', true, ScanMode.QR); - if (result != "") { - _addCAEntry(result, (err) { - if (err != null) { - Utils.popError(context, 'Error loading CA content', err); - } else { - setState(() {}); - } - }); + content: Text('Scan a QR code'), + onPressed: () async { + var result = await Navigator.push( + context, + platformPageRoute( + context: context, + builder: (context) => new ScanQRScreen(), + ), + ); + if (result != null) { + _addCAEntry(result, (err) { + if (err != null) { + Utils.popError(context, 'Error loading CA content', err); + } else { + setState(() {}); } - } catch (err) { - return Utils.popError(context, 'Error scanning QR code', err.toString()); - } - }) + }); + } + }, + ) ], ) ]; diff --git a/lib/screens/siteConfig/ScanQRScreen.dart b/lib/screens/siteConfig/ScanQRScreen.dart new file mode 100644 index 0000000..78fc00f --- /dev/null +++ b/lib/screens/siteConfig/ScanQRScreen.dart @@ -0,0 +1,229 @@ +import 'package:flutter/material.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; + +class ScanQRScreen extends StatefulWidget { + @override + State createState() => _ScanQRScreenState(); +} + +class _ScanQRScreenState extends State { + final MobileScannerController cameraController = MobileScannerController( + autoStart: true, + formats: [BarcodeFormat.qrCode], + ); + + @override + Widget build(BuildContext context) { + final scanWindow = Rect.fromCenter( + center: MediaQuery.sizeOf(context).center(Offset.zero), + width: 250, + height: 250, + ); + + return Scaffold( + appBar: AppBar(title: const Text('Scan QR')), + backgroundColor: Colors.black, + body: Stack(fit: StackFit.expand, children: [ + Center( + child: MobileScanner( + fit: BoxFit.contain, + controller: cameraController, + scanWindow: scanWindow, + onDetect: (BarcodeCapture barcodes) { + var barcode = barcodes.barcodes.firstOrNull; + if (barcode != null && mounted) { + cameraController.stop().then((_) { + Navigator.pop(context, barcode.rawValue); + }); + } + }), + ), + ValueListenableBuilder( + valueListenable: cameraController, + builder: (context, value, child) { + if (!value.isInitialized || !value.isRunning || value.error != null) { + return const SizedBox(); + } + + return CustomPaint( + painter: ScannerOverlay(scanWindow: scanWindow), + ); + }, + ), + Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ToggleFlashlightButton(controller: cameraController), + SwitchCameraButton(controller: cameraController), + ], + ), + ), + ), + ])); + } +} + +class ScannerOverlay extends CustomPainter { + const ScannerOverlay({ + required this.scanWindow, + this.borderRadius = 12.0, + }); + + final Rect scanWindow; + final double borderRadius; + + @override + void paint(Canvas canvas, Size size) { + // we need to pass the size to the custom paint widget + final backgroundPath = Path()..addRect(Rect.largest); + + final cutoutPath = Path() + ..addRRect( + RRect.fromRectAndCorners( + scanWindow, + topLeft: Radius.circular(borderRadius), + topRight: Radius.circular(borderRadius), + bottomLeft: Radius.circular(borderRadius), + bottomRight: Radius.circular(borderRadius), + ), + ); + + final backgroundPaint = Paint() + ..color = Colors.black.withOpacity(0.5) + ..style = PaintingStyle.fill + ..blendMode = BlendMode.dstOut; + + final backgroundWithCutout = Path.combine( + PathOperation.difference, + backgroundPath, + cutoutPath, + ); + + final borderPaint = Paint() + ..color = Colors.white + ..style = PaintingStyle.stroke + ..strokeWidth = 4.0; + + final borderRect = RRect.fromRectAndCorners( + scanWindow, + topLeft: Radius.circular(borderRadius), + topRight: Radius.circular(borderRadius), + bottomLeft: Radius.circular(borderRadius), + bottomRight: Radius.circular(borderRadius), + ); + + // First, draw the background, + // with a cutout area that is a bit larger than the scan window. + // Finally, draw the scan window itself. + canvas.drawPath(backgroundWithCutout, backgroundPaint); + canvas.drawRRect(borderRect, borderPaint); + } + + @override + bool shouldRepaint(ScannerOverlay oldDelegate) { + return scanWindow != oldDelegate.scanWindow || borderRadius != oldDelegate.borderRadius; + } +} + +class SwitchCameraButton extends StatelessWidget { + const SwitchCameraButton({required this.controller, super.key}); + + final MobileScannerController controller; + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: controller, + builder: (context, state, child) { + if (!state.isInitialized || !state.isRunning) { + return const SizedBox.shrink(); + } + + final int? availableCameras = state.availableCameras; + + if (availableCameras != null && availableCameras < 2) { + return const SizedBox.shrink(); + } + + final Widget icon; + + switch (state.cameraDirection) { + case CameraFacing.front: + icon = const Icon(Icons.camera_front); + case CameraFacing.back: + icon = const Icon(Icons.camera_rear); + } + + return IconButton( + iconSize: 32.0, + color: Colors.white, + icon: icon, + onPressed: () async { + await controller.switchCamera(); + }, + ); + }, + ); + } +} + +class ToggleFlashlightButton extends StatelessWidget { + const ToggleFlashlightButton({required this.controller, super.key}); + + final MobileScannerController controller; + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: controller, + builder: (context, controllerState, child) { + if (!controllerState.isInitialized || !controllerState.isRunning) { + return const SizedBox.shrink(); + } + + switch (controllerState.torchState) { + case TorchState.auto: + return IconButton( + color: Colors.white, + iconSize: 32.0, + icon: const Icon(Icons.flash_auto), + onPressed: () async { + await controller.toggleTorch(); + }, + ); + case TorchState.off: + return IconButton( + color: Colors.white, + iconSize: 32.0, + icon: const Icon(Icons.flash_off), + onPressed: () async { + await controller.toggleTorch(); + }, + ); + case TorchState.on: + return IconButton( + color: Colors.white, + iconSize: 32.0, + icon: const Icon(Icons.flash_on), + onPressed: () async { + await controller.toggleTorch(); + }, + ); + case TorchState.unavailable: + return const SizedBox.square( + dimension: 48.0, + child: Icon( + Icons.no_flash, + size: 32.0, + color: Colors.grey, + ), + ); + } + }, + ); + } +} diff --git a/nebula/go.mod b/nebula/go.mod index 1415ec1..7c85ce6 100644 --- a/nebula/go.mod +++ b/nebula/go.mod @@ -34,6 +34,7 @@ require ( github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 // indirect github.com/vishvananda/netlink v1.2.1-beta.2 // indirect github.com/vishvananda/netns v0.0.4 // indirect + golang.org/x/mobile v0.0.0-20240909163608-642950227fb3 // indirect golang.org/x/mod v0.21.0 // indirect golang.org/x/net v0.29.0 // indirect golang.org/x/sync v0.8.0 // indirect diff --git a/nebula/go.sum b/nebula/go.sum index cc1a986..b2e61b9 100644 --- a/nebula/go.sum +++ b/nebula/go.sum @@ -148,6 +148,8 @@ golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20240909163608-642950227fb3 h1:HOa20LMHFElnLsGI9j8/sxTIHpogkTuHZlyoIjl3kkw= +golang.org/x/mobile v0.0.0-20240909163608-642950227fb3/go.mod h1:5EJr05J3jS1A5hwVNxs4vC0pIRxtWmwM15D1ZxCj93s= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= diff --git a/pubspec.lock b/pubspec.lock index 11f68af..606c9c8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -118,14 +118,6 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_barcode_scanner: - dependency: "direct main" - description: - name: flutter_barcode_scanner - sha256: a4ba37daf9933f451a5e812c753ddd045d6354e4a3280342d895b07fecaab3fa - url: "https://pub.dev" - source: hosted - version: "2.0.0" flutter_platform_widgets: dependency: "direct main" description: @@ -240,6 +232,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.6" + mobile_scanner: + dependency: "direct main" + description: + name: mobile_scanner + sha256: "82c9beb863705831a779e02e80398e61a86a48d1fcfdf4241ebd4292605acd9b" + url: "https://pub.dev" + source: hosted + version: "5.2.2" package_info_plus: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index d995280..3513187 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,10 +30,10 @@ dependencies: package_info_plus: ^8.0.2 url_launcher: ^6.1.6 pull_to_refresh: ^2.0.0 - flutter_barcode_scanner: ^2.0.0 flutter_svg: ^2.0.10+1 intl: ^0.19.0 share_plus: ^10.0.2 + mobile_scanner: ^5.2.2 dev_dependencies: flutter_test: