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 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 = []; 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 createState() => _HighlightedLinkifyState(); } class _HighlightedLinkifyState extends State { final List _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; final linkStyle = (widget.linkStyle ?? const TextStyle(color: Colors.blue, decoration: TextDecoration.underline)) .merge(defaultStyle.copyWith(color: null, decoration: null)); const linkHighlight = TextStyle( backgroundColor: Color(0xFFFFD54F), color: Colors.black, decoration: TextDecoration.underline, ); final elements = linkify_pkg.linkify(widget.text); final spans = []; 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)); } }