From 2170ea9e50af6e64c7e03fb32a7a0b79d2afbcda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Mon, 25 Sep 2023 12:44:06 +0100 Subject: [PATCH 01/10] feat: Adding heading styles options. #8 --- lib/home_page.dart | 72 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 63 insertions(+), 9 deletions(-) diff --git a/lib/home_page.dart b/lib/home_page.dart index 81239c9..cb31ea8 100644 --- a/lib/home_page.dart +++ b/lib/home_page.dart @@ -151,12 +151,34 @@ class HomePageState extends State { fontSize: 32, color: Colors.black, height: 1.15, - fontWeight: FontWeight.w300, + fontWeight: FontWeight.w600, ), const VerticalSpacing(16, 0), const VerticalSpacing(0, 0), null, ), + h2: DefaultTextBlockStyle( + const TextStyle( + fontSize: 24, + color: Colors.black87, + height: 1.15, + fontWeight: FontWeight.w600, + ), + const VerticalSpacing(8, 0), + const VerticalSpacing(0, 0), + null, + ), + h3: DefaultTextBlockStyle( + const TextStyle( + fontSize: 20, + color: Colors.black87, + height: 1.25, + fontWeight: FontWeight.w600, + ), + const VerticalSpacing(8, 0), + const VerticalSpacing(0, 0), + null, + ), sizeSmall: const TextStyle(fontSize: 9), subscript: const TextStyle( fontFamily: 'SF-UI-Display', @@ -191,12 +213,34 @@ class HomePageState extends State { fontSize: 32, color: Colors.black, height: 1.15, - fontWeight: FontWeight.w300, + fontWeight: FontWeight.w600, ), const VerticalSpacing(16, 0), const VerticalSpacing(0, 0), null, ), + h2: DefaultTextBlockStyle( + const TextStyle( + fontSize: 24, + color: Colors.black87, + height: 1.15, + fontWeight: FontWeight.w600, + ), + const VerticalSpacing(8, 0), + const VerticalSpacing(0, 0), + null, + ), + h3: DefaultTextBlockStyle( + const TextStyle( + fontSize: 20, + color: Colors.black87, + height: 1.25, + fontWeight: FontWeight.w600, + ), + const VerticalSpacing(8, 0), + const VerticalSpacing(0, 0), + null, + ), sizeSmall: const TextStyle(fontSize: 9), ), embedBuilders: [...defaultEmbedBuildersWeb], @@ -215,7 +259,7 @@ class HomePageState extends State { // `onImagePickCallback` is called after image is picked on mobile platforms onImagePickCallback: _onImagePickCallback, - // `webImagePickImpl` is called after image is picked on the web + // `webImagePickImpl` is called after image is picked on the web webImagePickImpl: _webImagePickImpl, // defining the selector (we only want to open the gallery whenever the person wants to upload an image) @@ -240,6 +284,12 @@ class HomePageState extends State { controller: _controller!, undo: false, ), + SelectHeaderStyleButton( + controller: _controller!, + axis: Axis.horizontal, + iconSize: toolbarIconSize, + attributes: const [Attribute.h1, Attribute.h2, Attribute.h3], + ), ToggleStyleButton( attribute: Attribute.bold, icon: Icons.format_bold, @@ -291,12 +341,12 @@ class HomePageState extends State { /// 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 _onImagePickCallback(File file) async { - final appDocDir = await getApplicationDocumentsDirectory(); - final copiedFile = await file.copy('${appDocDir.path}/${basename(file.path)}'); - return copiedFile.path.toString(); + 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. @@ -323,8 +373,12 @@ class HomePageState extends State { 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,); + 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 From 6482f78f42cae4e3bd780b8f15d2b392794088de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Mon, 25 Sep 2023 14:46:07 +0100 Subject: [PATCH 02/10] chore: Adding section on README of subheadings. #8 --- README.md | 128 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/README.md b/README.md index 445ba7f..86182be 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,8 @@ in `Flutter` in a few easy steps. - [5.2 Change the `_webImagePickImpl` callback function](#52-change-the-_webimagepickimpl-callback-function) - [5.3 Change the `_onImagePickCallback` callback function](#53-change-the-_onimagepickcallback-callback-function) - [6. Give the app a whirl](#6-give-the-app-a-whirl) + - [7. Extending our `toolbar`](#7-extending-our-toolbar) + - [7.1 Header font sizes](#71-header-font-sizes) - [A note about testing ๐Ÿงช](#a-note-about-testing-) - [Found this useful?](#found-this-useful) @@ -1769,6 +1771,132 @@ for this. > ![xcode](https://github.com/dwyl/flutter-wysiwyg-editor-tutorial/assets/17494745/49274c28-2e1a-4dca-9195-73160a6f936f) +## 7. Extending our `toolbar` + +As it stands, our toolbar offers limited options. +We want it to do more! +Let's add these features so the person using our app +is free to customize the text further ๐Ÿ˜Š. + + +### 7.1 Header font sizes + +Let's start by adding different **header font sizes**. +This will allow the person to better organize their items. +We will provide three different headers (`h1`, `h2` and `h3`), +each one with decreasing sizes and vertical spacings. + +These subheadings will be toggleable from the toolbar. + +Let's add the buttons to the toolbar. +Locate the `toolbar` variable (with type `QuillToolbar`) +inside the `_buildEditor()` function. + +In the `children` parameter, +add the [`SelectHeaderStyleButton`](https://github.com/singerdmx/flutter-quill/blob/09113cbc90117c7d9967ed865d132e832a219832/lib/src/widgets/toolbar/select_header_style_button.dart#L11) +after the `HistoryButton`s. +Like so: + +```dart +final toolbar = QuillToolbar( + afterButtonPressed: _focusNode.requestFocus, + children: [ + HistoryButton( + icon: Icons.undo_outlined, + iconSize: toolbarIconSize, + controller: _controller!, + undo: true, + ), + HistoryButton( + icon: Icons.redo_outlined, + iconSize: toolbarIconSize, + controller: _controller!, + undo: false, + ), + + // Add this button + SelectHeaderStyleButton( + controller: _controller!, + axis: Axis.horizontal, + iconSize: toolbarIconSize, + attributes: const [Attribute.h1, Attribute.h2, Attribute.h3], + ), + + // rest of the buttons + ] +) +``` + +With this button, we will be able to define the subheadings +we want to make available to the person. +The `axis` parameter defines whether they should be sorted +horizontally or vertically. +The `attributes` field defines how many subheadings we want to add. +In our case, we'll just define three. + +Now we need to **define the styling for the headings**. +For this, we need to change the `customStyles` field +of the `quillEditor` variable +(from the [`QuillEditor`](https://github.com/singerdmx/flutter-quill/blob/09113cbc90117c7d9967ed865d132e832a219832/lib/src/widgets/editor.dart#L149) class ) +inside `_buildEditor()`. + +We are going to make these changes to both +mobile and web `quillEditor` variables. +Locate them at check the `customStyles` field. +Change it to the following: + +```dart +customStyles: DefaultStyles( + // Change these ------------- + h1: DefaultTextBlockStyle( + const TextStyle( + fontSize: 32, + color: Colors.black, + height: 1.15, + fontWeight: FontWeight.w600, + ), + const VerticalSpacing(16, 0), + const VerticalSpacing(0, 0), + null, + ), + h2: DefaultTextBlockStyle( + const TextStyle( + fontSize: 24, + color: Colors.black87, + height: 1.15, + fontWeight: FontWeight.w600, + ), + const VerticalSpacing(8, 0), + const VerticalSpacing(0, 0), + null, + ), + h3: DefaultTextBlockStyle( + const TextStyle( + fontSize: 20, + color: Colors.black87, + height: 1.25, + fontWeight: FontWeight.w600, + ), + const VerticalSpacing(8, 0), + const VerticalSpacing(0, 0), + null, + ), + // Change these ------------- + // .... +), +``` + +We have changed the pre-existing `h1` field +and added the `h2` and `h3` fields as well, +specifying different font weights and sizes and colour +for each subheading. + +And that's it! +That's all you need to do to change the subheadings! + +Awesome job! ๐Ÿ‘ + + # A note about testing ๐Ÿงช We try to get tests covering 100% of the lines of code From de35068f6622b7a8fbbb4bf67d1cf69bfcb8f1b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Mon, 25 Sep 2023 19:50:06 +0100 Subject: [PATCH 03/10] feat: Installing emoji picker. #8 --- ios/Podfile.lock | 13 ++++ linux/flutter/generated_plugin_registrant.cc | 4 ++ linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 4 ++ pubspec.lock | 64 +++++++++++++++++++ pubspec.yaml | 1 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 8 files changed, 91 insertions(+) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 359c8c4..273685d 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -32,6 +32,8 @@ PODS: - DKPhotoGallery/Resource (0.0.17): - SDWebImage - SwiftyGif + - emoji_picker_flutter (0.0.1): + - Flutter - file_picker (0.0.1): - DKImagePickerController/PhotoGallery - Flutter @@ -60,6 +62,9 @@ PODS: - SDWebImage (5.16.0): - SDWebImage/Core (= 5.16.0) - SDWebImage/Core (5.16.0) + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS - SwiftyGif (5.4.4) - url_launcher_ios (0.0.1): - Flutter @@ -68,6 +73,7 @@ PODS: DEPENDENCIES: - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) + - emoji_picker_flutter (from `.symlinks/plugins/emoji_picker_flutter/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) - Flutter (from `Flutter`) - flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`) @@ -77,6 +83,7 @@ DEPENDENCIES: - integration_test (from `.symlinks/plugins/integration_test/ios`) - pasteboard (from `.symlinks/plugins/pasteboard/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`) @@ -91,6 +98,8 @@ SPEC REPOS: EXTERNAL SOURCES: device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" + emoji_picker_flutter: + :path: ".symlinks/plugins/emoji_picker_flutter/ios" file_picker: :path: ".symlinks/plugins/file_picker/ios" Flutter: @@ -109,6 +118,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/pasteboard/ios" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" video_player_avfoundation: @@ -118,6 +129,7 @@ SPEC CHECKSUMS: device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 + emoji_picker_flutter: df19dac03a2b39ac667dc8d1da939ef3a9e21347 file_picker: ce3938a0df3cc1ef404671531facef740d03f920 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721 @@ -129,6 +141,7 @@ SPEC CHECKSUMS: pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0 path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 SDWebImage: 2aea163b50bfcb569a2726b6a754c54a4506fcf6 + shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 video_player_avfoundation: 81e49bb3d9fb63dccf9fa0f6d877dc3ddbeac126 diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 158f759..d560677 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,11 +6,15 @@ #include "generated_plugin_registrant.h" +#include #include #include #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) emoji_picker_flutter_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "EmojiPickerFlutterPlugin"); + emoji_picker_flutter_plugin_register_with_registrar(emoji_picker_flutter_registrar); g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 93c755e..620300d 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + emoji_picker_flutter file_selector_linux pasteboard url_launcher_linux diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index a6432e6..27b7c55 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,15 +6,19 @@ import FlutterMacOS import Foundation import device_info_plus +import emoji_picker_flutter import file_selector_macos import pasteboard import path_provider_foundation +import shared_preferences_foundation import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) + EmojiPickerFlutterPlugin.register(with: registry.registrar(forPlugin: "EmojiPickerFlutterPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 6520309..5fe6bee 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -217,6 +217,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.1" + emoji_picker_flutter: + dependency: "direct main" + description: + name: emoji_picker_flutter + sha256: "1ca31245cc1f7ab5304c68ccda8039f52b9f2372aa4d10803117160fad3faf12" + url: "https://pub.dev" + source: hosted + version: "1.6.1" equatable: dependency: transitive description: @@ -876,6 +884,62 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + shared_preferences: + dependency: transitive + description: + name: shared_preferences + sha256: b7f41bad7e521d205998772545de63ff4e6c97714775902c199353f8bf1511ac + url: "https://pub.dev" + source: hosted + version: "2.2.1" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7" + url: "https://pub.dev" + source: hosted + version: "2.3.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: c2eb5bf57a2fe9ad6988121609e47d3e07bb3bdca5b6f8444e4cf302428a128a + url: "https://pub.dev" + source: hosted + version: "2.3.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a + url: "https://pub.dev" + source: hosted + version: "2.3.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf + url: "https://pub.dev" + source: hosted + version: "2.2.1" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: f763a101313bd3be87edffe0560037500967de9c394a714cd598d945517f694f + url: "https://pub.dev" + source: hosted + version: "2.3.1" shelf: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5cd4d62..ee3f04f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,6 +42,7 @@ dependencies: http: ^0.13.6 mime: ^1.0.4 http_parser: ^4.0.2 + emoji_picker_flutter: ^1.6.1 dev_dependencies: integration_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 7d98a88..e94be21 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,11 +6,14 @@ #include "generated_plugin_registrant.h" +#include #include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + EmojiPickerFlutterPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("EmojiPickerFlutterPluginCApi")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); PasteboardPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 722afbe..0231913 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + emoji_picker_flutter file_selector_windows pasteboard url_launcher_windows From d7e16c8d89d1badab11d881a294ed926b7642a5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Tue, 26 Sep 2023 11:16:58 +0100 Subject: [PATCH 04/10] feat: Add emoji button. #8 --- lib/home_page.dart | 87 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/lib/home_page.dart b/lib/home_page.dart index cb31ea8..ff379f5 100644 --- a/lib/home_page.dart +++ b/lib/home_page.dart @@ -6,6 +6,7 @@ import 'dart:ui'; import 'package:app/main.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_quill/extensions.dart'; import 'package:flutter_quill/flutter_quill.dart' hide Text; import 'package:flutter_quill_extensions/flutter_quill_extensions.dart'; @@ -14,6 +15,9 @@ 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'; +import 'package:flutter/foundation.dart' as foundation; +import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; +import 'package:responsive_framework/responsive_framework.dart'; import 'web_embeds/web_embeds.dart'; @@ -48,6 +52,9 @@ class HomePageState extends State { /// Selection types for triple clicking _SelectionType _selectionType = _SelectionType.none; + /// Show emoji picker + bool _offstageEmojiPickerOffstage = true; + @override void initState() { super.initState(); @@ -128,6 +135,54 @@ class HomePageState extends State { return false; } + /// Callback called whenever the person taps on the emoji button in the toolbar. + /// It shows/hides the emoji picker and focus/unfocusses the keyboard accordingly. + void _onEmojiButtonPressed(BuildContext context) { + final isEmojiPickerShown = !_offstageEmojiPickerOffstage; + + // If emoji picker is being shown, we show the keyboard and hide the emoji picker. + if (isEmojiPickerShown) { + _focusNode.requestFocus(); + setState(() { + _offstageEmojiPickerOffstage = true; + }); + } + + // Otherwise, we do the inverse. + else { + // Unfocusing when the person clicks away. This is to hide the keyboard. + // See https://flutterigniter.com/dismiss-keyboard-form-lose-focus/ + // and https://www.youtube.com/watch?v=MKrEJtheGPk&t=40s&ab_channel=HeyFlutter%E2%80%A4com. + final currentFocus = FocusScope.of(context); + if (!currentFocus.hasPrimaryFocus) { + SystemChannels.textInput.invokeMethod('TextInput.hide'); + //currentFocus.unfocus(); + } + + setState(() { + _offstageEmojiPickerOffstage = false; + }); + } + } + + + /// Returns the emoji picker configuration according to screen size. + Config _buildEmojiPickerConfig(BuildContext context) { + if (ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE)) { + return const Config(emojiSizeMax: 32.0, columns: 7); + } + + if (ResponsiveBreakpoints.of(context).equals(TABLET)) { + return const Config(emojiSizeMax: 24.0, columns: 10); + } + + if (ResponsiveBreakpoints.of(context).equals(DESKTOP)) { + return const Config(emojiSizeMax: 16.0, columns: 15); + } + + return const Config(emojiSizeMax: 16.0, columns: 30); + } + /// Build the `flutter-quill` editor to be shown on screen. Widget _buildEditor(BuildContext context) { // Default editor (for mobile devices) @@ -142,6 +197,14 @@ class HomePageState extends State { enableSelectionToolbar: isMobile(), expands: false, padding: EdgeInsets.zero, + onTapDown: (details, p1) { + // When the person taps on the text, we want to hide the emoji picker + // so only the keyboard is shown + setState(() { + _offstageEmojiPickerOffstage = true; + }); + return false; + }, onTapUp: (details, p1) { return _onTripleClickSelection(); }, @@ -272,6 +335,11 @@ class HomePageState extends State { final toolbar = QuillToolbar( afterButtonPressed: _focusNode.requestFocus, children: [ + CustomButton( + onPressed: () => _onEmojiButtonPressed(context), + icon: Icons.emoji_emotions, + iconSize: toolbarIconSize, + ), HistoryButton( icon: Icons.undo_outlined, iconSize: toolbarIconSize, @@ -333,6 +401,25 @@ class HomePageState extends State { ), ), Container(child: toolbar), + Offstage( + offstage: _offstageEmojiPickerOffstage, + child: SizedBox( + height: 250, + child: EmojiPicker( + onEmojiSelected: (category, emoji) { + if (_controller != null) { + // Get pointer selection and insert emoji there + final selection = _controller?.selection; + _controller?.document.insert(selection!.end, emoji.emoji); + + // Update the pointer after the emoji we've just inserted + _controller?.updateSelection(TextSelection.collapsed(offset: selection!.end + emoji.emoji.length), ChangeSource.REMOTE); + } + }, + config: _buildEmojiPickerConfig(context), + ), + ), + ), ], ), ); From 7c0ab2ceeb645eda34af9bed2c8ed2e4c2ea98a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Tue, 26 Sep 2023 11:59:05 +0100 Subject: [PATCH 05/10] chore: Refactoring emoji picker. #8 --- lib/emoji_picker_widget.dart | 63 ++++++++++++++++++++++++++++++++++++ lib/home_page.dart | 40 +++-------------------- 2 files changed, 67 insertions(+), 36 deletions(-) create mode 100644 lib/emoji_picker_widget.dart diff --git a/lib/emoji_picker_widget.dart b/lib/emoji_picker_widget.dart new file mode 100644 index 0000000..be218b1 --- /dev/null +++ b/lib/emoji_picker_widget.dart @@ -0,0 +1,63 @@ +import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_quill/flutter_quill.dart'; +import 'package:responsive_framework/responsive_framework.dart'; + +/// Emoji picker widget that is offstage. +/// Shows an emoji picker when [offstageEmojiPickerOffstage] is `false`. +class OffstageEmojiPicker extends StatefulWidget { + + /// `QuillController` controller that is passed so the controller document is changed when emojis are inserted. + final QuillController? quillController; + + /// Determines if the emoji picker is offstage or not. + final bool offstageEmojiPickerOffstage; + + const OffstageEmojiPicker({required this.offstageEmojiPickerOffstage, this.quillController, super.key}); + + @override + State createState() => _OffstageEmojiPickerState(); +} + +class _OffstageEmojiPickerState extends State { + /// Returns the emoji picker configuration according to screen size. + Config _buildEmojiPickerConfig(BuildContext context) { + if (ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE)) { + return const Config(emojiSizeMax: 32.0, columns: 7); + } + + if (ResponsiveBreakpoints.of(context).equals(TABLET)) { + return const Config(emojiSizeMax: 24.0, columns: 10); + } + + if (ResponsiveBreakpoints.of(context).equals(DESKTOP)) { + return const Config(emojiSizeMax: 16.0, columns: 15); + } + + return const Config(emojiSizeMax: 16.0, columns: 30); + } + + @override + Widget build(BuildContext context) { + return Offstage( + offstage: widget.offstageEmojiPickerOffstage, + child: SizedBox( + height: 250, + child: EmojiPicker( + onEmojiSelected: (category, emoji) { + if (widget.quillController != null) { + // Get pointer selection and insert emoji there + final selection = widget.quillController?.selection; + widget.quillController?.document.insert(selection!.end, emoji.emoji); + + // Update the pointer after the emoji we've just inserted + widget.quillController?.updateSelection(TextSelection.collapsed(offset: selection!.end + emoji.emoji.length), ChangeSource.REMOTE); + } + }, + config: _buildEmojiPickerConfig(context), + ), + ), + ); + } +} diff --git a/lib/home_page.dart b/lib/home_page.dart index ff379f5..e3064e1 100644 --- a/lib/home_page.dart +++ b/lib/home_page.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:io'; import 'dart:ui'; +import 'package:app/emoji_picker_widget.dart'; import 'package:app/main.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; @@ -165,24 +166,6 @@ class HomePageState extends State { } } - - /// Returns the emoji picker configuration according to screen size. - Config _buildEmojiPickerConfig(BuildContext context) { - if (ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE)) { - return const Config(emojiSizeMax: 32.0, columns: 7); - } - - if (ResponsiveBreakpoints.of(context).equals(TABLET)) { - return const Config(emojiSizeMax: 24.0, columns: 10); - } - - if (ResponsiveBreakpoints.of(context).equals(DESKTOP)) { - return const Config(emojiSizeMax: 16.0, columns: 15); - } - - return const Config(emojiSizeMax: 16.0, columns: 30); - } - /// Build the `flutter-quill` editor to be shown on screen. Widget _buildEditor(BuildContext context) { // Default editor (for mobile devices) @@ -401,24 +384,9 @@ class HomePageState extends State { ), ), Container(child: toolbar), - Offstage( - offstage: _offstageEmojiPickerOffstage, - child: SizedBox( - height: 250, - child: EmojiPicker( - onEmojiSelected: (category, emoji) { - if (_controller != null) { - // Get pointer selection and insert emoji there - final selection = _controller?.selection; - _controller?.document.insert(selection!.end, emoji.emoji); - - // Update the pointer after the emoji we've just inserted - _controller?.updateSelection(TextSelection.collapsed(offset: selection!.end + emoji.emoji.length), ChangeSource.REMOTE); - } - }, - config: _buildEmojiPickerConfig(context), - ), - ), + OffstageEmojiPicker( + offstageEmojiPickerOffstage: _offstageEmojiPickerOffstage, + quillController: _controller, ), ], ), From 2c9342fb865cb85244becc1efe7ede1589f1006f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Tue, 26 Sep 2023 12:43:58 +0100 Subject: [PATCH 06/10] feat: Finishing README and slight refactor on emoji pick. #8 --- README.md | 256 +++++++++++++++++++++++++++++++++++ lib/emoji_picker_widget.dart | 10 +- lib/home_page.dart | 2 +- 3 files changed, 261 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 86182be..3168d22 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ in `Flutter` in a few easy steps. - [6. Give the app a whirl](#6-give-the-app-a-whirl) - [7. Extending our `toolbar`](#7-extending-our-toolbar) - [7.1 Header font sizes](#71-header-font-sizes) + - [7.2 Adding emojis](#72-adding-emojis) - [A note about testing ๐Ÿงช](#a-note-about-testing-) - [Found this useful?](#found-this-useful) @@ -1897,6 +1898,261 @@ That's all you need to do to change the subheadings! Awesome job! ๐Ÿ‘ +### 7.2 Adding emojis + +Let's add a button that will allow people to add emojis! +This is useful for both mobile and web platforms +(it's more relevant on the latter, +as there is not a native emoji keyboard to choose from). + +You might be wondering that, for mobile applications, +having a dedicated button to insert emojis is *redudant*, +because iOS and Android devices offer a native keyboard +in which you can select an emoji and insert it as text. + +However, we're doing this for two purposes: +- the emoji button is meant to be introduced as a separate feature +and as a custom button to be shown. +See https://github.com/dwyl/app/issues/275#issuecomment-1646862277 for more context. +- showing the native keyboard emoji selection does not work on all platforms in Flutter. +If this was the case, we could have easily used a package +like [`keyboard_emoji_picker`](https://pub.dev/packages/keyboard_emoji_picker). + +So let's do this! + +First, let's install the package we'll use +to select emojis. +Simply run `flutter pub add emoji_picker_flutter` +and all the dependencies will be installed. + +Now that's done with, let's start by creating our emoji picker. +Let's first create our widget in a separate file. +Inside `lib`, +create a file called `emoji_picker_widget.dart`. + +```dart +import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_quill/flutter_quill.dart'; +import 'package:responsive_framework/responsive_framework.dart'; + +/// Emoji picker widget that is offstage. +/// Shows an emoji picker when [offstageEmojiPicker] is `false`. +class OffstageEmojiPicker extends StatefulWidget { + /// `QuillController` controller that is passed so the controller document is changed when emojis are inserted. + final QuillController? quillController; + + /// Determines if the emoji picker is offstage or not. + final bool offstageEmojiPicker; + + const OffstageEmojiPicker({required this.offstageEmojiPicker, this.quillController, super.key}); + + @override + State createState() => _OffstageEmojiPickerState(); +} + +class _OffstageEmojiPickerState extends State { + /// Returns the emoji picker configuration according to screen size. + Config _buildEmojiPickerConfig(BuildContext context) { + if (ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE)) { + return const Config(emojiSizeMax: 32.0, columns: 7); + } + + if (ResponsiveBreakpoints.of(context).equals(TABLET)) { + return const Config(emojiSizeMax: 24.0, columns: 10); + } + + if (ResponsiveBreakpoints.of(context).equals(DESKTOP)) { + return const Config(emojiSizeMax: 16.0, columns: 15); + } + + return const Config(emojiSizeMax: 16.0, columns: 30); + } + + @override + Widget build(BuildContext context) { + return Offstage( + offstage: widget.offstageEmojiPicker, + child: SizedBox( + height: 250, + child: EmojiPicker( + onEmojiSelected: (category, emoji) { + if (widget.quillController != null) { + // Get pointer selection and insert emoji there + final selection = widget.quillController?.selection; + widget.quillController?.document.insert(selection!.end, emoji.emoji); + + // Update the pointer after the emoji we've just inserted + widget.quillController?.updateSelection(TextSelection.collapsed(offset: selection!.end + emoji.emoji.length), ChangeSource.REMOTE); + } + }, + config: _buildEmojiPickerConfig(context), + ), + ), + ); + } +} +``` + +Let's unpack what we've just implemented. +The widget we've create is a **stateful widget** +that receives two parameters: + +- `quillController` pertains to the `QuillController` +object related ot the editor. +This controller is used to access the document +so the emoji can be inserted. +- `offstageEmojiPicker` is a boolean +that determines if the widget is meant to be offstage (hidden) +or not. + +In the `build()` function, +we use the [`Offstage`](https://api.flutter.dev/flutter/widgets/Offstage-class.html) +class to wrap the widget. +This will make it possible to show and hide the emoji picker accordingly. + +We then use the `EmojiPicker` widget +from the package we've just downloaded. +In this widget, we define two parameters: + +- `config`, pertaining to the emoji picker configuration. +We use the `_buildEmojiPickerConfig()` function +to conditionally change the emoji picker dimensions +according to the size of the screen. +- `onEmojiSelected`, which is called after an emoji is selected by the person. +In this, we use the passed `quillController` to get the position +of the pointer and the document. +With these two, we add the selected emoji +and update the pointer to *after* the emoji that was inserted. +This will allow adding consecutive emojis properly +and maintain the pointer index aligned. + +Now all that's left is to +**use our newly created widget** in our homepage! +Head over to `lib/home_page.dart`, +and add a new field inside `HomePageState`. + +```dart + /// Show emoji picker + bool _offstageEmojiPickerOffstage = true; +``` + +In the same class, +we're going to create a callback function +that is to be called every time the person +clicks on the emoji toolbar button +(don't worry, we'll create this button in a minute). +This function will close the keyboard +and open the emoji picker widget we've just created. + +```dart + void _onEmojiButtonPressed(BuildContext context) { + final isEmojiPickerShown = !_offstageEmojiPickerOffstage; + + // If emoji picker is being shown, we show the keyboard and hide the emoji picker. + if (isEmojiPickerShown) { + _focusNode.requestFocus(); + setState(() { + _offstageEmojiPickerOffstage = true; + }); + } + + // Otherwise, we do the inverse. + else { + // Unfocusing when the person clicks away. This is to hide the keyboard. + // See https://flutterigniter.com/dismiss-keyboard-form-lose-focus/ + // and https://www.youtube.com/watch?v=MKrEJtheGPk&t=40s&ab_channel=HeyFlutter%E2%80%A4com. + final currentFocus = FocusScope.of(context); + if (!currentFocus.hasPrimaryFocus) { + SystemChannels.textInput.invokeMethod('TextInput.hide'); + } + + setState(() { + _offstageEmojiPickerOffstage = false; + }); + } + } +``` + +We are toggling the `_offstageEmojiPickerOffstage` field +by calling `setState()`, thus causing a re-render +and properly toggling the emoji picker. + +Now all we need to do is +**add the button to the toolbar to toggle the emoji picker** +and **add the offstage emoji picker to the widget tree**. + +Let's do the first one. +Locate `_buildEditor` and find the `toolbar` +(class `QuillToolbar`) definition. +In the `children` parameter, +we're going to add a +[`CustomButton`](https://github.com/singerdmx/flutter-quill/blob/09113cbc90117c7d9967ed865d132e832a219832/lib/src/widgets/toolbar/custom_button.dart#L6) +to these buttons. + +```dart +final toolbar = QuillToolbar( + afterButtonPressed: _focusNode.requestFocus, + children: [ + CustomButton( + onPressed: () => _onEmojiButtonPressed(context), + icon: Icons.emoji_emotions, + iconSize: toolbarIconSize, + ), + + // Other buttons... + ] +) +``` + +As you can see, we are calling the `_onEmojiButtonPressed` function +we've implemented every time the person taps on the emoji button. + +At the end of the function, we're going to return +the editor with the `OffstageEmojiPicker` widget +we've initially created. + +```dart + return SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + flex: 15, + child: Container( + key: quillEditorKey, + color: Colors.white, + padding: const EdgeInsets.only(left: 16, right: 16), + child: quillEditor, + ), + ), + Container(child: toolbar), + + // Add this --- + OffstageEmojiPicker( + offstageEmojiPicker: _offstageEmojiPickerOffstage, + quillController: _controller, + ), + ], + ), + ); +``` + +And that's it! +We've just successfully added an emoji picker +that is correctly toggled when clicking the appropriate button in the toolbar, +*and* adding the correct changes to the Delta document of the Quill editor. + + +

