Added additional toolbar icons
This commit is contained in:
parent
2aeb73d5de
commit
1b2b89d506
|
|
@ -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,12 +1365,10 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
|||
),
|
||||
|
||||
// Action taken (rich text)
|
||||
SingleChildScrollView(
|
||||
child: Padding(
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Action taken'),
|
||||
const SizedBox(height: 6),
|
||||
|
|
@ -1345,9 +1382,7 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
|||
context,
|
||||
).colorScheme.outline,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(
|
||||
8,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
|
|
@ -1422,6 +1457,185 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
|||
.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),
|
||||
|
|
@ -1429,10 +1643,21 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
|||
child: MouseRegion(
|
||||
cursor:
|
||||
SystemMouseCursors.text,
|
||||
child:
|
||||
quill.QuillEditor.basic(
|
||||
child: quill.QuillEditor.basic(
|
||||
controller:
|
||||
_actionController!,
|
||||
focusNode: _actionFocusNode,
|
||||
scrollController:
|
||||
_actionScrollController,
|
||||
config:
|
||||
quill.QuillEditorConfig(
|
||||
embedBuilders: const [
|
||||
_ImageEmbedBuilder(),
|
||||
],
|
||||
scrollable: true,
|
||||
padding:
|
||||
EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -1472,8 +1697,7 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
|||
child: Icon(
|
||||
Icons.check,
|
||||
size: 10,
|
||||
color:
|
||||
Colors.white,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -1487,7 +1711,6 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
|||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
24
pubspec.lock
24
pubspec.lock
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user