diff --git a/example/lib/presentation/quill/embeds/timestamp_embed.dart b/example/lib/presentation/quill/embeds/timestamp_embed.dart new file mode 100644 index 00000000..bffe16c6 --- /dev/null +++ b/example/lib/presentation/quill/embeds/timestamp_embed.dart @@ -0,0 +1,45 @@ +import 'dart:convert' show jsonDecode, jsonEncode; + +import 'package:flutter/material.dart' show Icons; +import 'package:flutter/widgets.dart'; +import 'package:flutter_quill/flutter_quill.dart'; + +class TimeStampEmbed extends Embeddable { + const TimeStampEmbed( + String value, + ) : super(timeStampType, value); + + static const String timeStampType = 'timeStamp'; + + static TimeStampEmbed fromDocument(Document document) => + TimeStampEmbed(jsonEncode(document.toDelta().toJson())); + + Document get document => Document.fromJson(jsonDecode(data)); +} + +class TimeStampEmbedBuilderWidget extends EmbedBuilder { + @override + String get key => 'timeStamp'; + + @override + String toPlainText(Embed node) { + return node.value.data; + } + + @override + Widget build( + BuildContext context, + QuillController controller, + Embed node, + bool readOnly, + bool inline, + TextStyle textStyle, + ) { + return Row( + children: [ + const Icon(Icons.access_time_rounded), + Text(node.value.data as String), + ], + ); + } +} diff --git a/example/lib/presentation/quill/quill_editor.dart b/example/lib/presentation/quill/quill_editor.dart index a4a74a8f..e7f67732 100644 --- a/example/lib/presentation/quill/quill_editor.dart +++ b/example/lib/presentation/quill/quill_editor.dart @@ -1,3 +1,5 @@ +import 'dart:io' as io show Directory, File; + import 'package:cached_network_image/cached_network_image.dart' show CachedNetworkImageProvider; import 'package:desktop_drop/desktop_drop.dart' show DropTarget; @@ -7,8 +9,10 @@ import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_quill_extensions/flutter_quill_extensions.dart'; import 'package:flutter_quill_extensions/presentation/embeds/widgets/image.dart' show getImageProviderByImageSource, imageFileExtensions; +import 'package:path/path.dart' as path; import '../extensions/scaffold_messenger.dart'; +import 'embeds/timestamp_embed.dart'; class MyQuillEditor extends StatelessWidget { const MyQuillEditor({ @@ -31,39 +35,57 @@ class MyQuillEditor extends StatelessWidget { scrollable: true, placeholder: 'Start writting your notes...', padding: const EdgeInsets.all(16), - embedBuilders: isWeb() - ? FlutterQuillEmbeds.editorWebBuilders() - : FlutterQuillEmbeds.editorBuilders( - imageEmbedConfigurations: QuillEditorImageEmbedConfigurations( - imageErrorWidgetBuilder: (context, error, stackTrace) { - return Text( - 'Error while loading an image: ${error.toString()}', - ); - }, - imageProviderBuilder: (imageUrl) { - // cached_network_image is supported - // only for Android, iOS and web + onImagePaste: (imageBytes) async { + if (isWeb()) { + return null; + } + // We will save it to system temporary files + final newFileName = '${DateTime.now().toIso8601String()}.png'; + final newPath = path.join( + io.Directory.systemTemp.path, + newFileName, + ); + final file = await io.File( + newPath, + ).writeAsBytes(imageBytes, flush: true); + return file.path; + }, + embedBuilders: [ + ...(isWeb() + ? FlutterQuillEmbeds.editorWebBuilders() + : FlutterQuillEmbeds.editorBuilders( + imageEmbedConfigurations: QuillEditorImageEmbedConfigurations( + imageErrorWidgetBuilder: (context, error, stackTrace) { + return Text( + 'Error while loading an image: ${error.toString()}', + ); + }, + imageProviderBuilder: (imageUrl) { + // cached_network_image is supported + // only for Android, iOS and web - // We will use it only if image from network - if (isAndroid(supportWeb: false) || - isIOS(supportWeb: false) || - isWeb()) { - if (isHttpBasedUrl(imageUrl)) { - return CachedNetworkImageProvider( - imageUrl, - ); + // We will use it only if image from network + if (isAndroid(supportWeb: false) || + isIOS(supportWeb: false) || + isWeb()) { + if (isHttpBasedUrl(imageUrl)) { + return CachedNetworkImageProvider( + imageUrl, + ); + } } - } - return getImageProviderByImageSource( - imageUrl, - imageProviderBuilder: null, - assetsPrefix: QuillSharedExtensionsConfigurations.get( - context: context) - .assetsPrefix, - ); - }, - ), - ), + return getImageProviderByImageSource( + imageUrl, + imageProviderBuilder: null, + assetsPrefix: QuillSharedExtensionsConfigurations.get( + context: context) + .assetsPrefix, + ); + }, + ), + )), + TimeStampEmbedBuilderWidget(), + ], builder: (context, rawEditor) { // The `desktop_drop` plugin doesn't support iOS platform for now if (isIOS(supportWeb: false)) { diff --git a/example/lib/presentation/quill/quill_toolbar.dart b/example/lib/presentation/quill/quill_toolbar.dart index 460771d8..a0f16677 100644 --- a/example/lib/presentation/quill/quill_toolbar.dart +++ b/example/lib/presentation/quill/quill_toolbar.dart @@ -12,6 +12,7 @@ import 'package:path_provider/path_provider.dart' import '../extensions/scaffold_messenger.dart'; import '../settings/cubit/settings_cubit.dart'; +import 'embeds/timestamp_embed.dart'; class MyQuillToolbar extends StatelessWidget { const MyQuillToolbar({super.key}); @@ -67,13 +68,19 @@ class MyQuillToolbar extends StatelessWidget { controller.insertImageBlock(imageSource: newSavedImage); } - /// Copies the picked file from temporary cache to applications directory + /// For mobile platforms it will copies the picked file from temporary cache + /// to applications directory + /// + /// for desktop platforms, it will do the same but from user files this time Future saveImage(io.File file) async { final appDocDir = await getApplicationDocumentsDirectory(); - final copiedFile = await file.copy(path.join( + final fileExt = path.extension(file.path); + final newFileName = '${DateTime.now().toIso8601String()}$fileExt'; + final newPath = path.join( appDocDir.path, - '${DateTime.now().toIso8601String()}${path.extension(file.path)}', - )); + newFileName, + ); + final copiedFile = await file.copy(newPath); return copiedFile.path; } @@ -205,6 +212,52 @@ class MyQuillToolbar extends StatelessWidget { return QuillToolbar( configurations: QuillToolbarConfigurations( customButtons: [ + QuillToolbarCustomButtonOptions( + icon: const Icon(Icons.add_alarm_rounded), + onPressed: () { + final controller = context.requireQuillController; + controller.document + .insert(controller.selection.extentOffset, '\n'); + controller.updateSelection( + TextSelection.collapsed( + offset: controller.selection.extentOffset + 1, + ), + ChangeSource.local, + ); + + controller.document.insert( + controller.selection.extentOffset, + TimeStampEmbed( + DateTime.now().toString(), + ), + ); + + controller.updateSelection( + TextSelection.collapsed( + offset: controller.selection.extentOffset + 1, + ), + ChangeSource.local, + ); + + controller.document + .insert(controller.selection.extentOffset, ' '); + controller.updateSelection( + TextSelection.collapsed( + offset: controller.selection.extentOffset + 1, + ), + ChangeSource.local, + ); + + controller.document + .insert(controller.selection.extentOffset, '\n'); + controller.updateSelection( + TextSelection.collapsed( + offset: controller.selection.extentOffset + 1, + ), + ChangeSource.local, + ); + }, + ), QuillToolbarCustomButtonOptions( icon: const Icon(Icons.ac_unit), onPressed: () { diff --git a/example/macos/Podfile.lock b/example/macos/Podfile.lock index 0e20c8b3..0df3e6fc 100644 --- a/example/macos/Podfile.lock +++ b/example/macos/Podfile.lock @@ -6,6 +6,9 @@ PODS: - file_selector_macos (0.0.1): - FlutterMacOS - FlutterMacOS (1.0.0) + - FMDB (2.7.5): + - FMDB/standard (= 2.7.5) + - FMDB/standard (2.7.5) - gal (1.0.0): - Flutter - FlutterMacOS @@ -16,6 +19,9 @@ PODS: - FlutterMacOS - share_plus (0.0.1): - FlutterMacOS + - sqflite (0.0.2): + - FlutterMacOS + - FMDB (>= 2.7.5) - url_launcher_macos (0.0.1): - FlutterMacOS - video_player_avfoundation (0.0.1): @@ -31,9 +37,14 @@ DEPENDENCIES: - pasteboard (from `Flutter/ephemeral/.symlinks/plugins/pasteboard/macos`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) + - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - video_player_avfoundation (from `Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin`) +SPEC REPOS: + trunk: + - FMDB + EXTERNAL SOURCES: desktop_drop: :path: Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos @@ -51,6 +62,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin share_plus: :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos + sqflite: + :path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos url_launcher_macos: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos video_player_avfoundation: @@ -61,10 +74,12 @@ SPEC CHECKSUMS: device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 + FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1 pasteboard: 9b69dba6fedbb04866be632205d532fe2f6b1d99 path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 share_plus: 76dd39142738f7a68dd57b05093b5e8193f220f7 + sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 video_player_avfoundation: 8563f13d8fc8b2c29dc2d09e60b660e4e8128837 diff --git a/example/macos/Runner/DebugProfile.entitlements b/example/macos/Runner/DebugProfile.entitlements index ef070abf..b3feca3e 100644 --- a/example/macos/Runner/DebugProfile.entitlements +++ b/example/macos/Runner/DebugProfile.entitlements @@ -12,5 +12,7 @@ com.apple.security.files.user-selected.read-only + diff --git a/example/macos/Runner/Release.entitlements b/example/macos/Runner/Release.entitlements index 389e8f35..319280f5 100644 --- a/example/macos/Runner/Release.entitlements +++ b/example/macos/Runner/Release.entitlements @@ -8,5 +8,7 @@ com.apple.security.files.user-selected.read-only + diff --git a/flutter_quill_extensions/lib/presentation/embeds/editor/image/image_menu.dart b/flutter_quill_extensions/lib/presentation/embeds/editor/image/image_menu.dart index 28a2d847..3b7f34bb 100644 --- a/flutter_quill_extensions/lib/presentation/embeds/editor/image/image_menu.dart +++ b/flutter_quill_extensions/lib/presentation/embeds/editor/image/image_menu.dart @@ -142,7 +142,7 @@ class ImageOptionsMenu extends StatelessWidget { imageUrl: imageSource, imageSaverService: imageSaverService, ); - final imageSavedSuccessfully = saveImageResult.isSuccess; + final imageSavedSuccessfully = saveImageResult.error == null; messenger.clearSnackBars(); diff --git a/flutter_quill_extensions/lib/presentation/utils/utils.dart b/flutter_quill_extensions/lib/presentation/utils/utils.dart index fe25b7c4..00e986fd 100644 --- a/flutter_quill_extensions/lib/presentation/utils/utils.dart +++ b/flutter_quill_extensions/lib/presentation/utils/utils.dart @@ -45,9 +45,9 @@ enum SaveImageResultMethod { network, localStorage } @immutable class SaveImageResult { - const SaveImageResult({required this.isSuccess, required this.method}); + const SaveImageResult({required this.error, required this.method}); - final bool isSuccess; + final String? error; final SaveImageResultMethod method; } @@ -67,12 +67,12 @@ Future saveImage({ Uri.parse(appendFileExtensionToImageUrl(imageUrl)), ); return const SaveImageResult( - isSuccess: true, + error: null, method: SaveImageResultMethod.network, ); } catch (e) { - return const SaveImageResult( - isSuccess: false, + return SaveImageResult( + error: e.toString(), method: SaveImageResultMethod.network, ); } @@ -80,12 +80,12 @@ Future saveImage({ try { await imageSaverService.saveLocalImage(imageUrl); return const SaveImageResult( - isSuccess: true, + error: null, method: SaveImageResultMethod.localStorage, ); } catch (e) { - return const SaveImageResult( - isSuccess: false, + return SaveImageResult( + error: e.toString(), method: SaveImageResultMethod.localStorage, ); } diff --git a/lib/src/utils/platform.dart b/lib/src/utils/platform.dart index 020cfae3..a06db05c 100644 --- a/lib/src/utils/platform.dart +++ b/lib/src/utils/platform.dart @@ -2,10 +2,12 @@ import 'dart:io' show Platform; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart' - show kIsWeb, TargetPlatform, defaultTargetPlatform; + show TargetPlatform, defaultTargetPlatform, kIsWeb, visibleForTesting; /// If you want to override the [kIsWeb] use [overrideIsWeb] -bool isWeb({bool? overrideIsWeb}) { +bool isWeb({ + @visibleForTesting bool? overrideIsWeb, +}) { return overrideIsWeb ?? kIsWeb; }