Flutter: Channel stable, 1.22.1 zefyr: ^1.0.0-dev.1.0Copy the code

** Complete code at the end, the body skip not to see the final complete code **

First, writing background

I’ve been working on projects for the last few years, so I haven’t been blogging much. One is lazy, the other is busy, just want to fast forward the project, after all, blogging is also a lot of effort.

However, because Zefyr has just upgraded to 1.0.0, the documentation is not perfect, the official case is not complete, I also read the source code to understand how to use. Write this article as a memo to record, two is also to share with friends in need. Without further ado, let’s move on to the main body.

Second, the integration of

The first thing to know is that this article is aimed at Zefyr 1.0.0, which is completely different from 0.x.

Step 1: Invoke the editor

	ZefyrEditor(
              controller: _controller,
              focusNode: _focusNode,
              autofocus: true,
              embedBuilder: customZefyrEmbedBuilder,   // embedBuilder is the function that handles image uploads
              // readOnly: true, // readOnly: true
            ),
Copy the code

Step 2, customize the toolbar

The official case doesn’t give us any guidance on how to customize the toolbars, but I just read the source code. The following code is copied from the Zefyr source code, and can be added or deleted as needed.

var toolbar =  ZefyrToolbar(children:[
      ToggleStyleButton(
        attribute: NotusAttribute.bold,
        icon: Icons.format_bold,
        controller: _controller,
      ),
      SizedBox(width: 1),
      ToggleStyleButton(
        attribute: NotusAttribute.italic,
        icon: Icons.format_italic,
        controller: _controller,
      ),
      VerticalDivider(indent: 16, endIndent: 16, color: Colors.grey.shade400),
      SelectHeadingStyleButton(controller: _controller),
      VerticalDivider(indent: 16, endIndent: 16, color: Colors.grey.shade400),
      ToggleStyleButton(
        attribute: NotusAttribute.block.numberList,
        controller: _controller,
        icon: Icons.format_list_numbered,
      ),
      ToggleStyleButton(
        attribute: NotusAttribute.block.bulletList,
        controller: _controller,
        icon: Icons.format_list_bulleted,
      ),
      VerticalDivider(indent: 16, endIndent: 16, color: Colors.grey.shade400),
      ToggleStyleButton(
        attribute: NotusAttribute.block.quote,
        controller: _controller,
        icon: Icons.format_quote,
      ),
      CustomInsertImageButton( // Create a custom image upload component
        controller: _controller,
        icon: Icons.image,
      ),]);
 
Copy the code
Expanded(
          child: Container(
            padding: const EdgeInsets.only(left: 16.0, right: 16.0),
            child: ZefyrEditor(
              controller: _controller,
              focusNode: _focusNode,
              autofocus: true,
              embedBuilder: customZefyrEmbedBuilder,
            ),
          ),
        ),
        SingleChildScrollView(  // Overflow screen scrollable
          scrollDirection: Axis.horizontal, 
          child: toolbar ),
Copy the code

Step 3: Customize the image upload button

When I upgraded to 1.0, I found that the implementation logic of this is completely different from 0.x. I asked the author on Github and the author replied that he would write a demo in a few days. So I went back to digging into source code. Here is the implementation code

	
Widget customZefyrEmbedBuilder(BuildContext context, EmbedNode node) {
   
    if ( node.value.type.contains('http://')) {
        return Container(
          width: MediaQuery.of(context).size.width,
          child: GestureDetector(
            child: Image.network(
                node.value.type,
                fit: BoxFit.fill
            ),
            onTap: () {
              //Navigator.push(context, MaterialPageRoute(builder: (_) {
              // return DetailScreen(node.value.type);
              / /})); },),); }return Container();
}
Copy the code

