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