feat(directory): implement comment editing functionality and enhance comment model

This commit is contained in:
Vaibhav Surve 2025-07-04 15:09:49 +05:30
parent 83ad10ffb4
commit be71544ae4
7 changed files with 523 additions and 114 deletions

View File

@ -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<ContactModel> allContacts = <ContactModel>[].obs;
RxList<ContactModel> filteredContacts = <ContactModel>[].obs;
@ -16,8 +16,15 @@ class DirectoryController extends GetxController {
RxList<ContactBucket> contactBuckets = <ContactBucket>[].obs;
RxString searchQuery = ''.obs;
RxBool showFabMenu = false.obs;
RxMap<String, List<DirectoryComment>> contactCommentsMap =
<String, List<DirectoryComment>>{}.obs;
final RxBool showFullEditorToolbar = false.obs;
final RxBool isEditorFocused = false.obs;
final Map<String, RxList<DirectoryComment>> contactCommentsMap = {};
RxList<DirectoryComment> getCommentsForContact(String contactId) {
return contactCommentsMap[contactId] ?? <DirectoryComment>[].obs;
}
final editingCommentId = Rxn<String>();
@override
void onInit() {
@ -25,41 +32,79 @@ class DirectoryController extends GetxController {
fetchContacts();
fetchBuckets();
}
// inside DirectoryController
void extractCategoriesFromContacts() {
final uniqueCategories = <String, ContactCategory>{};
Future<void> 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<void> 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] = <DirectoryComment>[].obs;
}
contactCommentsMap[contactId]!.assignAll(comments);
contactCommentsMap[contactId]?.refresh();
} catch (e) {
logSafe("Error fetching comments for contact $contactId: $e",
level: LogLevel.error);
contactCommentsMap[contactId] ??= <DirectoryComment>[].obs;
contactCommentsMap[contactId]!.clear();
}
contactCommentsMap.refresh();
} catch (e) {
logSafe("Error fetching comments for contact $contactId: $e",
level: LogLevel.error);
contactCommentsMap[contactId] = [];
contactCommentsMap.refresh();
}
}
Future<void> 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 = <String, ContactCategory>{};
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

View File

@ -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";
}

View File

@ -162,6 +162,49 @@ class ApiService {
}
}
static Future<http.Response?> _putRequest(
String endpoint,
dynamic body, {
Map<String, String>? 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<List<dynamic>?> getDashboardAttendanceOverview(
@ -177,16 +220,67 @@ class ApiService {
: null);
}
/// Directly calling the API
static Future<List<dynamic>?> 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<bool> 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<List<dynamic>?> 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<bool> createContact(Map<String, dynamic> payload) async {
try {

View File

@ -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<void> 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,
),
),
],
),
)
],
);
}
}

View File

@ -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,
);
}
}

View File

@ -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<DirectoryController>();
final projectController = Get.find<ProjectController>();
State<ContactDetailScreen> 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('<ul>');
inList = true;
}
// Close list if we are not in list mode anymore
if (!isListItem && inList) {
buffer.write('</ul>');
inList = false;
}
if (isListItem) buffer.write('<li>');
// Apply inline styles
if (attr.containsKey('bold')) buffer.write('<strong>');
if (attr.containsKey('italic')) buffer.write('<em>');
if (attr.containsKey('underline')) buffer.write('<u>');
if (attr.containsKey('strike')) buffer.write('<s>');
if (attr.containsKey('link')) buffer.write('<a href="${attr['link']}">');
buffer.write(data.replaceAll('\n', ''));
if (attr.containsKey('link')) buffer.write('</a>');
if (attr.containsKey('strike')) buffer.write('</s>');
if (attr.containsKey('underline')) buffer.write('</u>');
if (attr.containsKey('italic')) buffer.write('</em>');
if (attr.containsKey('bold')) buffer.write('</strong>');
if (isListItem) buffer.write('</li>');
else if (data.contains('\n')) buffer.write('<br>');
}
if (inList) buffer.write('</ul>');
return buffer.toString();
}
class _ContactDetailScreenState extends State<ContactDetailScreen> {
late final DirectoryController directoryController;
late final ProjectController projectController;
@override
void initState() {
super.initState();
directoryController = Get.find<DirectoryController>();
projectController = Get.find<ProjectController>();
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<ProjectController>(
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<String>()
.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)),
},
),
],
),
);

View File

@ -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