-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Adding files for visual editor folder. #3
1 parent
93975e9
commit cc2f104
Showing
5 changed files
with
511 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,288 @@ | ||
import 'dart:async'; | ||
import 'dart:convert'; | ||
import 'dart:io'; | ||
|
||
import 'package:app/main.dart'; | ||
import 'package:file_picker/file_picker.dart'; | ||
import 'package:flutter/material.dart'; | ||
import 'package:visual_editor/document/models/attributes/attributes.model.dart'; | ||
import 'package:visual_editor/visual-editor.dart'; | ||
import 'package:path/path.dart'; | ||
import 'package:path_provider/path_provider.dart'; | ||
import 'package:http/http.dart' as http; | ||
import 'package:mime/mime.dart'; | ||
import 'package:http_parser/http_parser.dart'; | ||
|
||
const quillEditorKey = Key('quillEditorKey'); | ||
|
||
/// Home page with the `flutter-quill` editor | ||
class HomePage extends StatefulWidget { | ||
const HomePage({ | ||
required this.platformService, | ||
super.key, | ||
}); | ||
|
||
final PlatformService platformService; | ||
|
||
@override | ||
HomePageState createState() => HomePageState(); | ||
} | ||
|
||
class HomePageState extends State<HomePage> { | ||
/// `flutter-quill` editor controller | ||
EditorController? _controller; | ||
|
||
/// Focus node used to obtain keyboard focus and events | ||
final FocusNode _focusNode = FocusNode(); | ||
|
||
@override | ||
void initState() { | ||
super.initState(); | ||
_initializeText(); | ||
} | ||
|
||
/// Initializing the [Delta](https://quilljs.com/docs/delta/) document with sample text. | ||
Future<void> _initializeText() async { | ||
// final doc = Document()..insert(0, 'Just a friendly empty text :)'); | ||
final doc = DeltaDocM(); | ||
setState(() { | ||
_controller = EditorController( | ||
document: doc, | ||
); | ||
}); | ||
} | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
/// Loading widget if controller's not loaded | ||
if (_controller == null) { | ||
return const Scaffold(body: Center(child: Text('Loading...'))); | ||
} | ||
|
||
/// Returning scaffold with editor as body | ||
return Scaffold( | ||
appBar: AppBar( | ||
backgroundColor: Colors.white, | ||
elevation: 0, | ||
centerTitle: false, | ||
title: const Text( | ||
'Visual Editor', | ||
), | ||
), | ||
body: _buildEditor(context), | ||
); | ||
} | ||
|
||
/// Build the `flutter-quill` editor to be shown on screen. | ||
Widget _buildEditor(BuildContext context) { | ||
// Default editor (for mobile devices) | ||
Widget quillEditor = VisualEditor( | ||
controller: _controller!, | ||
scrollController: ScrollController(), | ||
focusNode: _focusNode, | ||
config: EditorConfigM( | ||
scrollable: true, | ||
autoFocus: false, | ||
readOnly: false, | ||
placeholder: 'Write what\'s on your mind.', | ||
enableInteractiveSelection: true, | ||
expands: false, | ||
padding: EdgeInsets.zero, | ||
customStyles: const EditorStylesM( | ||
h1: TextBlockStyleM( | ||
TextStyle( | ||
fontSize: 32, | ||
color: Colors.black, | ||
height: 1.15, | ||
fontWeight: FontWeight.w300, | ||
), | ||
VerticalSpacing(top: 16, bottom: 0), | ||
VerticalSpacing(top: 0, bottom: 0), | ||
VerticalSpacing(top: 16, bottom: 0), | ||
null, | ||
), | ||
sizeSmall: TextStyle(fontSize: 9), | ||
), | ||
), | ||
); | ||
|
||
// Alternatively, the web editor version is shown (with the web embeds) | ||
if (widget.platformService.isWebPlatform()) { | ||
quillEditor = VisualEditor( | ||
controller: _controller!, | ||
scrollController: ScrollController(), | ||
focusNode: _focusNode, | ||
config: EditorConfigM( | ||
scrollable: true, | ||
enableInteractiveSelection: false, | ||
autoFocus: false, | ||
readOnly: false, | ||
placeholder: 'Add content', | ||
expands: false, | ||
padding: EdgeInsets.zero, | ||
customStyles: const EditorStylesM( | ||
h1: TextBlockStyleM( | ||
TextStyle( | ||
fontSize: 32, | ||
color: Colors.black, | ||
height: 1.15, | ||
fontWeight: FontWeight.w300, | ||
), | ||
VerticalSpacing(top: 16, bottom: 0), | ||
VerticalSpacing(top: 0, bottom: 0), | ||
VerticalSpacing(top: 16, bottom: 0), | ||
null, | ||
), | ||
sizeSmall: TextStyle(fontSize: 9), | ||
), | ||
), | ||
); | ||
} | ||
|
||
// Toolbar definitions | ||
const toolbarIconSize = 18.0; | ||
const toolbarButtonSpacing = 2.5; | ||
|
||
// Instantiating the toolbar | ||
final toolbar = EditorToolbar( | ||
children: [ | ||
HistoryButton( | ||
buttonsSpacing: toolbarButtonSpacing, | ||
icon: Icons.undo_outlined, | ||
iconSize: toolbarIconSize, | ||
controller: _controller!, | ||
isUndo: true, | ||
), | ||
HistoryButton( | ||
buttonsSpacing: toolbarButtonSpacing, | ||
icon: Icons.redo_outlined, | ||
iconSize: toolbarIconSize, | ||
controller: _controller!, | ||
isUndo: false, | ||
), | ||
ToggleStyleButton( | ||
buttonsSpacing: toolbarButtonSpacing, | ||
attribute: AttributesM.bold, | ||
icon: Icons.format_bold, | ||
iconSize: toolbarIconSize, | ||
controller: _controller!, | ||
), | ||
ToggleStyleButton( | ||
buttonsSpacing: toolbarButtonSpacing, | ||
attribute: AttributesM.italic, | ||
icon: Icons.format_italic, | ||
iconSize: toolbarIconSize, | ||
controller: _controller!, | ||
), | ||
ToggleStyleButton( | ||
buttonsSpacing: toolbarButtonSpacing, | ||
attribute: AttributesM.underline, | ||
icon: Icons.format_underline, | ||
iconSize: toolbarIconSize, | ||
controller: _controller!, | ||
), | ||
ToggleStyleButton( | ||
buttonsSpacing: toolbarButtonSpacing, | ||
attribute: AttributesM.strikeThrough, | ||
icon: Icons.format_strikethrough, | ||
iconSize: toolbarIconSize, | ||
controller: _controller!, | ||
), | ||
|
||
// Our embed buttons | ||
ImageButton( | ||
icon: Icons.image, | ||
iconSize: toolbarIconSize, | ||
buttonsSpacing: toolbarButtonSpacing, | ||
controller: _controller!, | ||
onImagePickCallback: _onImagePickCallback, | ||
webImagePickImpl: _webImagePickImpl, | ||
mediaPickSettingSelector: (context) { | ||
return Future.value(MediaPickSettingE.Gallery); | ||
}, | ||
), | ||
], | ||
); | ||
|
||
// Rendering the final editor + toolbar | ||
return SafeArea( | ||
child: Column( | ||
mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||
children: <Widget>[ | ||
Expanded( | ||
flex: 15, | ||
child: Container( | ||
key: quillEditorKey, | ||
color: Colors.white, | ||
padding: const EdgeInsets.only(left: 16, right: 16), | ||
child: quillEditor, | ||
), | ||
), | ||
Container(child: toolbar), | ||
], | ||
), | ||
); | ||
} | ||
|
||
/// Renders the image picked by imagePicker from local file storage | ||
/// You can also upload the picked image to any server (eg : AWS s3 | ||
/// or Firebase) and then return the uploaded image URL. | ||
/// | ||
/// It's only called on mobile platforms. | ||
Future<String> _onImagePickCallback(File file) async { | ||
final appDocDir = await getApplicationDocumentsDirectory(); | ||
final copiedFile = await file.copy('${appDocDir.path}/${basename(file.path)}'); | ||
return copiedFile.path.toString(); | ||
} | ||
|
||
/// Callback that is called after an image is picked whilst on the web platform. | ||
/// Returns the URL of the image. | ||
/// Returns null if an error occurred uploading the file or the image was not picked. | ||
Future<String?> _webImagePickImpl(OnImagePickCallback onImagePickCallback) async { | ||
// Lets the user pick one file; files with any file extension can be selected | ||
final result = await ImageFilePicker().pickImage(); | ||
|
||
// The result will be null, if the user aborted the dialog | ||
if (result == null || result.files.isEmpty) { | ||
return null; | ||
} | ||
|
||
// Read file as bytes (https://github.com/miguelpruivo/flutter_file_picker/wiki/FAQ#q-how-do-i-access-the-path-on-web) | ||
final platformFile = result.files.first; | ||
final bytes = platformFile.bytes; | ||
|
||
if (bytes == null) { | ||
return null; | ||
} | ||
|
||
// Make HTTP request to upload the image to the file | ||
const apiURL = 'https://imgup.fly.dev/api/images'; | ||
final request = http.MultipartRequest('POST', Uri.parse(apiURL)); | ||
|
||
final httpImage = http.MultipartFile.fromBytes( | ||
'image', | ||
bytes, | ||
contentType: MediaType.parse(lookupMimeType('', headerBytes: bytes)!), | ||
filename: platformFile.name, | ||
); | ||
request.files.add(httpImage); | ||
|
||
// Check the response and handle accordingly | ||
return http.Client().send(request).then((response) async { | ||
if (response.statusCode != 200) { | ||
return null; | ||
} | ||
|
||
final responseStream = await http.Response.fromStream(response); | ||
final responseData = json.decode(responseStream.body); | ||
return responseData['url']; | ||
}); | ||
} | ||
} | ||
|
||
// coverage:ignore-start | ||
/// Image file picker wrapper class | ||
class ImageFilePicker { | ||
Future<FilePickerResult?> pickImage() => FilePicker.platform.pickFiles(type: FileType.image); | ||
} | ||
// coverage:ignore-end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import 'package:app/home_page.dart'; | ||
import 'package:flutter/foundation.dart'; | ||
import 'package:flutter/material.dart'; | ||
import 'package:responsive_framework/responsive_framework.dart'; | ||
|
||
// coverage:ignore-start | ||
void main() { | ||
WidgetsFlutterBinding.ensureInitialized(); | ||
runApp( | ||
App( | ||
platformService: PlatformService(), | ||
), | ||
); | ||
} | ||
// coverage:ignore-end | ||
|
||
/// Entry gateway to the application. | ||
/// Defining the MaterialApp attributes and Responsive Framework breakpoints. | ||
class App extends StatelessWidget { | ||
const App({required this.platformService, super.key}); | ||
|
||
final PlatformService platformService; | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
return MaterialApp( | ||
title: 'Flutter Editor Demo', | ||
builder: (context, child) => ResponsiveBreakpoints.builder( | ||
child: child!, | ||
breakpoints: [ | ||
const Breakpoint(start: 0, end: 425, name: MOBILE), | ||
const Breakpoint(start: 426, end: 768, name: TABLET), | ||
const Breakpoint(start: 769, end: 1440, name: DESKTOP), | ||
const Breakpoint(start: 1441, end: double.infinity, name: '4K'), | ||
], | ||
), | ||
theme: ThemeData( | ||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.white), | ||
useMaterial3: true, | ||
), | ||
home: HomePage(platformService: platformService), | ||
); | ||
} | ||
} | ||
|
||
// coverage:ignore-start | ||
/// Platform service class that tells if the platform is web-based or not | ||
class PlatformService { | ||
bool isWebPlatform() { | ||
return kIsWeb; | ||
} | ||
} | ||
// coverage:ignore-end |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
// This is a basic Flutter widget test. | ||
// | ||
// To perform an interaction with a widget in your test, use the WidgetTester | ||
// utility in the flutter_test package. For example, you can send tap and scroll | ||
// gestures. You can also use WidgetTester to find child widgets in the widget | ||
// tree, read text, and verify that the values of widget properties are correct. | ||
|
||
import 'package:cross_file/cross_file.dart'; | ||
import 'package:flutter_test/flutter_test.dart'; | ||
import 'package:visual_editor/visual-editor.dart'; | ||
|
||
import 'package:app/main.dart'; | ||
import 'package:mockito/annotations.dart'; | ||
import 'package:mockito/mockito.dart'; | ||
|
||
import 'package:flutter/services.dart'; | ||
|
||
// importing mocks | ||
import 'widget_test.mocks.dart'; | ||
|
||
/// This attempts to override the `image_picker` plugin inside `flutter-quill`, | ||
/// but is currently failing. | ||
/// | ||
/// Please check the links below for more context. | ||
/// https://stackoverflow.com/questions/76586920/mocking-imagepicker-in-flutter-integration-tests-not-working | ||
/// and https://stackoverflow.com/questions/52028969/testing-flutter-code-that-uses-a-plugin-and-platform-channel | ||
/// and https://docs.flutter.dev/testing/plugins-in-tests#mock-the-platform-channel | ||
/// | ||
/// This is currently commented because it crashes with a `PlatformException` stating | ||
/// the `XFile` instance is an invalid argument (it does the same with `File`). | ||
/// | ||
/// `XFile` would make sense since the line that calls `image-picker` | ||
/// is in https://github.com/singerdmx/flutter-quill/blob/36d72c1987f0cb8d6c689c12542600364c07e20f/flutter_quill_extensions/lib/embeds/toolbar/image_video_utils.dart#L147. | ||
void mockImagePicker(WidgetTester tester) { | ||
const channel = MethodChannel('plugins.flutter.io/image_picker'); | ||
|
||
Future<XFile?> handler(MethodCall methodCall) async { | ||
if (methodCall.method == 'pickImage') { | ||
final file = XFile('test/sample.jpeg'); | ||
return file; | ||
} else { | ||
return null; | ||
} | ||
} | ||
|
||
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(channel, (message) { | ||
return handler(message); | ||
}); | ||
} | ||
|
||
@GenerateMocks([PlatformService]) | ||
void main() { | ||
/// Check for context: https://stackoverflow.com/questions/60671728/unable-to-load-assets-in-flutter-tests | ||
setUpAll(() { | ||
TestWidgetsFlutterBinding.ensureInitialized(); | ||
}); | ||
|
||
testWidgets('Normal setup', (WidgetTester tester) async { | ||
final platformServiceMock = MockPlatformService(); | ||
// Platform is mobile | ||
when(platformServiceMock.isWebPlatform()).thenAnswer((_) => false); | ||
|
||
// Build our app and trigger a frame. | ||
await tester.pumpWidget( | ||
App( | ||
platformService: platformServiceMock, | ||
), | ||
); | ||
await tester.pumpAndSettle(); | ||
|
||
// Expect to find the normal page setup | ||
expect(find.text('Flutter Quill'), findsOneWidget); | ||
|
||
// Enter 'hi' into Quill Editor. | ||
await tester.tap(find.byType(VisualEditor)); | ||
await tester.enterText(find.byType(VisualEditor), 'hi\n'); | ||
await tester.pumpAndSettle(); | ||
}); | ||
|
||
testWidgets('Select image', (WidgetTester tester) async { | ||
final platformServiceMock = MockPlatformService(); | ||
|
||
// Platform is mobile | ||
when(platformServiceMock.isWebPlatform()).thenAnswer((_) => false); | ||
|
||
// Mock image | ||
// mockImagePicker(tester); | ||
|
||
// Build our app and trigger a frame. | ||
await tester.pumpWidget( | ||
App( | ||
platformService: platformServiceMock, | ||
), | ||
); | ||
await tester.pumpAndSettle(); | ||
|
||
// Expect to find the normal page setup | ||
expect(find.text('Flutter Quill'), findsOneWidget); | ||
|
||
// Enter 'hi' into Quill Editor. | ||
await tester.tap(find.byType(VisualEditor)); | ||
await tester.enterText(find.byType(VisualEditor), 'hi\n'); | ||
await tester.pumpAndSettle(); | ||
|
||
final imageButton = find.byType(ImageButton); | ||
await tester.tap(imageButton); | ||
await tester.pumpAndSettle(); | ||
}); | ||
|
||
testWidgets('Normal setup (web version)', (WidgetTester tester) async { | ||
tester.view.physicalSize = const Size(400, 600); | ||
tester.view.devicePixelRatio = 1.0; | ||
|
||
final platformServiceMock = MockPlatformService(); | ||
// Platform is desktop | ||
when(platformServiceMock.isWebPlatform()).thenAnswer((_) => true); | ||
|
||
// Build our app and trigger a frame. | ||
await tester.pumpWidget( | ||
App( | ||
platformService: platformServiceMock, | ||
), | ||
); | ||
await tester.pumpAndSettle(); | ||
|
||
// Expect to find the normal page setup | ||
expect(find.text('Flutter Quill'), findsOneWidget); | ||
|
||
// Enter 'hi' into Quill Editor. | ||
await tester.tap(find.byType(VisualEditor)); | ||
await tester.enterText(find.byType(VisualEditor), 'hi\n'); | ||
await tester.pumpAndSettle(); | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
// Mocks generated by Mockito 5.4.2 from annotations | ||
// in app/test/widget_test.dart. | ||
// Do not manually edit this file. | ||
|
||
// ignore_for_file: no_leading_underscores_for_library_prefixes | ||
import 'package:app/main.dart' as _i2; | ||
import 'package:mockito/mockito.dart' as _i1; | ||
|
||
// ignore_for_file: type=lint | ||
// ignore_for_file: avoid_redundant_argument_values | ||
// ignore_for_file: avoid_setters_without_getters | ||
// ignore_for_file: comment_references | ||
// ignore_for_file: implementation_imports | ||
// ignore_for_file: invalid_use_of_visible_for_testing_member | ||
// ignore_for_file: prefer_const_constructors | ||
// ignore_for_file: unnecessary_parenthesis | ||
// ignore_for_file: camel_case_types | ||
// ignore_for_file: subtype_of_sealed_class | ||
|
||
/// A class which mocks [PlatformService]. | ||
/// | ||
/// See the documentation for Mockito's code generation for more information. | ||
class MockPlatformService extends _i1.Mock implements _i2.PlatformService { | ||
MockPlatformService() { | ||
_i1.throwOnMissingStub(this); | ||
} | ||
|
||
@override | ||
bool isWebPlatform() => (super.noSuchMethod( | ||
Invocation.method( | ||
#isWebPlatform, | ||
[], | ||
), | ||
returnValue: false, | ||
) as bool); | ||
} |