Skip to content

Commit cc2f104

Browse files
committed
feat: Adding files for visual editor folder. #3
1 parent 93975e9 commit cc2f104

File tree

5 files changed

+511
-0
lines changed

5 files changed

+511
-0
lines changed

Diff for: _visual-editor/lib/home_page.dart

+288
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
import 'dart:async';
2+
import 'dart:convert';
3+
import 'dart:io';
4+
5+
import 'package:app/main.dart';
6+
import 'package:file_picker/file_picker.dart';
7+
import 'package:flutter/material.dart';
8+
import 'package:visual_editor/document/models/attributes/attributes.model.dart';
9+
import 'package:visual_editor/visual-editor.dart';
10+
import 'package:path/path.dart';
11+
import 'package:path_provider/path_provider.dart';
12+
import 'package:http/http.dart' as http;
13+
import 'package:mime/mime.dart';
14+
import 'package:http_parser/http_parser.dart';
15+
16+
const quillEditorKey = Key('quillEditorKey');
17+
18+
/// Home page with the `flutter-quill` editor
19+
class HomePage extends StatefulWidget {
20+
const HomePage({
21+
required this.platformService,
22+
super.key,
23+
});
24+
25+
final PlatformService platformService;
26+
27+
@override
28+
HomePageState createState() => HomePageState();
29+
}
30+
31+
class HomePageState extends State<HomePage> {
32+
/// `flutter-quill` editor controller
33+
EditorController? _controller;
34+
35+
/// Focus node used to obtain keyboard focus and events
36+
final FocusNode _focusNode = FocusNode();
37+
38+
@override
39+
void initState() {
40+
super.initState();
41+
_initializeText();
42+
}
43+
44+
/// Initializing the [Delta](https://quilljs.com/docs/delta/) document with sample text.
45+
Future<void> _initializeText() async {
46+
// final doc = Document()..insert(0, 'Just a friendly empty text :)');
47+
final doc = DeltaDocM();
48+
setState(() {
49+
_controller = EditorController(
50+
document: doc,
51+
);
52+
});
53+
}
54+
55+
@override
56+
Widget build(BuildContext context) {
57+
/// Loading widget if controller's not loaded
58+
if (_controller == null) {
59+
return const Scaffold(body: Center(child: Text('Loading...')));
60+
}
61+
62+
/// Returning scaffold with editor as body
63+
return Scaffold(
64+
appBar: AppBar(
65+
backgroundColor: Colors.white,
66+
elevation: 0,
67+
centerTitle: false,
68+
title: const Text(
69+
'Visual Editor',
70+
),
71+
),
72+
body: _buildEditor(context),
73+
);
74+
}
75+
76+
/// Build the `flutter-quill` editor to be shown on screen.
77+
Widget _buildEditor(BuildContext context) {
78+
// Default editor (for mobile devices)
79+
Widget quillEditor = VisualEditor(
80+
controller: _controller!,
81+
scrollController: ScrollController(),
82+
focusNode: _focusNode,
83+
config: EditorConfigM(
84+
scrollable: true,
85+
autoFocus: false,
86+
readOnly: false,
87+
placeholder: 'Write what\'s on your mind.',
88+
enableInteractiveSelection: true,
89+
expands: false,
90+
padding: EdgeInsets.zero,
91+
customStyles: const EditorStylesM(
92+
h1: TextBlockStyleM(
93+
TextStyle(
94+
fontSize: 32,
95+
color: Colors.black,
96+
height: 1.15,
97+
fontWeight: FontWeight.w300,
98+
),
99+
VerticalSpacing(top: 16, bottom: 0),
100+
VerticalSpacing(top: 0, bottom: 0),
101+
VerticalSpacing(top: 16, bottom: 0),
102+
null,
103+
),
104+
sizeSmall: TextStyle(fontSize: 9),
105+
),
106+
),
107+
);
108+
109+
// Alternatively, the web editor version is shown (with the web embeds)
110+
if (widget.platformService.isWebPlatform()) {
111+
quillEditor = VisualEditor(
112+
controller: _controller!,
113+
scrollController: ScrollController(),
114+
focusNode: _focusNode,
115+
config: EditorConfigM(
116+
scrollable: true,
117+
enableInteractiveSelection: false,
118+
autoFocus: false,
119+
readOnly: false,
120+
placeholder: 'Add content',
121+
expands: false,
122+
padding: EdgeInsets.zero,
123+
customStyles: const EditorStylesM(
124+
h1: TextBlockStyleM(
125+
TextStyle(
126+
fontSize: 32,
127+
color: Colors.black,
128+
height: 1.15,
129+
fontWeight: FontWeight.w300,
130+
),
131+
VerticalSpacing(top: 16, bottom: 0),
132+
VerticalSpacing(top: 0, bottom: 0),
133+
VerticalSpacing(top: 16, bottom: 0),
134+
null,
135+
),
136+
sizeSmall: TextStyle(fontSize: 9),
137+
),
138+
),
139+
);
140+
}
141+
142+
// Toolbar definitions
143+
const toolbarIconSize = 18.0;
144+
const toolbarButtonSpacing = 2.5;
145+
146+
// Instantiating the toolbar
147+
final toolbar = EditorToolbar(
148+
children: [
149+
HistoryButton(
150+
buttonsSpacing: toolbarButtonSpacing,
151+
icon: Icons.undo_outlined,
152+
iconSize: toolbarIconSize,
153+
controller: _controller!,
154+
isUndo: true,
155+
),
156+
HistoryButton(
157+
buttonsSpacing: toolbarButtonSpacing,
158+
icon: Icons.redo_outlined,
159+
iconSize: toolbarIconSize,
160+
controller: _controller!,
161+
isUndo: false,
162+
),
163+
ToggleStyleButton(
164+
buttonsSpacing: toolbarButtonSpacing,
165+
attribute: AttributesM.bold,
166+
icon: Icons.format_bold,
167+
iconSize: toolbarIconSize,
168+
controller: _controller!,
169+
),
170+
ToggleStyleButton(
171+
buttonsSpacing: toolbarButtonSpacing,
172+
attribute: AttributesM.italic,
173+
icon: Icons.format_italic,
174+
iconSize: toolbarIconSize,
175+
controller: _controller!,
176+
),
177+
ToggleStyleButton(
178+
buttonsSpacing: toolbarButtonSpacing,
179+
attribute: AttributesM.underline,
180+
icon: Icons.format_underline,
181+
iconSize: toolbarIconSize,
182+
controller: _controller!,
183+
),
184+
ToggleStyleButton(
185+
buttonsSpacing: toolbarButtonSpacing,
186+
attribute: AttributesM.strikeThrough,
187+
icon: Icons.format_strikethrough,
188+
iconSize: toolbarIconSize,
189+
controller: _controller!,
190+
),
191+
192+
// Our embed buttons
193+
ImageButton(
194+
icon: Icons.image,
195+
iconSize: toolbarIconSize,
196+
buttonsSpacing: toolbarButtonSpacing,
197+
controller: _controller!,
198+
onImagePickCallback: _onImagePickCallback,
199+
webImagePickImpl: _webImagePickImpl,
200+
mediaPickSettingSelector: (context) {
201+
return Future.value(MediaPickSettingE.Gallery);
202+
},
203+
),
204+
],
205+
);
206+
207+
// Rendering the final editor + toolbar
208+
return SafeArea(
209+
child: Column(
210+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
211+
children: <Widget>[
212+
Expanded(
213+
flex: 15,
214+
child: Container(
215+
key: quillEditorKey,
216+
color: Colors.white,
217+
padding: const EdgeInsets.only(left: 16, right: 16),
218+
child: quillEditor,
219+
),
220+
),
221+
Container(child: toolbar),
222+
],
223+
),
224+
);
225+
}
226+
227+
/// Renders the image picked by imagePicker from local file storage
228+
/// You can also upload the picked image to any server (eg : AWS s3
229+
/// or Firebase) and then return the uploaded image URL.
230+
///
231+
/// It's only called on mobile platforms.
232+
Future<String> _onImagePickCallback(File file) async {
233+
final appDocDir = await getApplicationDocumentsDirectory();
234+
final copiedFile = await file.copy('${appDocDir.path}/${basename(file.path)}');
235+
return copiedFile.path.toString();
236+
}
237+
238+
/// Callback that is called after an image is picked whilst on the web platform.
239+
/// Returns the URL of the image.
240+
/// Returns null if an error occurred uploading the file or the image was not picked.
241+
Future<String?> _webImagePickImpl(OnImagePickCallback onImagePickCallback) async {
242+
// Lets the user pick one file; files with any file extension can be selected
243+
final result = await ImageFilePicker().pickImage();
244+
245+
// The result will be null, if the user aborted the dialog
246+
if (result == null || result.files.isEmpty) {
247+
return null;
248+
}
249+
250+
// Read file as bytes (https://github.com/miguelpruivo/flutter_file_picker/wiki/FAQ#q-how-do-i-access-the-path-on-web)
251+
final platformFile = result.files.first;
252+
final bytes = platformFile.bytes;
253+
254+
if (bytes == null) {
255+
return null;
256+
}
257+
258+
// Make HTTP request to upload the image to the file
259+
const apiURL = 'https://imgup.fly.dev/api/images';
260+
final request = http.MultipartRequest('POST', Uri.parse(apiURL));
261+
262+
final httpImage = http.MultipartFile.fromBytes(
263+
'image',
264+
bytes,
265+
contentType: MediaType.parse(lookupMimeType('', headerBytes: bytes)!),
266+
filename: platformFile.name,
267+
);
268+
request.files.add(httpImage);
269+
270+
// Check the response and handle accordingly
271+
return http.Client().send(request).then((response) async {
272+
if (response.statusCode != 200) {
273+
return null;
274+
}
275+
276+
final responseStream = await http.Response.fromStream(response);
277+
final responseData = json.decode(responseStream.body);
278+
return responseData['url'];
279+
});
280+
}
281+
}
282+
283+
// coverage:ignore-start
284+
/// Image file picker wrapper class
285+
class ImageFilePicker {
286+
Future<FilePickerResult?> pickImage() => FilePicker.platform.pickFiles(type: FileType.image);
287+
}
288+
// coverage:ignore-end

Diff for: _visual-editor/lib/main.dart

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import 'package:app/home_page.dart';
2+
import 'package:flutter/foundation.dart';
3+
import 'package:flutter/material.dart';
4+
import 'package:responsive_framework/responsive_framework.dart';
5+
6+
// coverage:ignore-start
7+
void main() {
8+
WidgetsFlutterBinding.ensureInitialized();
9+
runApp(
10+
App(
11+
platformService: PlatformService(),
12+
),
13+
);
14+
}
15+
// coverage:ignore-end
16+
17+
/// Entry gateway to the application.
18+
/// Defining the MaterialApp attributes and Responsive Framework breakpoints.
19+
class App extends StatelessWidget {
20+
const App({required this.platformService, super.key});
21+
22+
final PlatformService platformService;
23+
24+
@override
25+
Widget build(BuildContext context) {
26+
return MaterialApp(
27+
title: 'Flutter Editor Demo',
28+
builder: (context, child) => ResponsiveBreakpoints.builder(
29+
child: child!,
30+
breakpoints: [
31+
const Breakpoint(start: 0, end: 425, name: MOBILE),
32+
const Breakpoint(start: 426, end: 768, name: TABLET),
33+
const Breakpoint(start: 769, end: 1440, name: DESKTOP),
34+
const Breakpoint(start: 1441, end: double.infinity, name: '4K'),
35+
],
36+
),
37+
theme: ThemeData(
38+
colorScheme: ColorScheme.fromSeed(seedColor: Colors.white),
39+
useMaterial3: true,
40+
),
41+
home: HomePage(platformService: platformService),
42+
);
43+
}
44+
}
45+
46+
// coverage:ignore-start
47+
/// Platform service class that tells if the platform is web-based or not
48+
class PlatformService {
49+
bool isWebPlatform() {
50+
return kIsWeb;
51+
}
52+
}
53+
// coverage:ignore-end

Diff for: _visual-editor/test/sample.jpeg

541 KB
Loading

0 commit comments

Comments
 (0)