diff --git a/lib/components/toolbar/editor_camera.dart b/lib/components/toolbar/editor_camera.dart new file mode 100644 index 0000000000..ecdc02ba28 --- /dev/null +++ b/lib/components/toolbar/editor_camera.dart @@ -0,0 +1,99 @@ +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 { + TakePictureScreen({ + super.key, + required this.camera, // which camera to use + required this.onFileNameChanged, // function called with photo filename when photo is taken + }); + + final log = Logger('Camera'); + + final CameraDescription camera; // camera + final ValueChanged onFileNameChanged; // function obtaining photo name + + @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: 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. + 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; + widget.onFileNameChanged(image.path); // call callback with image path + } catch (e) { + // If an error occurs, log the error to the console. + widget.log.warning('Error taking photo ${e.toString()}'); + } + + }, + child: const Icon(Icons.camera_alt), + ), + ); + } +} + + + diff --git a/lib/components/toolbar/toolbar.dart b/lib/components/toolbar/toolbar.dart index d7179df019..e04d9cb7bc 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; @@ -454,6 +456,16 @@ class _ToolbarState extends State { 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 613f30aa36..ac8806c5ea 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'; @@ -11,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'; @@ -26,6 +28,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 +1055,95 @@ class EditorState extends State { return images.length; } + + +// 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 = [ + PngEditorImage( + id: coreInfo.nextImageId++, + extension: path.extension(photoName), + imageProvider: MemoryImage(jpgBytes), + 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; + } + + 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 { final FilePickerResult? result = await FilePicker.platform.pickFiles( type: FileType.custom, @@ -1513,6 +1605,7 @@ class EditorState extends State { }); }, pickPhoto: _pickPhotos, + takePhoto: _takePhoto, paste: paste, exportAsSba: exportAsSba, exportAsPdf: exportAsPdf, @@ -1675,6 +1768,7 @@ class EditorState extends State { )); } + Widget bottomSheet(BuildContext context) { final Brightness brightness = Theme.of(context).brightness; final bool invert = @@ -1777,6 +1871,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: