diff --git a/lib/controller/directory/directory_controller.dart b/lib/controller/directory/directory_controller.dart index 75f0974..72adfbd 100644 --- a/lib/controller/directory/directory_controller.dart +++ b/lib/controller/directory/directory_controller.dart @@ -4,7 +4,7 @@ import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/model/directory/contact_model.dart'; import 'package:marco/model/directory/contact_bucket_list_model.dart'; import 'package:marco/model/directory/directory_comment_model.dart'; - +import 'package:marco/helpers/widgets/my_snackbar.dart'; class DirectoryController extends GetxController { RxList allContacts = [].obs; RxList filteredContacts = [].obs; @@ -16,8 +16,15 @@ class DirectoryController extends GetxController { RxList contactBuckets = [].obs; RxString searchQuery = ''.obs; RxBool showFabMenu = false.obs; - RxMap> contactCommentsMap = - >{}.obs; + final RxBool showFullEditorToolbar = false.obs; + final RxBool isEditorFocused = false.obs; + + final Map> contactCommentsMap = {}; + RxList getCommentsForContact(String contactId) { + return contactCommentsMap[contactId] ?? [].obs; + } + + final editingCommentId = Rxn(); @override void onInit() { @@ -25,41 +32,79 @@ class DirectoryController extends GetxController { fetchContacts(); fetchBuckets(); } +// inside DirectoryController - void extractCategoriesFromContacts() { - final uniqueCategories = {}; + Future updateComment(DirectoryComment comment) async { + try { + logSafe( + "Attempting to update comment. id: ${comment.id}, contactId: ${comment.contactId}"); - for (final contact in allContacts) { - final category = contact.contactCategory; - if (category != null && !uniqueCategories.containsKey(category.id)) { - uniqueCategories[category.id] = category; + final commentList = contactCommentsMap[comment.contactId]; + final oldComment = + commentList?.firstWhereOrNull((c) => c.id == comment.id); + + if (oldComment == null) { + logSafe("Old comment not found. id: ${comment.id}"); + } else { + logSafe("Old comment note: ${oldComment.note}"); + logSafe("New comment note: ${comment.note}"); } - } - contactCategories.value = uniqueCategories.values.toList(); + if (oldComment != null && oldComment.note.trim() == comment.note.trim()) { + logSafe("No changes detected in comment. id: ${comment.id}"); + return; + } + + final success = await ApiService.updateContactComment( + comment.id, + comment.note, + comment.contactId, + ); + + if (success) { + logSafe("Comment updated successfully. id: ${comment.id}"); + await fetchCommentsForContact(comment.contactId); + } else { + logSafe("Failed to update comment via API. id: ${comment.id}"); + showAppSnackbar( + title: "Error", + message: "Failed to update comment.", + type: SnackbarType.error, + ); + } + } catch (e, stackTrace) { + logSafe("Update comment failed: ${e.toString()}"); + logSafe("StackTrace: ${stackTrace.toString()}"); + showAppSnackbar( + title: "Error", + message: "Failed to update comment.", + type: SnackbarType.error, + ); + } } Future fetchCommentsForContact(String contactId) async { - try { - final data = await ApiService.getDirectoryComments(contactId); - logSafe("Fetched comments for contact $contactId: $data"); + try { + final data = await ApiService.getDirectoryComments(contactId); + logSafe("Fetched comments for contact $contactId: $data"); - if (data != null ) { - final comments = data.map((e) => DirectoryComment.fromJson(e)).toList(); - contactCommentsMap[contactId] = comments; - } else { - contactCommentsMap[contactId] = []; + final comments = + data?.map((e) => DirectoryComment.fromJson(e)).toList() ?? []; + + if (!contactCommentsMap.containsKey(contactId)) { + contactCommentsMap[contactId] = [].obs; + } + + contactCommentsMap[contactId]!.assignAll(comments); + contactCommentsMap[contactId]?.refresh(); + } catch (e) { + logSafe("Error fetching comments for contact $contactId: $e", + level: LogLevel.error); + + contactCommentsMap[contactId] ??= [].obs; + contactCommentsMap[contactId]!.clear(); } - - contactCommentsMap.refresh(); - } catch (e) { - logSafe("Error fetching comments for contact $contactId: $e", - level: LogLevel.error); - contactCommentsMap[contactId] = []; - contactCommentsMap.refresh(); } -} - Future fetchBuckets() async { try { @@ -86,9 +131,7 @@ class DirectoryController extends GetxController { if (response != null) { final contacts = response.map((e) => ContactModel.fromJson(e)).toList(); allContacts.assignAll(contacts); - extractCategoriesFromContacts(); - applyFilters(); } else { allContacts.clear(); @@ -101,20 +144,30 @@ class DirectoryController extends GetxController { } } + void extractCategoriesFromContacts() { + final uniqueCategories = {}; + + for (final contact in allContacts) { + final category = contact.contactCategory; + if (category != null && !uniqueCategories.containsKey(category.id)) { + uniqueCategories[category.id] = category; + } + } + + contactCategories.value = uniqueCategories.values.toList(); + } + void applyFilters() { final query = searchQuery.value.toLowerCase(); filteredContacts.value = allContacts.where((contact) { - // 1. Category filter final categoryMatch = selectedCategories.isEmpty || (contact.contactCategory != null && selectedCategories.contains(contact.contactCategory!.id)); - // 2. Bucket filter final bucketMatch = selectedBuckets.isEmpty || contact.bucketIds.any((id) => selectedBuckets.contains(id)); - // 3. Search filter: match name, organization, email, or tags final nameMatch = contact.name.toLowerCase().contains(query); final orgMatch = contact.organization.toLowerCase().contains(query); final emailMatch = contact.contactEmails diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index 18c70fe..cfda4f8 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -41,4 +41,5 @@ class ApiEndpoints { static const String getDirectoryOrganization = "/directory/organization"; static const String createContact = "/directory"; static const String getDirectoryNotes = "/directory/notes"; + static const String updateDirectoryNotes = "/directory/note"; } diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index cd63e6c..b5cb819 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -162,6 +162,49 @@ class ApiService { } } + static Future _putRequest( + String endpoint, + dynamic body, { + Map? additionalHeaders, + Duration customTimeout = timeout, + bool hasRetried = false, + }) async { + String? token = await _getToken(); + if (token == null) return null; + + final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint"); + logSafe( + "PUT $uri\nHeaders: ${_headers(token)}\nBody: $body", + ); + final headers = { + ..._headers(token), + if (additionalHeaders != null) ...additionalHeaders, + }; + + logSafe("PUT $uri\nHeaders: $headers\nBody: $body", sensitive: true); + + try { + final response = await http + .put(uri, headers: headers, body: jsonEncode(body)) + .timeout(customTimeout); + + if (response.statusCode == 401 && !hasRetried) { + logSafe("Unauthorized PUT. Attempting token refresh..."); + if (await AuthService.refreshToken()) { + return await _putRequest(endpoint, body, + additionalHeaders: additionalHeaders, + customTimeout: customTimeout, + hasRetried: true); + } + } + + return response; + } catch (e) { + logSafe("HTTP PUT Exception: $e", level: LogLevel.error); + return null; + } + } + // === Dashboard Endpoints === static Future?> getDashboardAttendanceOverview( @@ -177,16 +220,67 @@ class ApiService { : null); } - /// Directly calling the API -static Future?> getDirectoryComments(String contactId) async { - final url = "${ApiEndpoints.getDirectoryNotes}/$contactId"; - final response = await _getRequest(url); - final data = response != null - ? _parseResponse(response, label: 'Directory Comments') - : null; + /// Directory calling the API + static Future updateContactComment( + String commentId, String note, String contactId) async { + final payload = { + "id": commentId, + "contactId": contactId, + "note": note, + }; - return data is List ? data : null; -} + final endpoint = "${ApiEndpoints.updateDirectoryNotes}/$commentId"; + + final headers = { + "comment-id": commentId, + }; + + logSafe("Updating comment with payload: $payload"); + logSafe("Headers for update comment: $headers"); + logSafe("Sending update comment request to $endpoint"); + + try { + final response = await _putRequest( + endpoint, + payload, + additionalHeaders: headers, + ); + + if (response == null) { + logSafe("Update comment failed: null response", level: LogLevel.error); + return false; + } + + logSafe("Update comment response status: ${response.statusCode}"); + logSafe("Update comment response body: ${response.body}"); + + final json = jsonDecode(response.body); + + if (json['success'] == true) { + logSafe("Comment updated successfully. commentId: $commentId"); + return true; + } else { + logSafe("Failed to update comment: ${json['message']}", + level: LogLevel.warning); + } + } catch (e, stack) { + logSafe("Exception during updateComment API: ${e.toString()}", + level: LogLevel.error); + logSafe("StackTrace: ${stack.toString()}", level: LogLevel.debug); + } + + return false; + } + + static Future?> getDirectoryComments(String contactId) async { + final url = "${ApiEndpoints.getDirectoryNotes}/$contactId"; + final response = await _getRequest(url); + final data = response != null + ? _parseResponse(response, label: 'Directory Comments') + : null; + + return data is List ? data : null; + } static Future createContact(Map payload) async { try { diff --git a/lib/helpers/widgets/Directory/comment_editor_card.dart b/lib/helpers/widgets/Directory/comment_editor_card.dart new file mode 100644 index 0000000..4c26dbf --- /dev/null +++ b/lib/helpers/widgets/Directory/comment_editor_card.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_quill/flutter_quill.dart' as quill; +import 'package:get/get.dart'; + +class CommentEditorCard extends StatelessWidget { + final quill.QuillController controller; + final VoidCallback onCancel; + final Future Function(quill.QuillController controller) onSave; + + const CommentEditorCard({ + super.key, + required this.controller, + required this.onCancel, + required this.onSave, + }); + + @override + Widget build(BuildContext context) { + final RxBool _showFullToolbar = false.obs; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Obx(() { + final showFull = _showFullToolbar.value; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + quill.QuillSimpleToolbar( + controller: controller, + configurations: quill.QuillSimpleToolbarConfigurations( + showBoldButton: true, + showItalicButton: true, + showUnderLineButton: showFull, + showListBullets: true, + showListNumbers: true, + showAlignmentButtons: showFull, + showLink: true, + showFontSize: showFull, + showFontFamily: showFull, + showColorButton: showFull, + showBackgroundColorButton: showFull, + showUndo: false, + showRedo: false, + showCodeBlock: showFull, + showQuote: showFull, + showSuperscript: false, + showSubscript: false, + showInlineCode: false, + showDirection: false, + showListCheck: false, + showStrikeThrough: false, + showClearFormat: showFull, + showDividers: false, + showHeaderStyle: showFull, + multiRowsDisplay: false, + ), + ), + Align( + alignment: Alignment.centerRight, + child: TextButton( + onPressed: () => _showFullToolbar.toggle(), + child: Text( + showFull ? "Hide Formatting" : "More Formatting", + style: const TextStyle(color: Colors.indigo), + ), + ), + ) + ], + ); + }), + const SizedBox(height: 8), + Container( + height: 120, + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(8), + color: const Color(0xFFFDFDFD), + ), + child: quill.QuillEditor.basic( + controller: controller, + configurations: const quill.QuillEditorConfigurations( + autoFocus: true, + expands: false, + scrollable: true, + ), + ), + ), + const SizedBox(height: 8), + Align( + alignment: Alignment.centerRight, + child: Wrap( + spacing: 8, + children: [ + OutlinedButton.icon( + onPressed: onCancel, + icon: const Icon(Icons.close, size: 18), + label: const Text("Cancel"), + style: OutlinedButton.styleFrom( + foregroundColor: Colors.grey[700], + ), + ), + ElevatedButton.icon( + onPressed: () => onSave(controller), + icon: const Icon(Icons.save, size: 18), + label: const Text("Save"), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.indigo, + foregroundColor: Colors.white, + ), + ), + ], + ), + ) + ], + ); + } +} diff --git a/lib/model/directory/directory_comment_model.dart b/lib/model/directory/directory_comment_model.dart index af6741f..562e6dc 100644 --- a/lib/model/directory/directory_comment_model.dart +++ b/lib/model/directory/directory_comment_model.dart @@ -69,6 +69,32 @@ class DirectoryComment { isActive: json['isActive'] ?? true, ); } + + DirectoryComment copyWith({ + String? id, + String? note, + String? contactName, + String? organizationName, + DateTime? createdAt, + CommentUser? createdBy, + DateTime? updatedAt, + CommentUser? updatedBy, + String? contactId, + bool? isActive, + }) { + return DirectoryComment( + id: id ?? this.id, + note: note ?? this.note, + contactName: contactName ?? this.contactName, + organizationName: organizationName ?? this.organizationName, + createdAt: createdAt ?? this.createdAt, + createdBy: createdBy ?? this.createdBy, + updatedAt: updatedAt ?? this.updatedAt, + updatedBy: updatedBy ?? this.updatedBy, + contactId: contactId ?? this.contactId, + isActive: isActive ?? this.isActive, + ); + } } class CommentUser { @@ -98,4 +124,22 @@ class CommentUser { jobRoleName: json['jobRoleName'] ?? '', ); } + + CommentUser copyWith({ + String? id, + String? firstName, + String? lastName, + String? photo, + String? jobRoleId, + String? jobRoleName, + }) { + return CommentUser( + id: id ?? this.id, + firstName: firstName ?? this.firstName, + lastName: lastName ?? this.lastName, + photo: photo ?? this.photo, + jobRoleId: jobRoleId ?? this.jobRoleId, + jobRoleName: jobRoleName ?? this.jobRoleName, + ); + } } diff --git a/lib/view/directory/contact_detail_screen.dart b/lib/view/directory/contact_detail_screen.dart index 3ded565..71e193c 100644 --- a/lib/view/directory/contact_detail_screen.dart +++ b/lib/view/directory/contact_detail_screen.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; -import 'package:flutter_html/flutter_html.dart'; +import 'package:flutter_html/flutter_html.dart' as html; +import 'package:flutter_quill/flutter_quill.dart' as quill; import 'package:marco/controller/project_controller.dart'; import 'package:marco/controller/directory/directory_controller.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; @@ -10,28 +11,92 @@ import 'package:marco/model/directory/contact_model.dart'; import 'package:marco/helpers/widgets/avatar.dart'; import 'package:marco/helpers/utils/launcher_utils.dart'; import 'package:tab_indicator_styler/tab_indicator_styler.dart'; +import 'package:marco/helpers/widgets/Directory/comment_editor_card.dart'; +import 'package:flutter_quill_delta_from_html/flutter_quill_delta_from_html.dart'; -class ContactDetailScreen extends StatelessWidget { +class ContactDetailScreen extends StatefulWidget { final ContactModel contact; const ContactDetailScreen({super.key, required this.contact}); @override - Widget build(BuildContext context) { - final directoryController = Get.find(); - final projectController = Get.find(); + State createState() => _ContactDetailScreenState(); +} - Future.microtask(() { - if (!directoryController.contactCommentsMap.containsKey(contact.id)) { - directoryController.fetchCommentsForContact(contact.id); +String _convertDeltaToHtml(dynamic delta) { + final buffer = StringBuffer(); + bool inList = false; + + for (var op in delta.toList()) { + final data = op.data?.toString() ?? ''; + final attr = op.attributes ?? {}; + + final isListItem = attr.containsKey('list'); + + // Start list + if (isListItem && !inList) { + buffer.write('
    '); + inList = true; + } + + // Close list if we are not in list mode anymore + if (!isListItem && inList) { + buffer.write('
'); + inList = false; + } + + if (isListItem) buffer.write('
  • '); + + // Apply inline styles + if (attr.containsKey('bold')) buffer.write(''); + if (attr.containsKey('italic')) buffer.write(''); + if (attr.containsKey('underline')) buffer.write(''); + if (attr.containsKey('strike')) buffer.write(''); + if (attr.containsKey('link')) buffer.write(''); + + buffer.write(data.replaceAll('\n', '')); + + if (attr.containsKey('link')) buffer.write(''); + if (attr.containsKey('strike')) buffer.write(''); + if (attr.containsKey('underline')) buffer.write(''); + if (attr.containsKey('italic')) buffer.write(''); + if (attr.containsKey('bold')) buffer.write(''); + + if (isListItem) buffer.write('
  • '); + else if (data.contains('\n')) buffer.write('
    '); + } + + if (inList) buffer.write(''); + + return buffer.toString(); +} + + +class _ContactDetailScreenState extends State { + late final DirectoryController directoryController; + late final ProjectController projectController; + + @override + void initState() { + super.initState(); + directoryController = Get.find(); + projectController = Get.find(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!directoryController.contactCommentsMap + .containsKey(widget.contact.id)) { + directoryController.fetchCommentsForContact(widget.contact.id); } }); + } + @override + Widget build(BuildContext context) { return DefaultTabController( length: 2, child: Scaffold( backgroundColor: const Color(0xFFF5F5F5), - appBar: _buildMainAppBar(projectController), + appBar: _buildMainAppBar(), body: SafeArea( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -40,8 +105,8 @@ class ContactDetailScreen extends StatelessWidget { Expanded( child: TabBarView( children: [ - _buildDetailsTab(directoryController, projectController), - _buildCommentsTab(directoryController), + _buildDetailsTab(), + _buildCommentsTab(context), ], ), ), @@ -52,7 +117,7 @@ class ContactDetailScreen extends StatelessWidget { ); } - PreferredSizeWidget _buildMainAppBar(ProjectController projectController) { + PreferredSizeWidget _buildMainAppBar() { return AppBar( backgroundColor: const Color(0xFFF5F5F5), elevation: 0.2, @@ -74,11 +139,8 @@ class ContactDetailScreen extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - MyText.titleLarge( - 'Contact Profile', - fontWeight: 700, - color: Colors.black, - ), + MyText.titleLarge('Contact Profile', + fontWeight: 700, color: Colors.black), MySpacing.height(2), GetBuilder( builder: (projectController) { @@ -120,9 +182,9 @@ class ContactDetailScreen extends StatelessWidget { Row( children: [ Avatar( - firstName: contact.name.split(" ").first, - lastName: contact.name.split(" ").length > 1 - ? contact.name.split(" ").last + firstName: widget.contact.name.split(" ").first, + lastName: widget.contact.name.split(" ").length > 1 + ? widget.contact.name.split(" ").last : "", size: 35, backgroundColor: Colors.indigo, @@ -131,10 +193,10 @@ class ContactDetailScreen extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - MyText.titleSmall(contact.name, + MyText.titleSmall(widget.contact.name, fontWeight: 600, color: Colors.black), MySpacing.height(2), - MyText.bodySmall(contact.organization, + MyText.bodySmall(widget.contact.organization, fontWeight: 500, color: Colors.grey[700]), ], ), @@ -161,28 +223,28 @@ class ContactDetailScreen extends StatelessWidget { ); } - Widget _buildDetailsTab(DirectoryController directoryController, - ProjectController projectController) { - final email = contact.contactEmails.isNotEmpty - ? contact.contactEmails.first.emailAddress + Widget _buildDetailsTab() { + final email = widget.contact.contactEmails.isNotEmpty + ? widget.contact.contactEmails.first.emailAddress : "-"; - final phone = contact.contactPhones.isNotEmpty - ? contact.contactPhones.first.phoneNumber + final phone = widget.contact.contactPhones.isNotEmpty + ? widget.contact.contactPhones.first.phoneNumber : "-"; - final createdDate = DateTime.now(); + final createdDate = + DateTime.now(); // TODO: Replace with actual creation date if available final formattedDate = DateFormat('MMMM dd, yyyy').format(createdDate); - final tags = contact.tags.map((e) => e.name).join(", "); + final tags = widget.contact.tags.map((e) => e.name).join(", "); - final bucketNames = contact.bucketIds + final bucketNames = widget.contact.bucketIds .map((id) => directoryController.contactBuckets .firstWhereOrNull((b) => b.id == id) ?.name) .whereType() .join(", "); - final projectNames = contact.projectIds + final projectNames = widget.contact.projectIds ?.map((id) => projectController.projects .firstWhereOrNull((p) => p.id == id) ?.name) @@ -190,7 +252,7 @@ class ContactDetailScreen extends StatelessWidget { .join(", ") ?? "-"; - final category = contact.contactCategory?.name ?? "-"; + final category = widget.contact.contactCategory?.name ?? "-"; return SingleChildScrollView( padding: MySpacing.xy(8, 8), @@ -207,10 +269,11 @@ class ContactDetailScreen extends StatelessWidget { onLongPress: () => LauncherUtils.copyToClipboard(phone, typeLabel: "Phone")), _iconInfoRow(Icons.calendar_today, "Created", formattedDate), - _iconInfoRow(Icons.location_on, "Address", contact.address), + _iconInfoRow(Icons.location_on, "Address", widget.contact.address), ]), _infoCard("Organization", [ - _iconInfoRow(Icons.business, "Organization", contact.organization), + _iconInfoRow( + Icons.business, "Organization", widget.contact.organization), _iconInfoRow(Icons.category, "Category", category), ]), _infoCard("Meta Info", [ @@ -224,7 +287,7 @@ class ContactDetailScreen extends StatelessWidget { Align( alignment: Alignment.topLeft, child: MyText.bodyMedium( - contact.description, + widget.contact.description, color: Colors.grey[800], maxLines: 10, textAlign: TextAlign.left, @@ -236,17 +299,21 @@ class ContactDetailScreen extends StatelessWidget { ); } - Widget _buildCommentsTab(DirectoryController directoryController) { + Widget _buildCommentsTab(BuildContext context) { return Obx(() { - final comments = directoryController.contactCommentsMap[contact.id]; - - if (comments == null) { + if (!directoryController.contactCommentsMap + .containsKey(widget.contact.id)) { return const Center(child: CircularProgressIndicator()); } + final comments = + directoryController.getCommentsForContact(widget.contact.id); + final editingId = directoryController.editingCommentId.value; + if (comments.isEmpty) { return Center( - child: MyText.bodyLarge("No comments yet.", color: Colors.grey)); + child: MyText.bodyLarge("No comments yet.", color: Colors.grey), + ); } return ListView.separated( @@ -255,10 +322,22 @@ class ContactDetailScreen extends StatelessWidget { separatorBuilder: (_, __) => MySpacing.height(12), itemBuilder: (_, index) { final comment = comments[index]; + final isEditing = editingId == comment.id; + final initials = comment.createdBy.firstName.isNotEmpty ? comment.createdBy.firstName[0].toUpperCase() : "?"; + final decodedDelta = HtmlToDelta().convert(comment.note); + + final quillController = isEditing + ? quill.QuillController( + document: quill.Document.fromDelta(decodedDelta), + selection: + TextSelection.collapsed(offset: decodedDelta.length), + ) + : null; + return Container( padding: MySpacing.xy(14, 12), decoration: BoxDecoration( @@ -296,40 +375,55 @@ class ContactDetailScreen extends StatelessWidget { ), ), IconButton( - icon: const Icon(Icons.more_vert, - size: 20, color: Colors.grey), - onPressed: () {}, - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), + icon: Icon(isEditing ? Icons.close : Icons.edit, + size: 20, color: Colors.grey[700]), + onPressed: () { + directoryController.editingCommentId.value = + isEditing ? null : comment.id; + }, ), ], ), MySpacing.height(10), - Html( - data: comment.note, - style: { - "body": Style( - margin: Margins.all(0), - padding: HtmlPaddings.all(0), - fontSize: FontSize.medium, - color: Colors.black87, - ), - "pre": Style( - padding: HtmlPaddings.all(8), - fontSize: FontSize.small, - fontFamily: 'monospace', - backgroundColor: const Color(0xFFF1F1F1), - border: Border.all(color: Colors.grey.shade300), - ), - "h3": Style( - fontSize: FontSize.large, - fontWeight: FontWeight.bold, - color: Colors.indigo[700], - ), - "strong": Style(fontWeight: FontWeight.w700), - "p": Style(margin: Margins.only(bottom: 8)), - }, - ), + if (isEditing && quillController != null) + CommentEditorCard( + controller: quillController, + onCancel: () { + directoryController.editingCommentId.value = null; + }, + onSave: (controller) async { + final delta = controller.document.toDelta(); + final htmlOutput = _convertDeltaToHtml(delta); + final updated = comment.copyWith(note: htmlOutput); + await directoryController.updateComment(updated); + directoryController.editingCommentId.value = null; + }) + else + html.Html( + data: comment.note, + style: { + "body": html.Style( + margin: html.Margins.all(0), + padding: html.HtmlPaddings.all(0), + fontSize: html.FontSize.medium, + color: Colors.black87, + ), + "pre": html.Style( + padding: html.HtmlPaddings.all(8), + fontSize: html.FontSize.small, + fontFamily: 'monospace', + backgroundColor: const Color(0xFFF1F1F1), + border: Border.all(color: Colors.grey.shade300), + ), + "h3": html.Style( + fontSize: html.FontSize.large, + fontWeight: FontWeight.bold, + color: Colors.indigo[700], + ), + "strong": html.Style(fontWeight: FontWeight.w700), + "p": html.Style(margin: html.Margins.only(bottom: 8)), + }, + ), ], ), ); diff --git a/pubspec.yaml b/pubspec.yaml index 1849534..fef9493 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -52,7 +52,7 @@ dependencies: intl: ^0.19.0 syncfusion_flutter_core: ^28.1.33 syncfusion_flutter_sliders: ^28.1.33 - file_picker: ^8.1.5 + file_picker: ^9.2.3 timelines_plus: ^1.0.4 syncfusion_flutter_charts: ^28.1.33 appflowy_board: ^0.1.2 @@ -74,6 +74,9 @@ dependencies: font_awesome_flutter: ^10.8.0 flutter_html: ^3.0.0 tab_indicator_styler: ^2.0.0 + html_editor_enhanced: ^2.7.0 + flutter_quill_delta_from_html: ^1.5.2 + quill_delta: ^3.0.0-nullsafety.2 dev_dependencies: flutter_test: sdk: flutter