Added additional toolbar icons

This commit is contained in:
Marc Rejohn Castillano 2026-02-21 17:27:24 +08:00
parent 2aeb73d5de
commit 1b2b89d506
3 changed files with 394 additions and 146 deletions

View File

@ -10,6 +10,7 @@ import '../../models/ticket_message.dart';
import '../../providers/notifications_provider.dart';
import 'dart:async';
import 'dart:convert';
import 'package:file_picker/file_picker.dart';
import 'package:flutter_quill/flutter_quill.dart' as quill;
import '../../providers/supabase_provider.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
@ -26,6 +27,36 @@ import '../../theme/app_surfaces.dart';
import '../../widgets/task_assignment_section.dart';
import '../../widgets/typing_dots.dart';
// Simple image embed builder to support data-URI and network images
class _ImageEmbedBuilder extends quill.EmbedBuilder {
const _ImageEmbedBuilder();
@override
String get key => quill.BlockEmbed.imageType;
@override
Widget build(BuildContext context, quill.EmbedContext embedContext) {
final data = embedContext.node.value.data as String;
if (data.startsWith('data:image/')) {
try {
final base64Str = data.split(',').last;
final bytes = base64Decode(base64Str);
return ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 240),
child: Image.memory(bytes, fit: BoxFit.contain),
);
} catch (_) {
return const SizedBox.shrink();
}
}
// Fallback to network image
return ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 240),
child: Image.network(data, fit: BoxFit.contain),
);
}
}
// Local request metadata options (kept consistent with other screens)
const List<String> requestTypeOptions = [
'Install',
@ -56,6 +87,8 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
// Rich text editor for Action taken
quill.QuillController? _actionController;
Timer? _actionDebounce;
late final FocusNode _actionFocusNode;
late final ScrollController _actionScrollController;
Timer? _requestedDebounce;
Timer? _notedDebounce;
Timer? _receivedDebounce;
@ -101,6 +134,10 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
);
// create an empty action controller by default; will seed per-task later
_actionController = quill.QuillController.basic();
_actionFocusNode = FocusNode();
_actionScrollController = ScrollController();
// Debugging: to enable a scroll jump detector, add a listener here.
// Keep it disabled in production to avoid analyzer dead_code warnings.
}
@override
@ -114,6 +151,8 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
_receivedDebounce?.cancel();
_actionDebounce?.cancel();
_actionController?.dispose();
_actionFocusNode.dispose();
_actionScrollController.dispose();
_saveAnimController.dispose();
super.dispose();
}
@ -1326,166 +1365,350 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
),
// Action taken (rich text)
SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
const Text('Action taken'),
const SizedBox(height: 6),
// Toolbar + editor with inline save indicator
Container(
height: isWide ? 260 : 220,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(
context,
).colorScheme.outline,
),
borderRadius: BorderRadius.circular(
8,
),
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Action taken'),
const SizedBox(height: 6),
// Toolbar + editor with inline save indicator
Container(
height: isWide ? 260 : 220,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(
context,
).colorScheme.outline,
),
child: Stack(
children: [
Column(
children: [
Row(
children: [
IconButton(
tooltip: 'Bold',
icon: const Icon(
Icons.format_bold,
),
onPressed: () =>
_actionController
?.formatSelection(
quill
.Attribute
.bold,
),
borderRadius: BorderRadius.circular(8),
),
child: Stack(
children: [
Column(
children: [
Row(
children: [
IconButton(
tooltip: 'Bold',
icon: const Icon(
Icons.format_bold,
),
IconButton(
tooltip: 'Italic',
icon: const Icon(
Icons.format_italic,
),
onPressed: () =>
_actionController
?.formatSelection(
quill
.Attribute
.italic,
),
onPressed: () =>
_actionController
?.formatSelection(
quill
.Attribute
.bold,
),
),
IconButton(
tooltip: 'Italic',
icon: const Icon(
Icons.format_italic,
),
IconButton(
tooltip: 'Underline',
icon: const Icon(
Icons.format_underlined,
),
onPressed: () =>
_actionController
?.formatSelection(
quill
.Attribute
.underline,
),
onPressed: () =>
_actionController
?.formatSelection(
quill
.Attribute
.italic,
),
),
IconButton(
tooltip: 'Underline',
icon: const Icon(
Icons.format_underlined,
),
IconButton(
tooltip: 'Bullet list',
icon: const Icon(
Icons
.format_list_bulleted,
),
onPressed: () =>
_actionController
?.formatSelection(
quill
.Attribute
.ul,
),
onPressed: () =>
_actionController
?.formatSelection(
quill
.Attribute
.underline,
),
),
IconButton(
tooltip: 'Bullet list',
icon: const Icon(
Icons
.format_list_bulleted,
),
IconButton(
tooltip: 'Numbered list',
icon: const Icon(
Icons
.format_list_numbered,
),
onPressed: () =>
_actionController
?.formatSelection(
quill
.Attribute
.ol,
),
onPressed: () =>
_actionController
?.formatSelection(
quill
.Attribute
.ul,
),
),
IconButton(
tooltip: 'Numbered list',
icon: const Icon(
Icons
.format_list_numbered,
),
],
),
const SizedBox(height: 6),
Expanded(
child: MouseRegion(
cursor:
SystemMouseCursors.text,
child:
quill.QuillEditor.basic(
controller:
_actionController!,
onPressed: () =>
_actionController
?.formatSelection(
quill
.Attribute
.ol,
),
),
const SizedBox(width: 8),
IconButton(
tooltip: 'Heading 2',
icon: const Icon(
Icons.format_size,
),
onPressed: () =>
_actionController
?.formatSelection(
quill
.Attribute
.h2,
),
),
IconButton(
tooltip: 'Heading 3',
icon: const Icon(
Icons.format_size,
size: 18,
),
onPressed: () =>
_actionController
?.formatSelection(
quill
.Attribute
.h3,
),
),
IconButton(
tooltip: 'Undo',
icon: const Icon(
Icons.undo,
),
onPressed: () =>
_actionController
?.undo(),
),
IconButton(
tooltip: 'Redo',
icon: const Icon(
Icons.redo,
),
onPressed: () =>
_actionController
?.redo(),
),
IconButton(
tooltip: 'Insert link',
icon: const Icon(
Icons.link,
),
onPressed: () async {
final urlCtrl =
TextEditingController();
final res = await showDialog<String?>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text(
'Insert link',
),
content: TextField(
controller: urlCtrl,
decoration:
const InputDecoration(
hintText:
'https://',
),
),
actions: [
TextButton(
onPressed: () =>
Navigator.of(
ctx,
).pop(),
child: const Text(
'Cancel',
),
),
TextButton(
onPressed: () =>
Navigator.of(
ctx,
).pop(
urlCtrl.text
.trim(),
),
child: const Text(
'Insert',
),
),
],
),
);
if (res == null ||
res.isEmpty) {
return;
}
final sel =
_actionController
?.selection ??
const TextSelection.collapsed(
offset: 0,
);
final start =
sel.baseOffset;
final end =
sel.extentOffset;
if (!sel.isCollapsed &&
end > start) {
final len = end - start;
try {
_actionController
?.document
.delete(
start,
len,
);
} catch (_) {}
_actionController
?.document
.insert(start, res);
} else {
_actionController
?.document
.insert(start, res);
}
},
),
IconButton(
tooltip: 'Insert image',
icon: const Icon(
Icons.image,
),
onPressed: () async {
try {
final r =
await FilePicker
.platform
.pickFiles(
withData:
true,
type: FileType
.image,
);
if (r == null ||
r.files.isEmpty) {
return;
}
final file =
r.files.first;
final bytes =
file.bytes;
if (bytes == null) {
return;
}
final b64 =
base64Encode(bytes);
final ext =
file.extension ??
'png';
final dataUri =
'data:image/$ext;base64,$b64';
final idx =
_actionController
?.selection
.baseOffset ??
0;
_actionController
?.document
.insert(
idx,
quill
.BlockEmbed.image(
dataUri,
),
);
} catch (_) {}
},
),
],
),
const SizedBox(height: 6),
Expanded(
child: MouseRegion(
cursor:
SystemMouseCursors.text,
child: quill.QuillEditor.basic(
controller:
_actionController!,
focusNode: _actionFocusNode,
scrollController:
_actionScrollController,
config:
quill.QuillEditorConfig(
embedBuilders: const [
_ImageEmbedBuilder(),
],
scrollable: true,
padding:
EdgeInsets.zero,
),
),
),
],
),
Positioned(
right: 6,
bottom: 6,
child: _actionSaving
? SizedBox(
width: 20,
height: 20,
child: ScaleTransition(
scale: _savePulse,
child: const Icon(
),
],
),
Positioned(
right: 6,
bottom: 6,
child: _actionSaving
? SizedBox(
width: 20,
height: 20,
child: ScaleTransition(
scale: _savePulse,
child: const Icon(
Icons.save,
size: 16,
),
),
)
: _actionSaved
? SizedBox(
width: 20,
height: 20,
child: Stack(
alignment:
Alignment.center,
children: const [
Icon(
Icons.save,
size: 16,
color: Colors.green,
),
),
)
: _actionSaved
? SizedBox(
width: 20,
height: 20,
child: Stack(
alignment:
Alignment.center,
children: const [
Icon(
Icons.save,
size: 16,
color: Colors.green,
Positioned(
right: -2,
bottom: -2,
child: Icon(
Icons.check,
size: 10,
color: Colors.white,
),
Positioned(
right: -2,
bottom: -2,
child: Icon(
Icons.check,
size: 10,
color:
Colors.white,
),
),
],
),
)
: const SizedBox.shrink(),
),
],
),
),
],
),
)
: const SizedBox.shrink(),
),
],
),
],
),
),
],
),
),
],

View File

@ -249,6 +249,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "10.8.3"
dbus:
dependency: transitive
description:
name: dbus
sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270
url: "https://pub.dev"
source: hosted
version: "0.7.12"
diff_match_patch:
dependency: transitive
description:
@ -289,6 +297,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.1.4"
file_picker:
dependency: "direct main"
description:
name: file_picker
sha256: "57d9a1dd5063f85fa3107fb42d1faffda52fdc948cefd5fe5ea85267a5fc7343"
url: "https://pub.dev"
source: hosted
version: "10.3.10"
file_selector_platform_interface:
dependency: transitive
description:
@ -419,6 +435,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "8.2.2"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1
url: "https://pub.dev"
source: hosted
version: "2.0.33"
flutter_quill:
dependency: "direct main"
description:

View File

@ -22,6 +22,7 @@ dependencies:
latlong2: ^0.9.0
flutter_typeahead: ^4.1.0
flutter_quill: ^11.5.0
file_picker: ^10.3.10
dev_dependencies:
flutter_test: