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.
> 
+## 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';