Files
tasq/lib/widgets/quill_field.dart
T
redz1029 f9b8831c71 feat(programmer): day rows builder + work-log workDate
- Add workDate field to ProgrammerTaskWorkLog (defaults to Manila day of createdAt)
- Add deltaJsonToPlainText function to quill_field.dart for Delta/plain text conversion
- Implement buildDayRows function to generate day sheet rows with proper sorting
- Implement DaySheetRow serialization and deserialization
- Add comprehensive tests for day rows builder and workDate field
- Verify backward compatibility via existing programmer_task_ledger_test
Co-Authored-By: claude-flow <ruv@ruv.net>
2026-09-27 21:04:23 +08:00

165 lines
5.2 KiB
Dart

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart' as quill;
/// Convert a stored Delta JSON or plain text string to trimmed plain text.
/// - Delta JSON (starts with '[') is decoded and converted to plain text
/// - Non-JSON strings are returned trimmed as-is
/// - null returns empty string
String deltaJsonToPlainText(String? stored) {
if (stored == null) return '';
final trimmed = stored.trim();
if (trimmed.isEmpty) return '';
if (trimmed.startsWith('[')) {
try {
final decoded = jsonDecode(trimmed);
if (decoded is List) {
final doc = quill.Document.fromJson(decoded);
return doc.toPlainText().trim();
}
} catch (_) {}
}
return trimmed;
}
/// Parses a stored Delta-JSON string into a [quill.QuillController], or returns
/// an empty one. Callers own the controller and must dispose it.
quill.QuillController quillControllerFromDelta(String? deltaJson) {
if (deltaJson != null && deltaJson.isNotEmpty) {
try {
final decoded = jsonDecode(deltaJson);
if (decoded is List) {
return quill.QuillController(
document: quill.Document.fromJson(decoded),
selection: const TextSelection.collapsed(offset: 0),
);
}
} catch (_) {}
}
return quill.QuillController.basic();
}
/// Serializes a controller's document to a Delta-JSON string for storage.
String quillToDeltaJson(quill.QuillController controller) =>
jsonEncode(controller.document.toDelta().toJson());
/// The controller's plain text (trimmed) — for empty checks and previews.
String quillPlainText(quill.QuillController controller) =>
controller.document.toPlainText().trim();
/// Bordered rich-text editor with a minimal toolbar (bold/italic/underline +
/// bullet/numbered lists). The caller owns [controller] and disposes it.
class QuillFieldEditor extends StatelessWidget {
const QuillFieldEditor({
super.key,
required this.controller,
this.placeholder,
this.height = 140,
});
final quill.QuillController controller;
final String? placeholder;
final double height;
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Container(
height: height,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
border: Border.all(color: cs.outlineVariant),
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
SizedBox(
height: 40,
child: quill.QuillSimpleToolbar(
controller: controller,
config: const quill.QuillSimpleToolbarConfig(
multiRowsDisplay: false,
showAlignmentButtons: false,
showBackgroundColorButton: false,
showCenterAlignment: false,
showColorButton: false,
showDividers: false,
showFontFamily: false,
showFontSize: false,
showHeaderStyle: false,
showIndent: false,
showInlineCode: false,
showLeftAlignment: false,
showLink: false,
showQuote: false,
showRightAlignment: false,
showSearchButton: false,
showCodeBlock: false,
showDirection: false,
showJustifyAlignment: false,
showListCheck: false,
showSubscript: false,
showSuperscript: false,
showStrikeThrough: false,
showSmallButton: false,
showClearFormat: false,
showRedo: false,
showUndo: false,
),
),
),
const Divider(height: 1),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
child: quill.QuillEditor.basic(
controller: controller,
config: quill.QuillEditorConfig(
placeholder: placeholder,
expands: true,
),
),
),
),
],
),
);
}
}
/// Renders a stored value read-only. Interprets a Delta-JSON string as rich
/// text; falls back to a plain [Text] for legacy plain-string values.
class QuillReadOnly extends StatelessWidget {
const QuillReadOnly(this.deltaOrText, {super.key, this.style});
final String deltaOrText;
final TextStyle? style;
@override
Widget build(BuildContext context) {
final trimmed = deltaOrText.trim();
if (trimmed.startsWith('[')) {
try {
final decoded = jsonDecode(trimmed);
if (decoded is List) {
final controller = quill.QuillController(
document: quill.Document.fromJson(decoded),
selection: const TextSelection.collapsed(offset: 0),
);
return IgnorePointer(
child: quill.QuillEditor.basic(
controller: controller,
config: const quill.QuillEditorConfig(
showCursor: false,
autoFocus: false,
),
),
);
}
} catch (_) {}
}
return Text(deltaOrText, style: style);
}
}