+ +

+ + + + + + # A note about testing ๐Ÿงช We try to get tests covering 100% of the lines of code diff --git a/lib/emoji_picker_widget.dart b/lib/emoji_picker_widget.dart index be218b1..d9472c6 100644 --- a/lib/emoji_picker_widget.dart +++ b/lib/emoji_picker_widget.dart @@ -1,20 +1,18 @@ import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_quill/flutter_quill.dart'; import 'package:responsive_framework/responsive_framework.dart'; /// Emoji picker widget that is offstage. -/// Shows an emoji picker when [offstageEmojiPickerOffstage] is `false`. +/// Shows an emoji picker when [offstageEmojiPicker] is `false`. class OffstageEmojiPicker extends StatefulWidget { - /// `QuillController` controller that is passed so the controller document is changed when emojis are inserted. final QuillController? quillController; /// Determines if the emoji picker is offstage or not. - final bool offstageEmojiPickerOffstage; + final bool offstageEmojiPicker; - const OffstageEmojiPicker({required this.offstageEmojiPickerOffstage, this.quillController, super.key}); + const OffstageEmojiPicker({required this.offstageEmojiPicker, this.quillController, super.key}); @override State createState() => _OffstageEmojiPickerState(); @@ -41,7 +39,7 @@ class _OffstageEmojiPickerState extends State { @override Widget build(BuildContext context) { return Offstage( - offstage: widget.offstageEmojiPickerOffstage, + offstage: widget.offstageEmojiPicker, child: SizedBox( height: 250, child: EmojiPicker( diff --git a/lib/home_page.dart b/lib/home_page.dart index e3064e1..c03f17a 100644 --- a/lib/home_page.dart +++ b/lib/home_page.dart @@ -385,7 +385,7 @@ class HomePageState extends State { ), Container(child: toolbar), OffstageEmojiPicker( - offstageEmojiPickerOffstage: _offstageEmojiPickerOffstage, + offstageEmojiPicker: _offstageEmojiPickerOffstage, quillController: _controller, ), ], From c66215834b5a0c03018a51036a8171c2c23eb8d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Tue, 26 Sep 2023 16:07:31 +0100 Subject: [PATCH 07/10] chore: Adding emoji test (and integration test for comparison). #8 --- integration_test/emoji_widget_test.dart | 73 +++++++++++++++++++++++++ lib/emoji_picker_widget.dart | 3 + lib/home_page.dart | 6 +- test/emoji_widget_test.dart | 72 ++++++++++++++++++++++++ 4 files changed, 151 insertions(+), 3 deletions(-) create mode 100644 integration_test/emoji_widget_test.dart create mode 100644 test/emoji_widget_test.dart diff --git a/integration_test/emoji_widget_test.dart b/integration_test/emoji_widget_test.dart new file mode 100644 index 0000000..9aedc30 --- /dev/null +++ b/integration_test/emoji_widget_test.dart @@ -0,0 +1,73 @@ +import 'package:app/emoji_picker_widget.dart'; +import 'package:app/home_page.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_quill/flutter_quill.dart'; +import 'package:flutter_quill_extensions/embeds/toolbar/image_button.dart'; +import 'package:flutter_quill/flutter_quill_test.dart'; + +import 'package:app/main.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'package:flutter/services.dart'; + +// importing mocks +import '../test/widget_test.mocks.dart'; + +/// Run `flutter test integration_test` with a device connected to see it running on the real device. + +@GenerateMocks([PlatformService]) +void main() { + /// Check for context: https://stackoverflow.com/questions/60671728/unable-to-load-assets-in-flutter-tests + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + testWidgets('Click on emoji button should show the emoji picker', (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 and emoji picker not being shown + expect(find.text('Flutter Quill'), findsOneWidget); + expect(find.byKey(emojiPickerWidgetKey).hitTestable(), findsNothing); + + // Click on emoji button should show the emoji picker + var emojiIcon = find.byIcon(Icons.emoji_emotions); + + await tester.tap(emojiIcon); + await tester.pumpAndSettle(); + + emojiIcon = find.byIcon(Icons.emoji_emotions); + var emojiPicker = find.byKey(emojiButtonKey); + + // Expect the emoji picker being shown + expect(emojiPicker.hitTestable(), findsOneWidget); + + // Tap on smile category + await tester.tapAt(const Offset(61, 580)); + await tester.pumpAndSettle(); + + // Tap on smile icon + await tester.tapAt(const Offset(14, 632)); + await tester.pumpAndSettle(); + + // Tap on emoji icon to close the emoji pickers + emojiIcon = find.byIcon(Icons.emoji_emotions); + + await tester.tap(emojiIcon); + await tester.pumpAndSettle(); + + expect(find.byKey(emojiPickerWidgetKey).hitTestable(), findsNothing); + //expect(find.text('๐Ÿ˜€'), findsOneWidget); + }); +} diff --git a/lib/emoji_picker_widget.dart b/lib/emoji_picker_widget.dart index d9472c6..45b71e1 100644 --- a/lib/emoji_picker_widget.dart +++ b/lib/emoji_picker_widget.dart @@ -3,6 +3,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_quill/flutter_quill.dart'; import 'package:responsive_framework/responsive_framework.dart'; +const emojiPickerWidgetKey = Key('emojiPickerWidgetKey'); + /// Emoji picker widget that is offstage. /// Shows an emoji picker when [offstageEmojiPicker] is `false`. class OffstageEmojiPicker extends StatefulWidget { @@ -43,6 +45,7 @@ class _OffstageEmojiPickerState extends State { child: SizedBox( height: 250, child: EmojiPicker( + key: emojiPickerWidgetKey, onEmojiSelected: (category, emoji) { if (widget.quillController != null) { // Get pointer selection and insert emoji there diff --git a/lib/home_page.dart b/lib/home_page.dart index c03f17a..756fe06 100644 --- a/lib/home_page.dart +++ b/lib/home_page.dart @@ -16,13 +16,12 @@ 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'; -import 'package:flutter/foundation.dart' as foundation; -import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; -import 'package:responsive_framework/responsive_framework.dart'; import 'web_embeds/web_embeds.dart'; const quillEditorKey = Key('quillEditorKey'); +const emojiButtonKey = Key('emojiButtonKey'); + /// Types of selection that person can make when triple clicking enum _SelectionType { @@ -319,6 +318,7 @@ class HomePageState extends State { afterButtonPressed: _focusNode.requestFocus, children: [ CustomButton( + key: emojiButtonKey, onPressed: () => _onEmojiButtonPressed(context), icon: Icons.emoji_emotions, iconSize: toolbarIconSize, diff --git a/test/emoji_widget_test.dart b/test/emoji_widget_test.dart new file mode 100644 index 0000000..96ad403 --- /dev/null +++ b/test/emoji_widget_test.dart @@ -0,0 +1,72 @@ +import 'package:app/emoji_picker_widget.dart'; +import 'package:app/home_page.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_quill/flutter_quill.dart'; +import 'package:flutter_quill_extensions/embeds/toolbar/image_button.dart'; +import 'package:flutter_quill/flutter_quill_test.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'; + +@GenerateMocks([PlatformService]) +void main() { + /// Check for context: https://stackoverflow.com/questions/60671728/unable-to-load-assets-in-flutter-tests + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + testWidgets('Click on emoji button should show the emoji picker', (WidgetTester tester) async { + // Set size because it's needed to correctly tap on emoji picker + await tester.binding.setSurfaceSize(const Size(380, 800)); + + 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 and emoji picker not being shown + expect(find.text('Flutter Quill'), findsOneWidget); + expect(find.byKey(emojiPickerWidgetKey).hitTestable(), findsNothing); + + // Click on emoji button should show the emoji picker + var emojiIcon = find.byIcon(Icons.emoji_emotions); + + await tester.tap(emojiIcon); + await tester.pumpAndSettle(); + + emojiIcon = find.byIcon(Icons.emoji_emotions); + + // Expect the emoji picker being shown + expect(find.byKey(emojiButtonKey).hitTestable(), findsOneWidget); + + // Tap on smile category + await tester.tapAt(const Offset(61, 580)); + await tester.pumpAndSettle(); + + // Tap on smile icon + await tester.tapAt(const Offset(14, 632)); + await tester.pumpAndSettle(); + + // Tap on emoji icon to close the emoji pickers + emojiIcon = find.byIcon(Icons.emoji_emotions); + + await tester.tap(emojiIcon); + await tester.pumpAndSettle(); + + expect(find.byKey(emojiPickerWidgetKey).hitTestable(), findsNothing); + }); +} From 3a1caf4c518d01b28d52def9914272f9f12a1b63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Tue, 26 Sep 2023 16:26:30 +0100 Subject: [PATCH 08/10] feat: Add link. #8 --- lib/home_page.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/home_page.dart b/lib/home_page.dart index 756fe06..addde18 100644 --- a/lib/home_page.dart +++ b/lib/home_page.dart @@ -365,6 +365,11 @@ class HomePageState extends State { iconSize: toolbarIconSize, controller: _controller!, ), + LinkStyleButton( + controller: _controller!, + iconSize: toolbarIconSize, + linkRegExp: RegExp(r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+'), + ), for (final builder in embedButtons) builder(_controller!, toolbarIconSize, null, null), ], ); From 10ac0ca6b415c977ccffb35d325e878d706de8d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Tue, 26 Sep 2023 16:37:31 +0100 Subject: [PATCH 09/10] chore: Add section to README. #8 --- README.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/README.md b/README.md index 3168d22..4f9d1e5 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ in `Flutter` in a few easy steps. - [7. Extending our `toolbar`](#7-extending-our-toolbar) - [7.1 Header font sizes](#71-header-font-sizes) - [7.2 Adding emojis](#72-adding-emojis) + - [7.3 Adding embeddable links](#73-adding-embeddable-links) - [A note about testing ๐Ÿงช](#a-note-about-testing-) - [Found this useful?](#found-this-useful) @@ -2149,8 +2150,42 @@ that is correctly toggled when clicking the appropriate button in the toolbar,

