diff --git a/assets/fonts/Lato-Regular.ttf b/assets/fonts/Lato-Regular.ttf new file mode 100644 index 00000000..bb2e8875 Binary files /dev/null and b/assets/fonts/Lato-Regular.ttf differ diff --git a/assets/fonts/Montserrat-Regular.ttf b/assets/fonts/Montserrat-Regular.ttf new file mode 100644 index 00000000..48ba65ed Binary files /dev/null and b/assets/fonts/Montserrat-Regular.ttf differ diff --git a/assets/fonts/OpenSans-Regular.ttf b/assets/fonts/OpenSans-Regular.ttf new file mode 100644 index 00000000..67803bb6 Binary files /dev/null and b/assets/fonts/OpenSans-Regular.ttf differ diff --git a/assets/fonts/Oswald-Regular.ttf b/assets/fonts/Oswald-Regular.ttf new file mode 100644 index 00000000..5903df43 Binary files /dev/null and b/assets/fonts/Oswald-Regular.ttf differ diff --git a/assets/fonts/Roboto_Condensed-Regular.ttf b/assets/fonts/Roboto_Condensed-Regular.ttf new file mode 100644 index 00000000..5af42d47 Binary files /dev/null and b/assets/fonts/Roboto_Condensed-Regular.ttf differ diff --git a/lib/draw_canvas/Dialogs/add_text_overlay_dialog.dart b/lib/draw_canvas/Dialogs/add_text_overlay_dialog.dart new file mode 100644 index 00000000..81567d4e --- /dev/null +++ b/lib/draw_canvas/Dialogs/add_text_overlay_dialog.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:magic_epaper_app/draw_canvas/models/overlay_item.dart'; + +Widget buildTextOverlayDialog({ + required BuildContext context, + required Color selectedColor, + required void Function(OverlayItem) onItemCreated, +}) { + TextEditingController controller = TextEditingController(); + double selectedFontSize = 24; + String selectedFont = 'Roboto'; + List fontOptions = [ + 'Roboto', + 'Open Sans', + 'Lato', + 'Montserrat', + 'Oswald' + ]; + + return StatefulBuilder( + builder: (context, setDialogState) => AlertDialog( + title: Text("Enter Text"), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField(controller: controller), + const SizedBox(height: 10), + DropdownButton( + value: selectedFont, + onChanged: (value) => setDialogState(() => selectedFont = value!), + items: fontOptions.map((font) { + return DropdownMenuItem( + value: font, + child: Text(font, style: TextStyle(fontFamily: font)), + ); + }).toList(), + ), + const SizedBox(height: 10), + Row( + children: [ + Text("Font size: ${selectedFontSize.toInt()}"), + Expanded( + child: Slider( + value: selectedFontSize, + min: 12, + max: 48, + divisions: 12, + onChanged: (value) => + setDialogState(() => selectedFontSize = value), + ), + ), + ], + ), + ], + ), + actions: [ + TextButton( + onPressed: () { + final text = controller.text; + Navigator.pop(context); + if (text.isNotEmpty) { + onItemCreated( + OverlayItem.text( + text: text, + color: selectedColor, + font: selectedFont, + fontSize: selectedFontSize, + ), + ); + } + }, + child: Text("Add"), + ), + ], + ), + ); +} diff --git a/lib/draw_canvas/Dialogs/build_image_preview_dialog.dart b/lib/draw_canvas/Dialogs/build_image_preview_dialog.dart new file mode 100644 index 00000000..efbef08c --- /dev/null +++ b/lib/draw_canvas/Dialogs/build_image_preview_dialog.dart @@ -0,0 +1,26 @@ +import 'dart:typed_data'; +import 'package:flutter/material.dart'; + +Widget buildImagePreviewDialog({ + required BuildContext context, + required Uint8List image, + required VoidCallback onSubmit, +}) { + return AlertDialog( + title: const Text("Preview Captured Image"), + content: SingleChildScrollView(child: Image.memory(image)), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text("Close"), + ), + TextButton( + onPressed: () { + Navigator.pop(context); + onSubmit(); + }, + child: const Text("Submit"), + ), + ], + ); +} diff --git a/lib/draw_canvas/Dialogs/pick_color_dialog.dart b/lib/draw_canvas/Dialogs/pick_color_dialog.dart new file mode 100644 index 00000000..f44ea4eb --- /dev/null +++ b/lib/draw_canvas/Dialogs/pick_color_dialog.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_colorpicker/flutter_colorpicker.dart'; + +Widget buildColorPickerDialog(BuildContext context, Color selectedColor, + ValueChanged onColorPicked) { + return AlertDialog( + title: const Text("Pick a color"), + content: BlockPicker( + availableColors: [Colors.black, Colors.white, Colors.red], + pickerColor: selectedColor, + onColorChanged: (color) { + onColorPicked(color); + Navigator.of(context).pop(); + }, + ), + ); +} diff --git a/lib/draw_canvas/Dialogs/show_layer_manager_dialog.dart b/lib/draw_canvas/Dialogs/show_layer_manager_dialog.dart new file mode 100644 index 00000000..f1d339c9 --- /dev/null +++ b/lib/draw_canvas/Dialogs/show_layer_manager_dialog.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:magic_epaper_app/draw_canvas/models/overlay_item.dart'; + +Widget buildLayerManagerDialog({ + required List items, + required void Function(int oldIndex, int newIndex) onReorder, + required void Function(void Function()) setModalState, +}) { + return ReorderableListView( + onReorder: (oldIndex, newIndex) { + onReorder(oldIndex, newIndex); + setModalState(() {}); + }, + children: [ + for (int i = 0; i < items.length; i++) + ListTile( + key: ValueKey(items[i].id), + title: Text( + items[i].type == 'text' + ? items[i].text ?? 'Text Layer' + : items[i].label ?? 'Image Layer', + ), + leading: Icon( + items[i].type == 'text' ? Icons.text_fields : Icons.image, + ), + trailing: const Icon(Icons.drag_handle), + ), + ], + ); +} diff --git a/lib/draw_canvas/ImageAdjust/image_adjust_parms.dart b/lib/draw_canvas/ImageAdjust/image_adjust_parms.dart new file mode 100644 index 00000000..ca0e011c --- /dev/null +++ b/lib/draw_canvas/ImageAdjust/image_adjust_parms.dart @@ -0,0 +1,9 @@ +import 'package:image/image.dart' as img; + +class ImageAdjustParams { + final img.Image image; + final double brightness; + final double contrast; + + ImageAdjustParams(this.image, this.brightness, this.contrast); +} diff --git a/lib/draw_canvas/ImageAdjust/process_image.dart b/lib/draw_canvas/ImageAdjust/process_image.dart new file mode 100644 index 00000000..e6d3561a --- /dev/null +++ b/lib/draw_canvas/ImageAdjust/process_image.dart @@ -0,0 +1,18 @@ +import 'dart:typed_data'; +import 'package:image/image.dart' as img; +import 'package:flutter/foundation.dart'; +import 'package:magic_epaper_app/draw_canvas/ImageAdjust/image_adjust_parms.dart'; + +Uint8List processImage(ImageAdjustParams params) { + final img.Image adjusted = img.adjustColor( + params.image.clone(), + brightness: params.brightness, + contrast: params.contrast, + ); + return Uint8List.fromList(img.encodePng(adjusted)); +} + +img.Image decodeImage(Uint8List bytes) { + final original = img.decodeImage(bytes)!; + return img.copyResize(original, width: 512); +} diff --git a/lib/draw_canvas/helper_functions/helper_functions.dart b/lib/draw_canvas/helper_functions/helper_functions.dart new file mode 100644 index 00000000..ffb258e8 --- /dev/null +++ b/lib/draw_canvas/helper_functions/helper_functions.dart @@ -0,0 +1,135 @@ +import 'dart:typed_data'; +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:magic_epaper_app/draw_canvas/Dialogs/add_text_overlay_dialog.dart'; +import 'package:magic_epaper_app/draw_canvas/Dialogs/build_image_preview_dialog.dart'; +import 'package:magic_epaper_app/draw_canvas/Dialogs/pick_color_dialog.dart'; +import 'package:magic_epaper_app/draw_canvas/Dialogs/show_layer_manager_dialog.dart'; +import 'package:magic_epaper_app/draw_canvas/models/overlay_item.dart'; +import 'package:magic_epaper_app/draw_canvas/view/image_adjust_page.dart'; +import 'package:magic_epaper_app/util/epd/epd.dart'; +import 'package:screenshot/screenshot.dart'; +import 'dart:io'; +import 'package:path_provider/path_provider.dart'; +import 'package:image_cropper/image_cropper.dart'; + +Future pickColorDialog(BuildContext context, Color selectedColor, + ValueChanged onColorPicked) async { + showDialog( + context: context, + builder: (_) => + buildColorPickerDialog(context, selectedColor, onColorPicked), + ); +} + +void addTextOverlayDialog({ + required BuildContext context, + required Color selectedColor, + required void Function(OverlayItem) onItemCreated, +}) { + showDialog( + context: context, + builder: (_) => buildTextOverlayDialog( + context: context, + selectedColor: selectedColor, + onItemCreated: onItemCreated, + ), + ); +} + +void showLayerManagerModal({ + required BuildContext context, + required List items, + required void Function(int oldIndex, int newIndex) onReorder, +}) { + showModalBottomSheet( + context: context, + builder: (_) { + return StatefulBuilder( + builder: (context, setModalState) { + return buildLayerManagerDialog( + items: items, + onReorder: onReorder, + setModalState: setModalState, + ); + }, + ); + }, + ); +} + +Future pickImageFromGallery({ + required void Function(OverlayItem) onImagePicked, +}) async { + final ImagePicker picker = ImagePicker(); + final XFile? image = await picker.pickImage(source: ImageSource.gallery); + + if (image != null) { + final bytes = await image.readAsBytes(); + final name = image.name; + + onImagePicked( + OverlayItem.image( + imageBytes: bytes, + label: name, + ), + ); + } +} + +Future captureAndProcessImage({ + required BuildContext context, + required ScreenshotController controller, + required Epd epd, + required void Function(Uint8List adjustedBytes) onImageExported, + required void Function() onCaptureStart, + required void Function() onCaptureEnd, +}) async { + onCaptureStart(); + + await Future.delayed(const Duration(milliseconds: 100)); + final image = await controller.capture(); + + onCaptureEnd(); + + if (image == null) { + ScaffoldMessenger.of(context) + .showSnackBar(const SnackBar(content: Text("Failed to capture image"))); + return; + } + + showDialog( + context: context, + builder: (_) => buildImagePreviewDialog( + context: context, + image: image, + onSubmit: () async { + final tempDir = await getTemporaryDirectory(); + final tempPath = '${tempDir.path}/captured_image.png'; + final file = await File(tempPath).writeAsBytes(image); + + final croppedFile = await ImageCropper().cropImage( + sourcePath: file.path, + aspectRatio: CropAspectRatio( + ratioX: epd.width.toDouble(), + ratioY: epd.height.toDouble(), + ), + ); + + if (croppedFile != null) { + final croppedBytes = await File(croppedFile.path).readAsBytes(); + final adjustedBytes = await Navigator.push( + context, + MaterialPageRoute( + builder: (_) => ImageAdjustScreen(imageBytes: croppedBytes), + ), + ); + + if (adjustedBytes != null) { + onImageExported(adjustedBytes); + } + } + }, + ), + ); +} diff --git a/lib/draw_canvas/models/draw_line.dart b/lib/draw_canvas/models/draw_line.dart new file mode 100644 index 00000000..7d21f959 --- /dev/null +++ b/lib/draw_canvas/models/draw_line.dart @@ -0,0 +1,8 @@ +import 'package:flutter/material.dart'; + +class DrawnLine { + List path; + Color color; + double width; + DrawnLine(this.path, this.color, this.width); +} diff --git a/lib/draw_canvas/models/overlay_item.dart b/lib/draw_canvas/models/overlay_item.dart new file mode 100644 index 00000000..78827e8b --- /dev/null +++ b/lib/draw_canvas/models/overlay_item.dart @@ -0,0 +1,44 @@ +import 'dart:typed_data'; +import 'package:flutter/material.dart'; + +enum OverlayType { text, image } + +class OverlayItem { + final String id; + final String type; + final String? text; + final Uint8List? imageBytes; + final Color? color; + final String font; + final double fontSize; + final String? label; + Offset position; + double scale; + double rotation; + + OverlayItem.text({ + required this.text, + required this.color, + this.font = 'Roboto', + this.fontSize = 24.0, + this.label, + this.position = const Offset(100, 100), + this.scale = 1.0, + this.rotation = 0.0, + }) : id = UniqueKey().toString(), + type = 'text', + imageBytes = null; + + OverlayItem.image({ + required this.imageBytes, + this.font = 'Roboto', + this.fontSize = 24.0, + this.label, + this.position = const Offset(100, 100), + this.scale = 1.0, + this.rotation = 0.0, + }) : id = UniqueKey().toString(), + type = 'image', + text = null, + color = null; +} diff --git a/lib/draw_canvas/models/sketcher.dart b/lib/draw_canvas/models/sketcher.dart new file mode 100644 index 00000000..a58778ba --- /dev/null +++ b/lib/draw_canvas/models/sketcher.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:magic_epaper_app/draw_canvas/models/draw_line.dart'; + +class Sketcher extends CustomPainter { + final List lines; + Sketcher(this.lines); + + @override + void paint(Canvas canvas, Size size) { + for (var line in lines) { + final paint = Paint() + ..color = line.color + ..strokeWidth = line.width + ..strokeCap = StrokeCap.round + ..style = PaintingStyle.stroke; + + for (int i = 0; i < line.path.length - 1; i++) { + canvas.drawLine(line.path[i], line.path[i + 1], paint); + } + } + } + + @override + bool shouldRepaint(Sketcher oldDelegate) => true; +} diff --git a/lib/draw_canvas/overlays/image_overlays.dart b/lib/draw_canvas/overlays/image_overlays.dart new file mode 100644 index 00000000..f571338f --- /dev/null +++ b/lib/draw_canvas/overlays/image_overlays.dart @@ -0,0 +1,142 @@ +import 'dart:typed_data'; +import 'package:flutter/material.dart'; +import 'package:magic_epaper_app/draw_canvas/models/overlay_item.dart'; + +class DraggableResizableImage extends StatefulWidget { + final Uint8List imageBytes; + final OverlayItem overlayItem; + final bool isCapturing; + final VoidCallback onDelete; + + const DraggableResizableImage({ + super.key, + required this.imageBytes, + required this.overlayItem, + required this.isCapturing, + required this.onDelete, + }); + + @override + State createState() => + _DraggableResizableImageState(); +} + +class _DraggableResizableImageState extends State { + late Offset _position; + late double _scale; + late double _rotation; + double _previousScale = 1.0; + + double _previousRotation = 0.0; + + Offset _initialFocalPoint = Offset.zero; + Offset _dragOffset = Offset.zero; + + bool _locked = false; + + @override + void initState() { + super.initState(); + _position = widget.overlayItem.position; + _scale = widget.overlayItem.scale; + _rotation = widget.overlayItem.rotation; + } + + void _updateStateFromGesture() { + widget.overlayItem.position = _position; + widget.overlayItem.scale = _scale; + widget.overlayItem.rotation = _rotation; + } + + @override + Widget build(BuildContext context) { + return Positioned( + left: _position.dx, + top: _position.dy, + child: Stack( + alignment: Alignment.topRight, + children: [ + GestureDetector( + onLongPress: () { + if (!widget.isCapturing) { + showDialog( + context: context, + builder: (_) => AlertDialog( + title: Text("Delete?"), + content: Text("Do you want to delete this overlay?"), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text("Cancel"), + ), + TextButton( + onPressed: () { + Navigator.pop(context); + widget.onDelete(); + }, + child: + Text("Delete", style: TextStyle(color: Colors.red)), + ), + ], + ), + ); + } + }, + onScaleStart: _locked + ? null + : (details) { + _previousScale = _scale; + _previousRotation = _rotation; + _initialFocalPoint = details.focalPoint; + _dragOffset = _position; + }, + onScaleUpdate: _locked + ? null + : (details) { + setState(() { + final delta = details.focalPoint - _initialFocalPoint; + _position = _dragOffset + delta; + _scale = _previousScale * details.scale; + _rotation = _previousRotation + details.rotation; + _updateStateFromGesture(); + }); + }, + child: Transform( + alignment: Alignment.center, + transform: Matrix4.identity()..rotateZ(_rotation), + child: Container( + color: Colors.red, + height: 100 * _scale, + child: Image.memory( + widget.imageBytes, + height: 100 * _scale, + fit: BoxFit.contain, + ), + ), + ), + ), + if (!widget.isCapturing) + GestureDetector( + onTap: () { + setState(() { + _locked = !_locked; + }); + }, + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.5), + shape: BoxShape.circle, + ), + child: Icon( + _locked ? Icons.lock : Icons.lock_open, + color: Colors.white, + size: 20, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/draw_canvas/overlays/text_overlays.dart b/lib/draw_canvas/overlays/text_overlays.dart new file mode 100644 index 00000000..18c37e8e --- /dev/null +++ b/lib/draw_canvas/overlays/text_overlays.dart @@ -0,0 +1,152 @@ +import 'package:flutter/material.dart'; +import 'package:magic_epaper_app/draw_canvas/models/overlay_item.dart'; + +class DraggableResizableText extends StatefulWidget { + final String text; + final Color color; + final bool isCapturing; + final VoidCallback onDelete; + final String font; + final double fontSize; + final OverlayItem overlayItem; + + const DraggableResizableText({ + super.key, + required this.text, + required this.overlayItem, + required this.color, + required this.isCapturing, + required this.onDelete, + this.font = 'Roboto', + this.fontSize = 24.0, + }); + + @override + State createState() => _DraggableResizableTextState(); +} + +class _DraggableResizableTextState extends State { + late Offset _position; + late double _scale; + late double _rotation; + double _previousScale = 1.0; + + double _previousRotation = 0.0; + + Offset _initialFocalPoint = Offset.zero; + Offset _dragOffset = Offset.zero; + + bool _locked = false; + + @override + void initState() { + super.initState(); + _position = widget.overlayItem.position; + _scale = widget.overlayItem.scale; + _rotation = widget.overlayItem.rotation; + } + + void _updateStateFromGesture() { + widget.overlayItem.position = _position; + widget.overlayItem.scale = _scale; + widget.overlayItem.rotation = _rotation; + } + + @override + Widget build(BuildContext context) { + return Positioned( + left: _position.dx, + top: _position.dy, + child: Stack( + alignment: Alignment.topRight, + children: [ + GestureDetector( + onLongPress: () { + if (!widget.isCapturing) { + showDialog( + context: context, + builder: (_) => AlertDialog( + title: Text("Delete?"), + content: Text("Do you want to delete this overlay?"), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text("Cancel"), + ), + TextButton( + onPressed: () { + Navigator.pop(context); + widget.onDelete(); + }, + child: + Text("Delete", style: TextStyle(color: Colors.red)), + ), + ], + ), + ); + } + }, + onScaleStart: _locked + ? null + : (details) { + _previousScale = _scale; + _previousRotation = _rotation; + _initialFocalPoint = details.focalPoint; + _dragOffset = _position; + }, + onScaleUpdate: _locked + ? null + : (details) { + setState(() { + final delta = details.focalPoint - _initialFocalPoint; + _position = _dragOffset + delta; + _scale = _previousScale * details.scale; + _rotation = _previousRotation + details.rotation; + _updateStateFromGesture(); + }); + }, + child: Container( + color: Colors.transparent, + height: widget.fontSize * _scale * 4, + child: Center( + child: Transform( + alignment: Alignment.center, + transform: Matrix4.identity()..rotateZ(_rotation), + child: Material( + color: Colors.transparent, + child: Text(widget.text, + style: TextStyle( + fontFamily: widget.font, + fontSize: widget.fontSize * _scale, + color: widget.color, + )), + ), + ), + ), + ), + ), + if (!widget.isCapturing) + GestureDetector( + onTap: () { + setState(() { + _locked = !_locked; + }); + }, + child: Container( + padding: EdgeInsets.all(4), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.5), + shape: BoxShape.circle, + ), + child: Icon( + _locked ? Icons.lock : Icons.lock_open, + color: Colors.white, + size: 20, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/draw_canvas/view/drawing_page.dart b/lib/draw_canvas/view/drawing_page.dart new file mode 100644 index 00000000..2963c779 --- /dev/null +++ b/lib/draw_canvas/view/drawing_page.dart @@ -0,0 +1,164 @@ +import 'package:flutter/material.dart'; +import 'package:magic_epaper_app/draw_canvas/helper_functions/helper_functions.dart'; +import 'package:magic_epaper_app/draw_canvas/models/draw_line.dart'; +import 'package:magic_epaper_app/draw_canvas/models/overlay_item.dart'; +import 'package:magic_epaper_app/draw_canvas/models/sketcher.dart'; +import 'package:magic_epaper_app/draw_canvas/overlays/image_overlays.dart'; +import 'package:magic_epaper_app/draw_canvas/overlays/text_overlays.dart'; +import 'package:magic_epaper_app/util/epd/epd.dart'; +import 'package:screenshot/screenshot.dart'; + +class DrawingPage extends StatefulWidget { + final Epd epd; + + const DrawingPage({Key? key, required this.epd}) : super(key: key); + + @override + _DrawingPageState createState() => _DrawingPageState(); +} + +class _DrawingPageState extends State { + List lines = []; + Color selectedColor = Colors.black; + double strokeWidth = 4.0; + bool isCapturing = false; + List overlayItems = []; + final ScreenshotController screenshotController = ScreenshotController(); + + void startDrawing(Offset position) { + setState(() { + lines.add(DrawnLine([position], selectedColor, strokeWidth)); + }); + } + + void drawUpdate(Offset position) { + setState(() { + lines.last.path.add(position); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: const Text( + "Magic ePaper Editor", + style: TextStyle( + fontSize: 17, + fontWeight: FontWeight.bold, + ), + ), + actions: [ + IconButton( + icon: const Icon(Icons.image), + onPressed: () { + pickImageFromGallery(onImagePicked: (item) { + setState(() { + overlayItems.add(item); + }); + }); + }, + ), + IconButton( + icon: const Icon(Icons.text_fields), + onPressed: () { + addTextOverlayDialog( + context: context, + selectedColor: selectedColor, + onItemCreated: (item) { + setState(() { + overlayItems.add(item); + }); + }, + ); + }, + ), + IconButton( + icon: const Icon(Icons.color_lens), + onPressed: () { + pickColorDialog(context, selectedColor, (color) { + setState(() => selectedColor = color); + }); + }, + ), + IconButton( + icon: const Icon(Icons.download), + onPressed: () { + captureAndProcessImage( + context: context, + controller: screenshotController, + epd: widget.epd, + onCaptureStart: () => setState(() => isCapturing = true), + onCaptureEnd: () => setState(() => isCapturing = false), + onImageExported: (adjustedBytes) { + Navigator.pop(context, adjustedBytes); + }, + ); + }, + ), + IconButton( + icon: const Icon(Icons.layers), + onPressed: () { + showLayerManagerModal( + context: context, + items: overlayItems, + onReorder: (oldIndex, newIndex) { + setState(() { + if (newIndex > oldIndex) newIndex -= 1; + final item = overlayItems.removeAt(oldIndex); + overlayItems.insert(newIndex, item); + }); + }, + ); + }, + ), + ], + ), + body: Screenshot( + controller: screenshotController, + child: Container( + color: Colors.white, + child: Stack( + children: [ + GestureDetector( + onPanStart: (details) => startDrawing(details.localPosition), + onPanUpdate: (details) => drawUpdate(details.localPosition), + child: CustomPaint( + size: Size.infinite, + painter: Sketcher(lines), + ), + ), + for (var item in overlayItems) + if (item.type == 'text') + DraggableResizableText( + overlayItem: item, + text: item.text!, + color: item.color!, + isCapturing: isCapturing, + font: item.font, + fontSize: item.fontSize, + onDelete: () { + setState(() { + overlayItems.removeWhere((e) => e.id == item.id); + }); + }, + ) + else if (item.type == 'image') + DraggableResizableImage( + overlayItem: item, + imageBytes: item.imageBytes!, + isCapturing: isCapturing, + onDelete: () { + setState(() { + overlayItems.removeWhere((e) => e.id == item.id); + }); + }, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/draw_canvas/view/image_adjust_page.dart b/lib/draw_canvas/view/image_adjust_page.dart new file mode 100644 index 00000000..ebc4bed6 --- /dev/null +++ b/lib/draw_canvas/view/image_adjust_page.dart @@ -0,0 +1,129 @@ +import 'dart:async'; +import 'dart:typed_data'; +import 'package:flutter/material.dart'; +import 'package:image/image.dart' as img; +import 'package:flutter/foundation.dart'; +import 'package:magic_epaper_app/draw_canvas/ImageAdjust/process_image.dart'; +import 'package:magic_epaper_app/draw_canvas/ImageAdjust/image_adjust_parms.dart'; + +class ImageAdjustScreen extends StatefulWidget { + final Uint8List imageBytes; + + const ImageAdjustScreen({Key? key, required this.imageBytes}) + : super(key: key); + + @override + State createState() => _ImageAdjustScreenState(); +} + +class _ImageAdjustScreenState extends State { + double brightness = 1.0; + double contrast = 1.0; + late img.Image fullResImage; + + Uint8List? processedBytes; + late img.Image originalImage; + bool isLoading = true; + + @override + void initState() { + super.initState(); + decodeAndProcess(); + } + + Future decodeAndProcess() async { + fullResImage = img.decodeImage(widget.imageBytes)!; + originalImage = img.copyResize(fullResImage, width: 512); + await applyAdjustments(); + } + + Future applyAdjustments() async { + setState(() => isLoading = true); + + final resultBytes = await compute( + processImage, + ImageAdjustParams( + originalImage, + brightness, + contrast, + ), + ); + + if (mounted) { + setState(() { + processedBytes = resultBytes; + isLoading = false; + }); + } + } + + Timer? debounceTimer; + + void onSliderChanged({double? b, double? c}) { + if (b != null) brightness = b; + if (c != null) contrast = c; + + debounceTimer?.cancel(); + debounceTimer = Timer(const Duration(milliseconds: 300), () { + applyAdjustments(); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text("Adjust Image")), + body: Column( + children: [ + if (isLoading) + const Expanded(child: Center(child: CircularProgressIndicator())) + else if (processedBytes != null) + Expanded(child: Image.memory(processedBytes!)), + const SizedBox(height: 10), + Text("Brightness"), + Slider( + value: brightness, + min: 0.0, + max: 2.0, + divisions: 20, + label: brightness.toStringAsFixed(2), + onChanged: (val) => onSliderChanged(b: val), + ), + Text("Contrast"), + Slider( + value: contrast, + min: 0.0, + max: 2.0, + divisions: 30, + label: contrast.toStringAsFixed(2), + onChanged: (val) => onSliderChanged(c: val), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text("Cancel"), + ), + ElevatedButton( + onPressed: () async { + final fullProcessedBytes = await compute( + processImage, + ImageAdjustParams( + fullResImage, + brightness, + contrast, + ), + ); + + Navigator.pop(context, fullProcessedBytes); + }, + child: const Text("Submit"), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/provider/image_loader.dart b/lib/provider/image_loader.dart index d0fbd4fc..ae1c7c7f 100644 --- a/lib/provider/image_loader.dart +++ b/lib/provider/image_loader.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:image/image.dart' as img; import 'package:image_cropper/image_cropper.dart'; import 'package:image_picker/image_picker.dart'; +import 'dart:typed_data'; class ImageLoader extends ChangeNotifier { img.Image? image; @@ -24,4 +25,17 @@ class ImageLoader extends ChangeNotifier { notifyListeners(); } + + Future loadFromBytes({ + required Uint8List bytes, + required int width, + required int height, + }) async { + final decoded = img.decodeImage(bytes); + if (decoded != null) { + final resized = img.copyResize(decoded, width: width, height: height); + image = resized; + notifyListeners(); + } + } } diff --git a/lib/view/image_editor.dart b/lib/view/image_editor.dart index ad047b25..0b250f34 100644 --- a/lib/view/image_editor.dart +++ b/lib/view/image_editor.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:magic_epaper_app/draw_canvas/view/drawing_page.dart'; import 'package:magic_epaper_app/view/widget/image_list.dart'; import 'package:provider/provider.dart'; import 'package:image/image.dart' as img; import 'package:magic_epaper_app/provider/image_loader.dart'; import 'package:magic_epaper_app/util/epd/epd.dart'; +import 'dart:typed_data'; class ImageEditor extends StatelessWidget { final Epd epd; @@ -38,6 +40,26 @@ class ImageEditor extends StatelessWidget { }, child: const Text("Import Image"), ), + TextButton( + onPressed: () async { + final capturedImage = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => DrawingPage( + epd: epd, + )), + ); + + if (capturedImage != null && capturedImage is Uint8List) { + await context.read().loadFromBytes( + bytes: capturedImage, + width: epd.width, + height: epd.height, + ); + } + }, + child: const Text("Draw Canvas"), + ), ], ), body: Center(child: imgList), diff --git a/pubspec.yaml b/pubspec.yaml index c17d153e..83bb828e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,6 +28,9 @@ dependencies: image_cropper: ^9.0.0 app_settings: ^6.1.1 fluttertoast: ^8.2.12 + path_provider: ^2.1.5 + flutter_colorpicker: ^1.1.0 + screenshot: ^3.0.0 dev_dependencies: @@ -46,3 +49,20 @@ flutter: # Add assets from the images directory to the application. - assets/images/ + + fonts: + - family: Roboto Condensed + fonts: + - asset: assets/fonts/Roboto_Condensed-Regular.ttf + - family: Open Sans + fonts: + - asset: assets/fonts/OpenSans-Regular.ttf + - family: Lato + fonts: + - asset: assets/fonts/Lato-Regular.ttf + - family: Montserrat + fonts: + - asset: assets/fonts/Montserrat-Regular.ttf + - family: Oswald + fonts: + - asset: assets/fonts/Oswald-Regular.ttf