appflowy-editor icon indicating copy to clipboard operation
appflowy-editor copied to clipboard

[Bug] Cannot capture AppFlowy Editor widget including images inserted as base64 encoded string

Open hjkim-mango opened this issue 11 months ago • 2 comments

Bug Description

I want to export the image file from the current editor content. So I used Screenshot package to capture it. It’s no issue to capture a visible widget. But there is an issue only to capture an invisible widget. I have to capture an invisible widget because my contents are over the screen. The result shows the base64-encoded images to [X] mark. If I insert images as an url or a local path, no issue for same case. It’s a problem just only for base64 encoded data.

How to Reproduce

  1. Insert an image as the base64 encoded in an editor.
  2. Capture from AppFlowy Editor widget with the current document.

Expected Behavior

Capture image show the same contents with the current editor.

Operating System

iOS, Android

AppFlowy Editor Version(s)

4.0.0

Screenshots

Current AppFlowy Editor Contents Screenshot 2024-12-26 at 4 40 07 PM

Captured Image Screenshot 2024-12-26 at 4 39 45 PM

Additional Context

appflowy_editor_widget.dart

import 'package:feasibility_markdown/appflowy_editor/save_image_page.dart';
import 'package:flutter/material.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:image_picker/image_picker.dart';
import 'dart:io';
import 'dart:convert';

class AppFlowyEditorWidget extends StatefulWidget {
  const AppFlowyEditorWidget({super.key});

  @override
  State<AppFlowyEditorWidget> createState() => _AppFlowyEditorWidgetState();
}

class _AppFlowyEditorWidgetState extends State<AppFlowyEditorWidget> {
  final _scaffoldKey = GlobalKey<ScaffoldState>();
  late EditorState _editorState;
  final _localToolbarItems = [
    textDecorationMobileToolbarItem,
    headingMobileToolbarItem,
    todoListMobileToolbarItem,
    listMobileToolbarItem,
    linkMobileToolbarItem,
    quoteMobileToolbarItem,
    codeMobileToolbarItem,
    blocksMobileToolbarItem,
    textDecorationMobileToolbarItemV2
  ];

  @override
  void initState() {
    super.initState();
    _editorState = EditorState.blank();
  }

  @override
  void dispose() {
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        key: _scaffoldKey,
        appBar: AppBar(
          backgroundColor: Theme.of(context).colorScheme.inversePrimary,
          title: const Text('AppFlowy Editor'),
        ),
        body: Column(children: [
          Expanded(
              child: AppFlowyEditor(
                  characterShortcutEvents: standardCharacterShortcutEvents,
                  editorStyle: const EditorStyle.mobile(),
                  editorState: _editorState)),
          Padding(
              padding: const EdgeInsets.all(5),
              child:
                  Row(mainAxisAlignment: MainAxisAlignment.center, children: [
                InkWell(
                    child: const Icon(Icons.image),
                    onTap: () async {
                      final ImagePicker picker = ImagePicker();
                      final XFile? pickedImage =
                          await picker.pickImage(source: ImageSource.gallery);
                      if (pickedImage != null) {
                        var imageBytes =
                            File(pickedImage.path).readAsBytesSync();
                        _editorState.insertImageNode(base64Encode(imageBytes));
                      }
                    }),
                InkWell(
                    child: const Icon(Icons.save_alt),
                    onTap: () async {
                      Navigator.of(context).push(MaterialPageRoute(
                          builder: (_) =>
                              SaveImagePage(editorState: _editorState)));
                    }),
              ])),
          MobileToolbar(
              editorState: _editorState, toolbarItems: _localToolbarItems)
        ]));
  }
}

save_image_page.dart

import 'dart:io';

import 'package:appflowy_editor/appflowy_editor.dart' as appflowy;
import 'package:flutter/material.dart';
import 'package:screenshot/screenshot.dart';
import 'package:gal/gal.dart';

class SaveImagePage extends StatefulWidget {
  const SaveImagePage({super.key, required this.editorState});

  final appflowy.EditorState editorState;

  @override
  State<SaveImagePage> createState() => _SaveImagePageState();
}

class _SaveImagePageState extends State<SaveImagePage> {
  final _scaffoldKey = GlobalKey<ScaffoldState>();
  late ScreenshotController _screenshotController;
  late appflowy.EditorState _editorState;

  @override
  void initState() {
    super.initState();
    _editorState = appflowy.EditorState(
        document:
            appflowy.Document.fromJson(widget.editorState.document.toJson()));
    _screenshotController = ScreenshotController();

    WidgetsBinding.instance.addPostFrameCallback((_) => _saveImage());
  }

  Widget _captureToSaveToImageWidget() {
    const physicalPixelBaseLength = 512.0;
    final logicalPixelBaseLength =
        physicalPixelBaseLength / MediaQuery.of(context).devicePixelRatio;

    return Container(
      color: Colors.white,
      width: logicalPixelBaseLength,
      constraints: BoxConstraints(minHeight: logicalPixelBaseLength),
      child: MediaQuery(
          data: const MediaQueryData(),
          child: IntrinsicHeight(
            child: appflowy.AppFlowyEditor(
                characterShortcutEvents:
                    appflowy.standardCharacterShortcutEvents,
                editorStyle: const appflowy.EditorStyle.mobile(),
                disableKeyboardService: true,
                disableSelectionService: true,
                shrinkWrap: true,
                editorScrollController: appflowy.EditorScrollController(
                    editorState: _editorState, shrinkWrap: true),
                editorState: _editorState),
            // )),
          )),
    );
  }

  void _saveImage() async {
    _screenshotController
        .captureFromLongWidget(_captureToSaveToImageWidget(),
            pixelRatio: MediaQuery.of(context).devicePixelRatio,
            delay: const Duration(seconds: 1))
        .then((capturedImage) async {
      if (!mounted) return;

      final file =
          await File('${Directory.systemTemp.path}/flutter_image.png').create();
      await file.writeAsBytes(capturedImage);
      await Gal.putImage(file.path);

      Navigator.of(context).pop();
    });
  }

  @override
  void dispose() {
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        key: _scaffoldKey,
        backgroundColor: Colors.white,
        body: const SafeArea(
            child: SizedBox(
          width: double.infinity,
          height: double.infinity,
          child: Column(
              mainAxisSize: MainAxisSize.max,
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text(
                  "Saving ...",
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    color: Colors.black,
                    fontWeight: FontWeight.w600,
                    fontSize: 14.0,
                  ),
                ),
              ]),
        )));
  }
}

hjkim-mango avatar Dec 26 '24 07:12 hjkim-mango