diff --git a/lib/pages/transaction/attach_picture.dart b/lib/pages/transaction/attach_picture.dart deleted file mode 100644 index 2fe910df..00000000 --- a/lib/pages/transaction/attach_picture.dart +++ /dev/null @@ -1,541 +0,0 @@ -import 'dart:io'; - -import 'package:async/async.dart' show RestartableTimer; -import 'package:camera/camera.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:logging/logging.dart'; - -import 'package:waterflyiii/animations.dart'; - -class CameraDialog extends StatefulWidget { - const CameraDialog({ - super.key, - required this.cameras, - }); - - final List cameras; - - @override - State createState() => _CameraDialogState(); -} - -class _CameraDialogState extends State - with WidgetsBindingObserver, TickerProviderStateMixin { - CameraController? controller; - XFile? imageFile; - final double _minAvailableZoom = 1.0; - double _maxAvailableZoom = 1.0; - double _currentScale = 1.0; - double _baseScale = 1.0; - - Offset? _focusPoint; - late AnimationController _focusPointAnimationController; - late Animation _focusPointAnimation; - final double _focusPointIconSize = 72; - late RestartableTimer _focusPointHideTimer; - - final Logger log = Logger("Pages.Transaction.AttachmentDialog.AttachPicture"); - - // Counting pointers (number of user fingers on screen) - int _pointers = 0; - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addObserver(this); - - _focusPointAnimationController = AnimationController( - duration: animDurationEmphasized, - vsync: this, - ); - _focusPointAnimation = TweenSequence( - >[ - TweenSequenceItem( - tween: Tween( - begin: _focusPointIconSize * 0.7, - end: _focusPointIconSize * 1.2) - .chain(CurveTween(curve: animCurveEmphasizedDecelerate)), - weight: 50.0, - ), - TweenSequenceItem( - tween: Tween( - begin: _focusPointIconSize * 1.2, end: _focusPointIconSize) - .chain(CurveTween(curve: animCurveEmphasizedAccelerate)), - weight: 50.0, - ), - ], - ).animate(_focusPointAnimationController); - _focusPointHideTimer = RestartableTimer( - const Duration(milliseconds: 1200), - () { - if (mounted) { - setState(() { - _focusPoint = null; - }); - } - }, - )..cancel(); - } - - @override - void dispose() { - if (controller != null) { - controller!.dispose(); - } - WidgetsBinding.instance.removeObserver(this); - _focusPointAnimationController.dispose(); - - super.dispose(); - } - - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - final CameraController? cameraController = controller; - - // App state changed before we got the chance to initialize. - if (cameraController == null || !cameraController.value.isInitialized) { - return; - } - - if (state == AppLifecycleState.inactive) { - cameraController.dispose(); - } else if (state == AppLifecycleState.resumed) { - _initializeCameraController(cameraController.description); - } - } - - void _handleScaleStart(ScaleStartDetails details) { - _baseScale = _currentScale; - } - - Future _handleScaleUpdate(ScaleUpdateDetails details) async { - // When there are not exactly two fingers on screen don't scale - if (controller == null || _pointers != 2) { - return; - } - - // Clamp zoom to 1, crashes when zoom < 1! - _currentScale = (_baseScale * details.scale) - .clamp(_minAvailableZoom, _maxAvailableZoom); - - await controller!.setZoomLevel(_currentScale); - } - - void showInSnackBar(String message) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(message), - behavior: SnackBarBehavior.floating, - )); - } - - void onViewFinderTap(TapDownDetails details, BoxConstraints constraints) { - if (controller == null) { - return; - } - - final CameraController cameraController = controller!; - - final Offset offset = Offset( - details.localPosition.dx / constraints.maxWidth, - details.localPosition.dy / constraints.maxHeight, - ); - - _focusPointAnimationController.reset(); - _focusPointAnimationController.forward(); - _focusPointHideTimer.reset(); - if (mounted) { - setState(() { - _focusPoint = details.localPosition; - }); - } - - cameraController.setExposurePoint(offset); - cameraController.setFocusPoint(offset); - } - - Future onNewCameraSelected(CameraDescription cameraDescription) async { - if (controller != null) { - return controller!.setDescription(cameraDescription); - } else { - return _initializeCameraController(cameraDescription); - } - } - - Future _initializeCameraController( - CameraDescription cameraDescription) async { - final CameraController cameraController = CameraController( - cameraDescription, - ResolutionPreset.max, - enableAudio: false, - imageFormatGroup: ImageFormatGroup.jpeg, - ); - - final S l10n = S.of(context); - final NavigatorState nav = Navigator.of(context); - - controller = cameraController; - - // If the controller is updated then update the UI. - cameraController.addListener(() { - if (mounted) { - setState(() {}); - } - if (cameraController.value.hasError) { - log.warning( - "Camera Controller Error: ${cameraController.value.errorDescription}"); - showInSnackBar( - S.of(context).cameraErrorGeneric( - cameraController.value.errorDescription ?? - S.of(context).errorUnknown), - ); - } - }); - - try { - await cameraController.initialize(); - await Future.wait(>[ - cameraController - .getMaxZoomLevel() - .then((double value) => _maxAvailableZoom = value), - /* - // Flutter crashes when minAvailableZoom < 1. So don't fetch this, - // 1 is the default! - cameraController - .getMinZoomLevel() - .then((double value) => _minAvailableZoom = value),*/ - ]); - } on CameraException catch (e) { - log.warning("Camera Error", e); - switch (e.code) { - case 'CameraAccessDenied': - showInSnackBar(l10n.cameraErrorDenied); - break; - default: - _showCameraException(e); - break; - } - nav.pop(); - } - - if (mounted) { - setState(() {}); - } - } - - void onTakePictureButtonPressed() { - takePicture().then((XFile? file) { - if (file == null) { - log.warning("picture file empty"); - return; - } - if (mounted) { - setState(() { - imageFile = file; - }); - } - }); - } - - Future takePicture() async { - final CameraController? cameraController = controller; - if (cameraController == null || !cameraController.value.isInitialized) { - return null; - } - - if (cameraController.value.isTakingPicture) { - // A capture is already pending, do nothing. - return null; - } - - try { - final XFile file = await cameraController.takePicture(); - return file; - } on CameraException catch (e) { - _showCameraException(e); - return null; - } - } - - void _showCameraException(CameraException e) { - log.severe("Camera Error", e); - showInSnackBar( - S - .of(context) - .cameraErrorGeneric(e.description ?? S.of(context).errorUnknown), - ); - } - - @override - Widget build(BuildContext context) { - if (imageFile != null) { - if (controller != null) { - controller!.dispose(); - controller = null; - } - return WillPopScope( - onWillPop: () async { - setState(() { - imageFile = null; - }); - return false; - }, - child: Scaffold( - backgroundColor: Colors.transparent, - body: CameraResult( - imageFile: imageFile!, - backCallback: () => setState(() { - imageFile = null; - }), - okCallback: () => Navigator.of(context).pop(imageFile), - ), - ), - ); - } - - if (controller == null) { - for (final CameraDescription cameraDescription in widget.cameras) { - if (cameraDescription.lensDirection == CameraLensDirection.back) { - _initializeCameraController(cameraDescription); - } - } - if (controller == null) { - _initializeCameraController(widget.cameras.first); - } - if (controller == null) { - Navigator.of(context).pop(); - return const SizedBox.shrink(); - } - } - - if (controller == null || !controller!.value.isInitialized) { - return const Center(child: CircularProgressIndicator()); - } - - return Scaffold( - backgroundColor: Colors.transparent, - body: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Listener( - onPointerDown: (_) => _pointers++, - onPointerUp: (_) => _pointers--, - child: CameraPreview( - controller!, - child: LayoutBuilder( - builder: ( - BuildContext context, - BoxConstraints constraints, - ) => - GestureDetector( - behavior: HitTestBehavior.opaque, - onScaleStart: _handleScaleStart, - onScaleUpdate: _handleScaleUpdate, - onTapDown: (TapDownDetails details) => - onViewFinderTap(details, constraints), - child: Stack( - children: [ - _focusPoint != null - ? AnimatedBuilder( - animation: _focusPointAnimationController, - builder: (BuildContext context, Widget? child) => - Positioned( - left: _focusPoint!.dx - - (_focusPointAnimation.value / 2), - top: _focusPoint!.dy - - (_focusPointAnimation.value / 2), - child: Icon( - Icons.filter_center_focus_outlined, - size: _focusPointAnimation.value, - ), - ), - ) - : const SizedBox.shrink(), - CameraControls( - controller: controller!, - cameras: widget.cameras, - newCameraFunc: onNewCameraSelected, - pictureFunc: onTakePictureButtonPressed, - ), - ], - ), - ), - ), - ), - ), - ], - ), - ); - } -} - -class CameraResult extends StatelessWidget { - const CameraResult({ - super.key, - required this.imageFile, - required this.backCallback, - required this.okCallback, - }); - - final XFile imageFile; - final void Function() backCallback; - final void Function() okCallback; - - @override - Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Stack( - children: [ - Image.file(File(imageFile.path)), - Positioned( - bottom: 16, - left: 16, - right: 16, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - IconButton.filledTonal( - icon: const Icon(Icons.close), - onPressed: () => backCallback(), - ), - IconButton.filledTonal( - icon: const Icon(Icons.check_circle), - iconSize: 72, - onPressed: () => okCallback(), - ), - const SizedBox( - // Button placeholder - width: 48, - ), - ], - )), - ], - ), - ], - ); - } -} - -class CameraControls extends StatefulWidget { - const CameraControls({ - super.key, - required this.controller, - required this.cameras, - required this.newCameraFunc, - required this.pictureFunc, - }); - - final CameraController controller; - final List cameras; - final Future Function(CameraDescription) newCameraFunc; - final void Function() pictureFunc; - - @override - State createState() => _CameraControlsState(); -} - -class _CameraControlsState extends State { - bool capturing = false; - void onSetFlashModeButtonPressed(FlashMode mode) { - setFlashMode(mode).then((_) { - if (mounted) { - setState(() {}); - } - }); - } - - Future setFlashMode(FlashMode mode) async { - widget.controller.setFlashMode(mode).then((_) { - if (mounted) { - setState(() {}); - } - }); - } - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - IconButton( - icon: const Icon(Icons.close), - color: Colors.white, - onPressed: () { - Navigator.of(context).pop(); - }, - ), - IconButton( - icon: Icon(switch (widget.controller.value.flashMode) { - FlashMode.off => Icons.flash_off, - FlashMode.auto => Icons.flash_auto, - FlashMode.always => Icons.flash_on, - FlashMode.torch => Icons.highlight, - }), - color: Colors.white, - onPressed: () { - switch (widget.controller.value.flashMode) { - case FlashMode.auto: - setFlashMode(FlashMode.off); - case FlashMode.off: - setFlashMode(FlashMode.always); - default: - setFlashMode(FlashMode.auto); - } - }, - ) - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const SizedBox( - // Button placeholder - width: 48, - ), - capturing - ? const SizedBox( - height: 72, - width: 72, - child: CircularProgressIndicator(), - ) - : IconButton( - icon: const Icon(Icons.camera), - color: Colors.white, - iconSize: 72, - onPressed: () { - setState(() { - capturing = true; - }); - widget.pictureFunc(); - }, - ), - IconButton( - icon: const Icon(Icons.cameraswitch), - color: Colors.white, - onPressed: () { - for (final CameraDescription cameraDescription - in widget.cameras) { - if (cameraDescription.lensDirection != - widget.controller.description.lensDirection) { - widget.newCameraFunc(cameraDescription); - } - } - }, - ), - ], - ), - ], - ), - ); - } -} diff --git a/lib/pages/transaction/attachments.dart b/lib/pages/transaction/attachments.dart index a84282b7..9562328b 100644 --- a/lib/pages/transaction/attachments.dart +++ b/lib/pages/transaction/attachments.dart @@ -2,9 +2,9 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:image_picker/image_picker.dart'; import 'package:intl/intl.dart'; import 'package:logging/logging.dart'; import 'package:path_provider/path_provider.dart' show getTemporaryDirectory; @@ -17,7 +17,6 @@ import 'package:open_filex/open_filex.dart'; import 'package:waterflyiii/auth.dart'; import 'package:waterflyiii/generated/swagger_fireflyiii_api/firefly_iii.swagger.dart'; -import 'package:waterflyiii/pages/transaction/attach_picture.dart'; import 'package:waterflyiii/widgets/materialiconbutton.dart'; class AttachmentDialog extends StatefulWidget { @@ -374,51 +373,26 @@ class _AttachmentDialogState extends State ), FilledButton( onPressed: () async { - final ScaffoldMessengerState msg = ScaffoldMessenger.of(context); - final S l10n = S.of(context); - final BuildContext ctx = context; - late List cameras = []; - try { - WidgetsFlutterBinding.ensureInitialized(); - cameras = await availableCameras(); - if (cameras.isEmpty) { - throw CameraException("404", "No camera found."); - } - } on CameraException catch (e) { - log.warning("Could not get camera list", e); - msg.showSnackBar(SnackBar( - content: Text( - l10n.cameraErrorInitialize( - e.description ?? l10n.errorUnknown), - ), - behavior: SnackBarBehavior.floating, - )); + final ImagePicker picker = ImagePicker(); + final XFile? imageFile = + await picker.pickImage(source: ImageSource.camera); + + if (imageFile == null) { + log.finest(() => "no image returned"); return; } + log.finer(() => "Image ${imageFile.path} will be uploaded"); + final PlatformFile file = PlatformFile( + path: imageFile.path, + name: imageFile.name, + size: await imageFile.length(), + ); if (mounted) { - final XFile? imageFile = await showDialog( - context: ctx, - builder: (BuildContext context) => - CameraDialog(cameras: cameras), - ); - if (imageFile == null) { - log.finest(() => "no image returned"); - return; - } - - log.finer(() => "Image ${imageFile.path} will be uploaded"); - final PlatformFile file = PlatformFile( - path: imageFile.path, - name: imageFile.name, - size: await imageFile.length(), - ); - if (mounted) { - if (widget.transactionId == null) { - fakeUploadAttachment(context, file); - } else { - uploadAttachment(context, file); - } + if (widget.transactionId == null) { + fakeUploadAttachment(context, file); + } else { + uploadAttachment(context, file); } } }, diff --git a/pubspec.lock b/pubspec.lock index 174bc7f8..958cc107 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -129,46 +129,6 @@ packages: url: "https://pub.dev" source: hosted version: "8.6.3" - camera: - dependency: "direct main" - description: - name: camera - sha256: "1f9010f0689774380fbcd7d6b7820a5157e8e97685fa66d619e1d1f58b3fdf93" - url: "https://pub.dev" - source: hosted - version: "0.10.5+5" - camera_android: - dependency: transitive - description: - name: camera_android - sha256: "58463140f1b39591b8e2155861b436abad4ceb48160058be8374164ff0309ef3" - url: "https://pub.dev" - source: hosted - version: "0.10.8+13" - camera_avfoundation: - dependency: transitive - description: - name: camera_avfoundation - sha256: "9495e633cda700717bbe299b0979e6c4a08cee45f298945973dc9cf3e4c1cba5" - url: "https://pub.dev" - source: hosted - version: "0.9.13+6" - camera_platform_interface: - dependency: transitive - description: - name: camera_platform_interface - sha256: "86fd4fc597c6e455265ddb5884feb352d0171ad14b9cdf3aba30da59b25738c4" - url: "https://pub.dev" - source: hosted - version: "2.6.0" - camera_web: - dependency: transitive - description: - name: camera_web - sha256: d4c2c571c7af04f8b10702ca16bb9ed2a26e64534171e8f75c9349b2c004d8f1 - url: "https://pub.dev" - source: hosted - version: "0.3.2+3" characters: dependency: transitive description: @@ -345,6 +305,38 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492" + url: "https://pub.dev" + source: hosted + version: "0.9.2+1" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: b15c3da8bd4908b9918111fa486903f5808e388b8d1c559949f584725a6594d6 + url: "https://pub.dev" + source: hosted + version: "0.9.3+3" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: "0aa47a725c346825a2bd396343ce63ac00bda6eff2fbc43eabe99737dede8262" + url: "https://pub.dev" + source: hosted + version: "2.6.1" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0 + url: "https://pub.dev" + source: hosted + version: "0.9.3+1" filesize: dependency: "direct main" description: @@ -565,6 +557,70 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.3" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "7d7f2768df2a8b0a3cefa5ef4f84636121987d403130e70b17ef7e2cf650ba84" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: d6a6e78821086b0b737009b09363018309bbc6de3fd88cc5c26bc2bb44a4957f + url: "https://pub.dev" + source: hosted + version: "0.8.8+2" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "50bc9ae6a77eea3a8b11af5eb6c661eeb858fdd2f734c2a4fd17086922347ef7" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: c5538cacefacac733c724be7484377923b476216ad1ead35a0d2eadcdc0fc497 + url: "https://pub.dev" + source: hosted + version: "0.8.8+2" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: ed9b00e63977c93b0d2d2b343685bed9c324534ba5abafbb3dfbd6a780b1b514 + url: "https://pub.dev" + source: hosted + version: "2.9.1" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" infinite_scroll_pagination: dependency: "direct main" description: @@ -926,14 +982,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" - quiver: - dependency: transitive - description: - name: quiver - sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 - url: "https://pub.dev" - source: hosted - version: "3.2.1" recase: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 24843e20..1cf20e0d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -43,10 +43,10 @@ dependencies: dynamic_color: ^1.6.8 material_color_utilities: ^0.5.0 version: ^3.0.2 - camera: ^0.10.5+2 quick_actions: ^1.0.6 stock: ^1.1.0 syncfusion_flutter_charts: ^23.1.41 + image_picker: ^1.0.4 dev_dependencies: flutter_lints: ^3.0.0