+### 7.3 Adding embeddable links +This one's the easiest. +`flutter-quill` already provides a specific button +which we can invoke that'll do all the work for us, +including formatting, embedding the link +and properly adding the change to the controller's document. +Simply add the following snippet of code +to the `children` field of the `toolbar` +variable you've worked with earlier. + +```dart + // .... + ToggleStyleButton( + attribute: Attribute.strikeThrough, + icon: Icons.format_strikethrough, + iconSize: toolbarIconSize, + controller: _controller!, + ), + + // Add this button + LinkStyleButton( + controller: _controller!, + iconSize: toolbarIconSize, + linkRegExp: RegExp(r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+'), + ), + + for (final builder in embedButtons) builder(_controller!, toolbarIconSize, null, null), +``` + +And that's it! +We're using the +[`LinkStyleButton`](https://github.com/singerdmx/flutter-quill/blob/09113cbc90117c7d9967ed865d132e832a219832/lib/src/widgets/toolbar/link_style_button.dart#L13) +class with a regular expression that we've defined ourselves +that will only allow a link to be added if it's valid. # A note about testing ๐Ÿงช From 5b30f58c9ca52a47b116436bed1d2fe6bd1482d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Tue, 26 Sep 2023 18:53:00 +0100 Subject: [PATCH 10/10] fix: Run format. #8 --- integration_test/emoji_widget_test.dart | 6 +----- test/emoji_widget_test.dart | 4 ---- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/integration_test/emoji_widget_test.dart b/integration_test/emoji_widget_test.dart index 9aedc30..e537f53 100644 --- a/integration_test/emoji_widget_test.dart +++ b/integration_test/emoji_widget_test.dart @@ -2,15 +2,11 @@ import 'package:app/emoji_picker_widget.dart'; import 'package:app/home_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_quill/flutter_quill.dart'; -import 'package:flutter_quill_extensions/embeds/toolbar/image_button.dart'; -import 'package:flutter_quill/flutter_quill_test.dart'; import 'package:app/main.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; -import 'package:flutter/services.dart'; // importing mocks import '../test/widget_test.mocks.dart'; @@ -48,7 +44,7 @@ void main() { await tester.pumpAndSettle(); emojiIcon = find.byIcon(Icons.emoji_emotions); - var emojiPicker = find.byKey(emojiButtonKey); + final emojiPicker = find.byKey(emojiButtonKey); // Expect the emoji picker being shown expect(emojiPicker.hitTestable(), findsOneWidget); diff --git a/test/emoji_widget_test.dart b/test/emoji_widget_test.dart index 96ad403..a314e8b 100644 --- a/test/emoji_widget_test.dart +++ b/test/emoji_widget_test.dart @@ -2,15 +2,11 @@ import 'package:app/emoji_picker_widget.dart'; import 'package:app/home_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_quill/flutter_quill.dart'; -import 'package:flutter_quill_extensions/embeds/toolbar/image_button.dart'; -import 'package:flutter_quill/flutter_quill_test.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';