153 lines
4.0 KiB
Dart
153 lines
4.0 KiB
Dart
import 'package:flutter/gestures.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_linkify/flutter_linkify.dart';
|
|
import 'package:linkify/linkify.dart' as linkify_pkg;
|
|
|
|
const TextStyle kSearchHighlightStyle = TextStyle(
|
|
backgroundColor: Color(0xFFFFD54F),
|
|
color: Colors.black,
|
|
);
|
|
|
|
List<TextSpan> buildHighlightedSpans({
|
|
required String text,
|
|
required String? query,
|
|
required TextStyle baseStyle,
|
|
TextStyle highlightStyle = kSearchHighlightStyle,
|
|
GestureRecognizer? recognizer,
|
|
}) {
|
|
final q = query?.trim().toLowerCase();
|
|
if (q == null || q.isEmpty) {
|
|
return [TextSpan(text: text, style: baseStyle, recognizer: recognizer)];
|
|
}
|
|
|
|
final spans = <TextSpan>[];
|
|
final lower = text.toLowerCase();
|
|
var cursor = 0;
|
|
while (cursor < text.length) {
|
|
final hit = lower.indexOf(q, cursor);
|
|
if (hit < 0) {
|
|
spans.add(
|
|
TextSpan(
|
|
text: text.substring(cursor),
|
|
style: baseStyle,
|
|
recognizer: recognizer,
|
|
),
|
|
);
|
|
break;
|
|
}
|
|
if (hit > cursor) {
|
|
spans.add(
|
|
TextSpan(
|
|
text: text.substring(cursor, hit),
|
|
style: baseStyle,
|
|
recognizer: recognizer,
|
|
),
|
|
);
|
|
}
|
|
final end = hit + q.length;
|
|
spans.add(
|
|
TextSpan(
|
|
text: text.substring(hit, end),
|
|
style: baseStyle.merge(highlightStyle),
|
|
recognizer: recognizer,
|
|
),
|
|
);
|
|
cursor = end;
|
|
}
|
|
return spans;
|
|
}
|
|
|
|
class HighlightedLinkify extends StatefulWidget {
|
|
final String text;
|
|
final String? highlight;
|
|
final LinkCallback? onOpen;
|
|
final TextStyle? style;
|
|
final TextStyle? linkStyle;
|
|
|
|
const HighlightedLinkify({
|
|
required this.text,
|
|
this.highlight,
|
|
this.onOpen,
|
|
this.style,
|
|
this.linkStyle,
|
|
super.key,
|
|
});
|
|
|
|
@override
|
|
State<HighlightedLinkify> createState() => _HighlightedLinkifyState();
|
|
}
|
|
|
|
class _HighlightedLinkifyState extends State<HighlightedLinkify> {
|
|
final List<TapGestureRecognizer> _recognizers = [];
|
|
|
|
@override
|
|
void dispose() {
|
|
for (final r in _recognizers) {
|
|
r.dispose();
|
|
}
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
for (final r in _recognizers) {
|
|
r.dispose();
|
|
}
|
|
_recognizers.clear();
|
|
|
|
final defaultStyle = widget.style ??
|
|
Theme.of(context).textTheme.bodyMedium ??
|
|
DefaultTextStyle.of(context).style;
|
|
// Start from the surrounding text style so links inherit font family,
|
|
// size, weight, etc., then layer the link-specific color and underline
|
|
// on top. (Going the other way around — link style as base — used to
|
|
// work because TextStyle.copyWith treats `null` as "leave unchanged",
|
|
// so the explicit `color: null, decoration: null` were silently
|
|
// ignored and the merge pulled defaultStyle's color/decoration over
|
|
// the blue + underline. Result: links rendered in body-text color
|
|
// with no underline.)
|
|
final linkStyle = defaultStyle.merge(
|
|
widget.linkStyle ??
|
|
const TextStyle(
|
|
color: Colors.blue,
|
|
decoration: TextDecoration.underline,
|
|
),
|
|
);
|
|
const linkHighlight = TextStyle(
|
|
backgroundColor: Color(0xFFFFD54F),
|
|
color: Colors.black,
|
|
decoration: TextDecoration.underline,
|
|
);
|
|
|
|
final elements = linkify_pkg.linkify(widget.text);
|
|
final spans = <InlineSpan>[];
|
|
|
|
for (final el in elements) {
|
|
if (el is LinkableElement) {
|
|
final recognizer = TapGestureRecognizer()
|
|
..onTap = () => widget.onOpen?.call(el);
|
|
_recognizers.add(recognizer);
|
|
spans.addAll(
|
|
buildHighlightedSpans(
|
|
text: el.text,
|
|
query: widget.highlight,
|
|
baseStyle: linkStyle,
|
|
highlightStyle: linkHighlight,
|
|
recognizer: recognizer,
|
|
),
|
|
);
|
|
} else {
|
|
spans.addAll(
|
|
buildHighlightedSpans(
|
|
text: el.text,
|
|
query: widget.highlight,
|
|
baseStyle: defaultStyle,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
return Text.rich(TextSpan(children: spans));
|
|
}
|
|
}
|