mirror of
https://github.com/kuhyx/testsAndMisc-archive.git
synced 2026-07-04 15:23:06 +02:00
Add the complete annotations feature for marking and annotating script text: Core models (horatio_core): - TextMark, LineNote, AnnotationSnapshot, MarkType, NoteCategory - Script.id field + UUID generation in text_parser Database layer (horatio_app): - Drift tables: text_marks, line_notes, annotation_snapshots - AppDatabase with AnnotationDao (full CRUD + streams + bulk replace) State management: - AnnotationCubit: mark/note CRUD, line selection, editing context - AnnotationHistoryCubit: snapshot save/restore with stream updates UI components: - MarkOverlay: colored span rendering for text marks - NoteIndicator: per-line note count badge - MarkTypePicker: 6-type ActionChip selector - NoteEditorSheet: category dropdown + text field bottom sheet - AnnotationEditorScreen: full editor with long-press marks + note editing - AnnotationHistoryScreen: snapshot timeline with restore dialog Wiring: - main.dart: async DB init with path_provider - app.dart: RepositoryProvider<AnnotationDao> - router.dart: /annotations + /annotation-history routes - role_selection_screen: Annotate Script option - run.sh: app_codegen step + coverage filtering for generated code 352 tests (105 core + 247 app), 100% branch coverage, zero dead code.
88 lines
2.5 KiB
Dart
88 lines
2.5 KiB
Dart
import 'dart:math';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:horatio_core/horatio_core.dart';
|
|
|
|
/// Color map for each [MarkType].
|
|
const Map<MarkType, Color> markColors = {
|
|
MarkType.stress: Color.fromRGBO(244, 67, 54, 0.3),
|
|
MarkType.pause: Color.fromRGBO(33, 150, 243, 0.3),
|
|
MarkType.breath: Color.fromRGBO(76, 175, 80, 0.3),
|
|
MarkType.emphasis: Color.fromRGBO(255, 152, 0, 0.3),
|
|
MarkType.slowDown: Color.fromRGBO(156, 39, 176, 0.3),
|
|
MarkType.speedUp: Color.fromRGBO(0, 150, 136, 0.3),
|
|
};
|
|
|
|
/// Renders text with colored highlight spans for [TextMark] overlays.
|
|
class MarkOverlay extends StatelessWidget {
|
|
/// Creates a [MarkOverlay].
|
|
const MarkOverlay({
|
|
required this.text,
|
|
required this.marks,
|
|
this.style,
|
|
super.key,
|
|
});
|
|
|
|
/// The full line text.
|
|
final String text;
|
|
|
|
/// Marks to overlay on the text.
|
|
final List<TextMark> marks;
|
|
|
|
/// Base text style.
|
|
final TextStyle? style;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final defaultStyle =
|
|
style ?? DefaultTextStyle.of(context).style;
|
|
if (marks.isEmpty) {
|
|
return RichText(text: TextSpan(text: text, style: defaultStyle));
|
|
}
|
|
return RichText(
|
|
text: TextSpan(style: defaultStyle, children: _buildSpans()),
|
|
);
|
|
}
|
|
|
|
List<TextSpan> _buildSpans() {
|
|
// Collect boundary events, clamped to valid text range.
|
|
final length = text.length;
|
|
final events = <({int offset, bool isStart, MarkType type})>[];
|
|
for (final mark in marks) {
|
|
final start = mark.startOffset.clamp(0, length);
|
|
final end = mark.endOffset.clamp(0, length);
|
|
if (start >= end) continue;
|
|
events
|
|
..add((offset: start, isStart: true, type: mark.type))
|
|
..add((offset: end, isStart: false, type: mark.type));
|
|
}
|
|
events.sort((a, b) => a.offset.compareTo(b.offset));
|
|
|
|
final spans = <TextSpan>[];
|
|
var cursor = 0;
|
|
final activeTypes = <MarkType>[];
|
|
|
|
for (final event in events) {
|
|
final pos = min(event.offset, length);
|
|
if (pos > cursor) {
|
|
spans.add(TextSpan(
|
|
text: text.substring(cursor, pos),
|
|
style: activeTypes.isEmpty
|
|
? null
|
|
: TextStyle(backgroundColor: markColors[activeTypes.last]),
|
|
));
|
|
cursor = pos;
|
|
}
|
|
if (event.isStart) {
|
|
activeTypes.add(event.type);
|
|
} else {
|
|
activeTypes.remove(event.type);
|
|
}
|
|
}
|
|
if (cursor < length) {
|
|
spans.add(TextSpan(text: text.substring(cursor)));
|
|
}
|
|
return spans;
|
|
}
|
|
}
|