From 8b7a0dce0eb785a03801aba2202a4930c29813d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Zl=C3=A1mal?= Date: Tue, 19 Mar 2024 18:45:26 +0100 Subject: [PATCH 1/2] Starting to implement taking photo by Camera --- lib/components/toolbar/editor_camera.dart | 117 ++++++++++++++++++++++ lib/components/toolbar/toolbar.dart | 4 +- lib/pages/editor/editor.dart | 85 ++++++++++++++++ pubspec.yaml | 1 + 4 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 lib/components/toolbar/editor_camera.dart diff --git a/lib/components/toolbar/editor_camera.dart b/lib/components/toolbar/editor_camera.dart new file mode 100644 index 0000000000..9abbc66f54 --- /dev/null +++ b/lib/components/toolbar/editor_camera.dart @@ -0,0 +1,117 @@ +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; +import 'package:saber/i18n/strings.g.dart'; + +class TakePictureScreen extends StatefulWidget { + const TakePictureScreen({ + super.key, + required this.camera, + }); + + final CameraDescription camera; + + @override + TakePictureScreenState createState() => TakePictureScreenState(); +} + +class TakePictureScreenState extends State { + late CameraController _controller; + late Future _initializeControllerFuture; + + @override + void initState() { + super.initState(); + // To display the current output from the Camera, + // create a CameraController. + _controller = CameraController( + // Get a specific camera from the list of available cameras. + widget.camera, + // Define the resolution to use. + ResolutionPreset.medium, + ); + + // Next, initialize the controller. This returns a Future. + _initializeControllerFuture = _controller.initialize(); + } + + @override + void dispose() { + // Dispose of the controller when the widget is disposed. + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Take a picture')), + // You must wait until the controller is initialized before displaying the + // camera preview. Use a FutureBuilder to display a loading spinner until the + // controller has finished initializing. + body: FutureBuilder( + future: _initializeControllerFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + // If the Future is complete, display the preview. + return CameraPreview(_controller); + } else { + // Otherwise, display a loading indicator. + return const Center(child: CircularProgressIndicator()); + } + }, + ), + floatingActionButton: FloatingActionButton( + // Provide an onPressed callback. + onPressed: () async { + // Take the Picture in a try / catch block. If anything goes wrong, + // catch the error. + try { + // Ensure that the camera is initialized. + await _initializeControllerFuture; + + // Attempt to take a picture and get the file `image` + // where it was saved. + final image = await _controller.takePicture(); + + if (!context.mounted) return; + + // // If the picture was taken, display it on a new screen. + // await Navigator.of(context).push( + // MaterialPageRoute( + // builder: (context) => DisplayPictureScreen( + // // Pass the automatically generated path to + // // the DisplayPictureScreen widget. + // imagePath: image.path, + // ), + // ), + // ); + } catch (e) { + // If an error occurs, log the error to the console. + print(e); + } + }, + child: const Icon(Icons.camera_alt), + ), + ); + } +} + +// // A widget that displays the picture taken by the user. +// class DisplayPictureScreen extends StatelessWidget { +// final String imagePath; +// +// const DisplayPictureScreen({super.key, required this.imagePath}); +// +// @override +// Widget build(BuildContext context) { +// return Scaffold( +// appBar: AppBar(title: const Text('Display the Picture')), +// // The image is stored as a file on the device. Use the `Image.file` +// // constructor with the given path to display the image. +// body: Image.file(File(imagePath)), +// ); +// } +// } + + + diff --git a/lib/components/toolbar/toolbar.dart b/lib/components/toolbar/toolbar.dart index d7179df019..38f0b49a35 100644 --- a/lib/components/toolbar/toolbar.dart +++ b/lib/components/toolbar/toolbar.dart @@ -40,6 +40,7 @@ class Toolbar extends StatefulWidget { required this.isRedoPossible, required this.toggleFingerDrawing, required this.pickPhoto, + required this.takePhoto, required this.paste, required this.duplicateSelection, required this.deleteSelection, @@ -66,6 +67,7 @@ class Toolbar extends StatefulWidget { final VoidCallback toggleFingerDrawing; final VoidCallback pickPhoto; + final VoidCallback takePhoto; final VoidCallback paste; @@ -447,7 +449,7 @@ class _ToolbarState extends State { ToolbarIconButton( tooltip: t.editor.toolbar.photo, enabled: !widget.readOnly, - onPressed: widget.pickPhoto, + onPressed: widget.takePhoto, //pickPhoto, padding: buttonPadding, child: const AdaptiveIcon( icon: Icons.photo, diff --git a/lib/pages/editor/editor.dart b/lib/pages/editor/editor.dart index 613f30aa36..a518995bee 100644 --- a/lib/pages/editor/editor.dart +++ b/lib/pages/editor/editor.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:camera/camera.dart'; import 'package:collapsible/collapsible.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/cupertino.dart'; @@ -26,6 +27,7 @@ import 'package:saber/components/theming/adaptive_icon.dart'; import 'package:saber/components/theming/dynamic_material_app.dart'; import 'package:saber/components/toolbar/color_bar.dart'; import 'package:saber/components/toolbar/editor_bottom_sheet.dart'; +import 'package:saber/components/toolbar/editor_camera.dart'; import 'package:saber/components/toolbar/editor_page_manager.dart'; import 'package:saber/components/toolbar/toolbar.dart'; import 'package:saber/data/editor/_color_change.dart'; @@ -1052,6 +1054,86 @@ class EditorState extends State { return images.length; } + Widget takePhoto(BuildContext context, CameraDescription camera ) { + return TakePictureScreen( + camera: camera, + ); + } + + + + + Future _takePhoto([List<_PhotoInfo>? photoInfos]) async { // + if (coreInfo.readOnly) return 0; + + WidgetsFlutterBinding.ensureInitialized(); + // Obtain a list of the available cameras on the device. + final cameras = await availableCameras(); + // Get a specific camera from the list of available cameras. + final CameraDescription camera= cameras.first; + + final currentPageIndex = this.currentPageIndex; + + showDialog( + context: context, + builder: (context) => AdaptiveAlertDialog( + title: Text(t.editor.pages), + content: takePhoto(context,camera), + actions: const [], + ), + ); + + photoInfos ??= await _pickPhotosWithFilePicker(); + if (photoInfos.isEmpty) return 0; + + // use the Select tool so that the user can move the new image + currentTool = Select.currentSelect; + + List images = [ + for (final _PhotoInfo photoInfo in photoInfos) + if (photoInfo.extension == '.svg') + SvgEditorImage( + id: coreInfo.nextImageId++, + svgString: utf8.decode(photoInfo.bytes), + svgFile: null, + pageIndex: currentPageIndex, + pageSize: coreInfo.pages[currentPageIndex].size, + onMoveImage: onMoveImage, + onDeleteImage: onDeleteImage, + onMiscChange: autosaveAfterDelay, + onLoad: () => setState(() {}), + assetCache: coreInfo.assetCache, + ) + else + PngEditorImage( + id: coreInfo.nextImageId++, + extension: photoInfo.extension, + imageProvider: MemoryImage(photoInfo.bytes), + pageIndex: currentPageIndex, + pageSize: coreInfo.pages[currentPageIndex].size, + onMoveImage: onMoveImage, + onDeleteImage: onDeleteImage, + onMiscChange: autosaveAfterDelay, + onLoad: () => setState(() {}), + assetCache: coreInfo.assetCache, + ), + ]; + + history.recordChange(EditorHistoryItem( + type: EditorHistoryItemType.draw, + pageIndex: currentPageIndex, + strokes: [], + images: images, + )); + createPage(currentPageIndex); + coreInfo.pages[currentPageIndex].images.addAll(images); + autosaveAfterDelay(); + + return images.length; + } + + + Future> _pickPhotosWithFilePicker() async { final FilePickerResult? result = await FilePicker.platform.pickFiles( type: FileType.custom, @@ -1513,6 +1595,7 @@ class EditorState extends State { }); }, pickPhoto: _pickPhotos, + takePhoto: _takePhoto, paste: paste, exportAsSba: exportAsSba, exportAsPdf: exportAsPdf, @@ -1675,6 +1758,7 @@ class EditorState extends State { )); } + Widget bottomSheet(BuildContext context) { final Brightness brightness = Theme.of(context).brightness; final bool invert = @@ -1777,6 +1861,7 @@ class EditorState extends State { ); } + Widget pageManager(BuildContext context) { return EditorPageManager( coreInfo: coreInfo, diff --git a/pubspec.yaml b/pubspec.yaml index c4e125c834..52c46a0833 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -144,6 +144,7 @@ dependencies: mutex: ^3.1.0 collection: ^1.0.0 + camera: ^0.10.5+9 dev_dependencies: flutter_test: From 5e94b8f6e4dc688936c2ace78db9ba39fdcea107 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Zl=C3=A1mal?= Date: Tue, 19 Mar 2024 22:00:42 +0100 Subject: [PATCH 2/2] Added camera button to toolbar. Widget with camera should be manually closed when image is taken. --- lib/components/toolbar/editor_camera.dart | 48 ++++------ lib/components/toolbar/toolbar.dart | 12 ++- lib/i18n/strings.g.dart | 12 +++ lib/pages/editor/editor.dart | 104 ++++++++++++---------- 4 files changed, 95 insertions(+), 81 deletions(-) diff --git a/lib/components/toolbar/editor_camera.dart b/lib/components/toolbar/editor_camera.dart index 9abbc66f54..ecdc02ba28 100644 --- a/lib/components/toolbar/editor_camera.dart +++ b/lib/components/toolbar/editor_camera.dart @@ -1,14 +1,22 @@ import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; import 'package:saber/i18n/strings.g.dart'; + +/// class used to take photo by camera +/// class TakePictureScreen extends StatefulWidget { - const TakePictureScreen({ + TakePictureScreen({ super.key, - required this.camera, + required this.camera, // which camera to use + required this.onFileNameChanged, // function called with photo filename when photo is taken }); - final CameraDescription camera; + final log = Logger('Camera'); + + final CameraDescription camera; // camera + final ValueChanged onFileNameChanged; // function obtaining photo name @override TakePictureScreenState createState() => TakePictureScreenState(); @@ -44,7 +52,7 @@ class TakePictureScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('Take a picture')), + appBar: AppBar(title: Text(t.editor.camera.takePhoto)), // You must wait until the controller is initialized before displaying the // camera preview. Use a FutureBuilder to display a loading spinner until the // controller has finished initializing. @@ -74,21 +82,12 @@ class TakePictureScreenState extends State { final image = await _controller.takePicture(); if (!context.mounted) return; - - // // If the picture was taken, display it on a new screen. - // await Navigator.of(context).push( - // MaterialPageRoute( - // builder: (context) => DisplayPictureScreen( - // // Pass the automatically generated path to - // // the DisplayPictureScreen widget. - // imagePath: image.path, - // ), - // ), - // ); + widget.onFileNameChanged(image.path); // call callback with image path } catch (e) { // If an error occurs, log the error to the console. - print(e); + widget.log.warning('Error taking photo ${e.toString()}'); } + }, child: const Icon(Icons.camera_alt), ), @@ -96,22 +95,5 @@ class TakePictureScreenState extends State { } } -// // A widget that displays the picture taken by the user. -// class DisplayPictureScreen extends StatelessWidget { -// final String imagePath; -// -// const DisplayPictureScreen({super.key, required this.imagePath}); -// -// @override -// Widget build(BuildContext context) { -// return Scaffold( -// appBar: AppBar(title: const Text('Display the Picture')), -// // The image is stored as a file on the device. Use the `Image.file` -// // constructor with the given path to display the image. -// body: Image.file(File(imagePath)), -// ); -// } -// } - diff --git a/lib/components/toolbar/toolbar.dart b/lib/components/toolbar/toolbar.dart index 38f0b49a35..e04d9cb7bc 100644 --- a/lib/components/toolbar/toolbar.dart +++ b/lib/components/toolbar/toolbar.dart @@ -449,13 +449,23 @@ class _ToolbarState extends State { ToolbarIconButton( tooltip: t.editor.toolbar.photo, enabled: !widget.readOnly, - onPressed: widget.takePhoto, //pickPhoto, + onPressed: widget.pickPhoto, padding: buttonPadding, child: const AdaptiveIcon( icon: Icons.photo, cupertinoIcon: CupertinoIcons.photo, ), ), + ToolbarIconButton( + tooltip: t.editor.toolbar.camera, + enabled: !widget.readOnly, + onPressed: widget.takePhoto, + padding: buttonPadding, + child: const AdaptiveIcon( + icon: Icons.camera_alt, + cupertinoIcon: CupertinoIcons.camera, + ), + ), ToolbarIconButton( tooltip: t.editor.toolbar.text, selected: widget.textEditing, diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index 5a83a501f1..1099f7d1b7 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -288,6 +288,7 @@ class _StringsEditorEn { late final _StringsEditorToolbarEn toolbar = _StringsEditorToolbarEn._(_root); late final _StringsEditorPensEn pens = _StringsEditorPensEn._(_root); late final _StringsEditorPenOptionsEn penOptions = _StringsEditorPenOptionsEn._(_root); + late final _StringsEditorCameraEn camera = _StringsEditorCameraEn._(_root); late final _StringsEditorColorsEn colors = _StringsEditorColorsEn._(_root); late final _StringsEditorImageOptionsEn imageOptions = _StringsEditorImageOptionsEn._(_root); late final _StringsEditorSelectionBarEn selectionBar = _StringsEditorSelectionBarEn._(_root); @@ -713,6 +714,7 @@ class _StringsEditorToolbarEn { String get select => 'Select'; String get toggleEraser => 'Toggle eraser (Ctrl E)'; String get photo => 'Images'; + String get camera => 'Take photo'; String get text => 'Text'; String get toggleFingerDrawing => 'Toggle finger drawing (Ctrl F)'; String get undo => 'Undo'; @@ -747,6 +749,16 @@ class _StringsEditorPenOptionsEn { String get size => 'Size'; } +// Path: editor.campea +class _StringsEditorCameraEn { + _StringsEditorCameraEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get takePhoto => 'Take photo'; +} + // Path: editor.colors class _StringsEditorColorsEn { _StringsEditorColorsEn._(this._root); diff --git a/lib/pages/editor/editor.dart b/lib/pages/editor/editor.dart index a518995bee..ac8806c5ea 100644 --- a/lib/pages/editor/editor.dart +++ b/lib/pages/editor/editor.dart @@ -12,6 +12,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_quill/flutter_quill.dart' as flutter_quill; import 'package:keybinder/keybinder.dart'; import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; import 'package:printing/printing.dart'; import 'package:saber/components/canvas/_asset_cache.dart'; import 'package:saber/components/canvas/_stroke.dart'; @@ -1054,61 +1055,31 @@ class EditorState extends State { return images.length; } - Widget takePhoto(BuildContext context, CameraDescription camera ) { - return TakePictureScreen( - camera: camera, - ); - } - - - - - Future _takePhoto([List<_PhotoInfo>? photoInfos]) async { // - if (coreInfo.readOnly) return 0; - - WidgetsFlutterBinding.ensureInitialized(); - // Obtain a list of the available cameras on the device. - final cameras = await availableCameras(); - // Get a specific camera from the list of available cameras. - final CameraDescription camera= cameras.first; - final currentPageIndex = this.currentPageIndex; - showDialog( - context: context, - builder: (context) => AdaptiveAlertDialog( - title: Text(t.editor.pages), - content: takePhoto(context,camera), - actions: const [], - ), - ); - - photoInfos ??= await _pickPhotosWithFilePicker(); - if (photoInfos.isEmpty) return 0; +// functions taking photos + /// function called when photo is taken by camera + void parsePhotoName( + String photoName // name of photo created by camera + ) async{ // use the Select tool so that the user can move the new image currentTool = Select.currentSelect; + + final jpgFile = File(photoName); + final Uint8List jpgBytes; + try { + jpgBytes = await jpgFile.readAsBytes(); + } catch (e) { + log.severe('Failed to read file when importing $photoName: $e', e); + return; + } List images = [ - for (final _PhotoInfo photoInfo in photoInfos) - if (photoInfo.extension == '.svg') - SvgEditorImage( - id: coreInfo.nextImageId++, - svgString: utf8.decode(photoInfo.bytes), - svgFile: null, - pageIndex: currentPageIndex, - pageSize: coreInfo.pages[currentPageIndex].size, - onMoveImage: onMoveImage, - onDeleteImage: onDeleteImage, - onMiscChange: autosaveAfterDelay, - onLoad: () => setState(() {}), - assetCache: coreInfo.assetCache, - ) - else PngEditorImage( id: coreInfo.nextImageId++, - extension: photoInfo.extension, - imageProvider: MemoryImage(photoInfo.bytes), + extension: path.extension(photoName), + imageProvider: MemoryImage(jpgBytes), pageIndex: currentPageIndex, pageSize: coreInfo.pages[currentPageIndex].size, onMoveImage: onMoveImage, @@ -1128,10 +1099,49 @@ class EditorState extends State { createPage(currentPageIndex); coreInfo.pages[currentPageIndex].images.addAll(images); autosaveAfterDelay(); +// return images.length; + } - return images.length; + void _takePhoto() async { + /// take photo by camera + if (coreInfo.readOnly) return; + + WidgetsFlutterBinding.ensureInitialized(); + // Obtain a list of the available cameras on the device. + + try { + final cameras = await availableCameras(); + // Get a specific camera from the list of available cameras. + final CameraDescription camera= cameras.first; + + // show camera dialog and wait until it ends + await showDialog( + context: context, + builder: (context) { return AlertDialog( + title: Text(t.editor.camera.takePhoto), + content: takePhoto(context, + camera, + ), + ); + } + ); + return; + } catch (e) { + // If an error occurs, log the error to the console. + log.warning(e.toString()); + return; // no image taken + } } + /// widget calling camera + Widget takePhoto(BuildContext context, + CameraDescription camera, + ){ + return TakePictureScreen( + camera: camera, + onFileNameChanged: parsePhotoName, + ); + } Future> _pickPhotosWithFilePicker() async {