Contains (‘http://’) ‘) contains(node.value.type.

Because I found the Zefyr toolbar, you’ll call embedBuilder for every button you click (which always calls customZefyrEmbedBuilder) and pass in the node.value.type variable. In this case, we cannot determine whether the user clicked the upload button and then process the relevant logic.

Then I came up with the idea of using Node.value.type. After reviewing the source code, I realized that I could implement the button myself and have it pass in specific parameters. So we can tell.

	
class CustomInsertImageButton extends StatelessWidget {
  final ZefyrController controller;
  final IconData icon;

  const CustomInsertImageButton({
    Key key,
    @required this.controller,
    @required this.icon,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ZIconButton(
      highlightElevation: 0,
      hoverElevation: 0,
      size: 32,
      icon: Icon(
        icon,
        size: 18,
        color: Theme.of(context).iconTheme.color,
      ),
      fillColor: Theme.of(context).canvasColor,
      onPressed: () {
        final index = controller.selection.baseOffset;
        final length = controller.selection.extentOffset - index;
        ImageSource  gallerySource = ImageSource.gallery;
        finalimage = pickImage(gallerySource); image.then((value) => { controller.replaceText(index, length, BlockEmbed(value) ) } ); }); }}Copy the code

Basically, these are the lines below, so we get the image from the imagpicker, and when we upload it to the server, we get the address of the image. It is then passed to the EmbedBuilder function as the value of node.value.type.

	image.then((value) => {
          controller.replaceText(index, length, BlockEmbed(value) )
        } );
Copy the code

The above BlockEmbed(value) element comes to the bottom here. The image address is xxxxxxx.img.

	if ( node.value.type.contains('http://')) {}Copy the code

Complete code

import 'dart:convert';
import 'dart:io';

import 'package: bilingualapp/plugins/image_picker 0.6.7 + 11 / lib/image_picker. Dart';
import 'package:bilingualapp/util/print.dart';
import 'package:flutter/material.dart';
import 'package:quill_delta/quill_delta.dart';
import 'package:zefyr/zefyr.dart';
import 'package:path/path.dart';
import 'package:http/http.dart' as http;
import '.. /config.dart';
import 'package:async/async.dart';

class EditorPage extends StatefulWidget {
  final ZefyrController _controller;
  final bool _editing ;
  
  EditorPage(this._controller,this._editing);

  @override
  EditorPageState createState() => EditorPageState(_controller,this._editing);
}

class EditorPageState extends State<EditorPage> {
  ZefyrController _controller;

  FocusNode _focusNode;
  bool _editing = false;

  EditorPageState(this._controller,this._editing);

  @override
  void initState() {
    super.initState(); Delta().. insert('Karl', {'bold': true})
      ..insert(' the ')
      ..insert('Fog', {'italic': true});

    
    if (_controller == null) {final document = _loadDocument();
      _controller = ZefyrController(document);
      _controller.addListener((){
        final contents = jsonEncode(_controller.document);
      });
    }
    _focusNode = FocusNode();
  }

  Widget _buildWelcomeEditor(BuildContext context) {

    var toolbar =  ZefyrToolbar(children:[
      ToggleStyleButton(
        attribute: NotusAttribute.bold,
        icon: Icons.format_bold,
        controller: _controller,
      ),
      SizedBox(width: 1),
      ToggleStyleButton(
        attribute: NotusAttribute.italic,
        icon: Icons.format_italic,
        controller: _controller,
      ),
      VerticalDivider(indent: 16, endIndent: 16, color: Colors.grey.shade400),
      SelectHeadingStyleButton(controller: _controller),
      VerticalDivider(indent: 16, endIndent: 16, color: Colors.grey.shade400),
      ToggleStyleButton(
        attribute: NotusAttribute.block.numberList,
        controller: _controller,
        icon: Icons.format_list_numbered,
      ),
      ToggleStyleButton(
        attribute: NotusAttribute.block.bulletList,
        controller: _controller,
        icon: Icons.format_list_bulleted,
      ),
      VerticalDivider(indent: 16, endIndent: 16, color: Colors.grey.shade400),
      ToggleStyleButton(
        attribute: NotusAttribute.block.quote,
        controller: _controller,
        icon: Icons.format_quote,
      ),
      CustomInsertImageButton(
        controller: _controller,
        icon: Icons.image,
      ),]);
    return Column(
      children: [
        Divider(height: 1, thickness: 1, color: Colors.grey.shade200),
        Expanded(
          child: Container(
            padding: const EdgeInsets.only(left: 16.0, right: 16.0),
            child: ZefyrEditor(
              controller: _controller,
              focusNode: _focusNode,
              autofocus: true,
              embedBuilder: customZefyrEmbedBuilder,
              // readOnly: true,
              // padding: EdgeInsets.only(left: 16, right: 16),
              // onLaunchUrl: _launchUrl,
            ),
          ),
        ),
        SingleChildScrollView(
          scrollDirection: Axis.horizontal, 
          child: toolbar ),
      ],
    );
  }

  

  @override
  Widget build(BuildContext context) {
    return Expanded(
      child:_buildWelcomeEditor(context)
    );
  }

  NotusDocument _loadDocument() {
    finalDelta delta = Delta().. insert("\n");
    return NotusDocument.fromJson(delta.toJson());
  }
}
  
Widget customZefyrEmbedBuilder(BuildContext context, EmbedNode node) {
   
    if ( node.value.type.contains('http://')) {
        return Container(
          width: MediaQuery.of(context).size.width,
          child: GestureDetector(
            child: Image.network(
                node.value.type,
                fit: BoxFit.fill
            ),
            onTap: () {
              Navigator.push(context, MaterialPageRoute(builder: (_) {
                returnDetailScreen(node.value.type); })); },),); }return Container();
}

class CustomInsertImageButton extends StatelessWidget {
  final ZefyrController controller;
  final IconData icon;

  const CustomInsertImageButton({
    Key key,
    @required this.controller,
    @required this.icon,
  }) : super(key: key);

  Future<String> upload(File imageFile) async {    
      // open a bytestream
      var stream = http.ByteStream(DelegatingStream.typed(imageFile.openRead()));
      // get file length
      var length = await imageFile.length();

      // string to uri
      var uri = Uri.parse(server + "/upload");

      // create multipart request
      var request = http.MultipartRequest("POST", uri);

      // multipart that takes file
      var multipartFile = http.MultipartFile('note', stream, length,
          filename: basename(imageFile.path));

      // add file to multipart
      request.files.add(multipartFile);

      // send
      var response = await request.send();
      // listen for response.join()
      return response.stream.transform(utf8.decoder).join();
    }
  
  Future<String> pickImage(ImageSource source) async {
      final file = await ImagePicker.pickImage(source: source,imageQuality: 65);
      if (file == null) return null;
      String value =  await upload(file);
      var v = jsonDecode(value);
      var url = server + "/" + v["data"] ["filepath"];
      print(url);
      return url;
    }

  @override
  Widget build(BuildContext context) {
    return ZIconButton(
      highlightElevation: 0,
      hoverElevation: 0,
      size: 32,
      icon: Icon(
        icon,
        size: 18,
        color: Theme.of(context).iconTheme.color,
      ),
      fillColor: Theme.of(context).canvasColor,
      onPressed: () {
        final index = controller.selection.baseOffset;
        final length = controller.selection.extentOffset - index;
        ImageSource  gallerySource = ImageSource.gallery;
        // controller.replaceText(index, length, BlockEmbed.image("https://img.alicdn.com/imgextra/i1/6000000003634/O1CN01XkL17h1ciPvkUalkW_!! 6000000003634-2-octopus.png",));
        finalimage = pickImage(gallerySource); image.then((value) => { controller.replaceText(index, length, BlockEmbed(value) ) } ); }); }}class DetailScreen extends StatelessWidget {
  
  String _image = "";
  DetailScreen(this._image);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: GestureDetector(
        child: Center(
          child: Hero(
            tag: 'imageHero', child: Image.network( _image, fit: BoxFit.contain ) ), ), onTap: () { Navigator.pop(context); },),); }}Copy the code

Personal home page: YEE Domain

Recite the words Flutter APP: domain English APP