flutter_richify 0.0.1
flutter_richify: ^0.0.1 copied to clipboard
A Flutter package to make it easy to create links, mentions, hashtags, emails, phones, and custom patterns.
import 'package:flutter/material.dart';
import 'package:flutter_richify/flutter_richify.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Richify Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
home: const RichifyDemo(),
);
}
}
class RichifyDemo extends StatefulWidget {
const RichifyDemo({super.key});
@override
State<RichifyDemo> createState() => _RichifyDemoState();
}
class _RichifyDemoState extends State<RichifyDemo> {
late RichifyController _socialController;
late RichifyController _emailController;
late RichifyController _linkController;
final List<String> _detectedMentions = [];
final List<String> _detectedHashtags = [];
final List<String> _detectedEmails = [];
String _lastClickedItem = '';
@override
void initState() {
super.initState();
// Social media style input with mentions and hashtags
_socialController = RichifyController(
text: 'Hey @john! Check out #flutter_richify for rich text patterns',
matchers: [
// Mentions matcher
RegexMatcher(
pattern: RegExp(r'@\w+'),
spanBuilder: (candidate) => TextSpan(
text: candidate.text,
style: const TextStyle(
color: Colors.blue,
fontWeight: FontWeight.bold,
),
),
options: const TextMatcherOptions(
deleteOnBack: true, // Delete entire mention on backspace
),
),
// Hashtags matcher
RegexMatcher(
pattern: RegExp(r'#\w+'),
spanBuilder: (candidate) => TextSpan(
text: candidate.text,
style: const TextStyle(
color: Colors.green,
fontWeight: FontWeight.w600,
),
),
options: const TextMatcherOptions(
deleteOnBack: true,
),
),
],
onMatch: (matches) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {
_detectedMentions.clear();
_detectedHashtags.clear();
for (final match in matches) {
if (match.startsWith('@')) {
_detectedMentions.add(match);
} else if (match.startsWith('#')) {
_detectedHashtags.add(match);
}
}
});
}
});
},
);
// Email input with chip-like behavior
_emailController = RichifyController(
text: '',
blockCursorMovement: true, // Gmail-like behavior
matchers: [
RegexMatcher(
pattern: RegExp(r'\S+@\S+\.\S+'),
spanBuilder: (candidate) => WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 2, vertical: 2),
padding: const EdgeInsets.fromLTRB(10, 4, 4, 4),
decoration: BoxDecoration(
color: Colors.blue.shade100,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.blue.shade300),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
candidate.text,
style: const TextStyle(
color: Colors.blue,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(width: 4),
GestureDetector(
onTap: () {
// Remove the email by replacing it with empty string
final currentText = _emailController.text;
final newText = currentText
.replaceFirst(candidate.text, '')
.trim();
_emailController.text = newText;
_emailController.selection = TextSelection.collapsed(
offset: newText.length,
);
},
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: Icon(
Icons.close,
size: 16,
color: Colors.blue.shade700,
),
),
),
],
),
),
),
options: const TextMatcherOptions(
deleteOnBack: true,
),
),
],
onMatch: (matches) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {
_detectedEmails.clear();
_detectedEmails.addAll(matches);
});
}
});
},
);
// URL and phone number detector with interactions
_linkController = RichifyController(
text: 'Visit https://flutter.dev or call +1-234-567-8900',
matchers: [
// URL matcher with tap interaction
RegexMatcher(
pattern: RegExp(
r'https?://[^\s]+',
caseSensitive: false,
),
spanBuilder: (candidate) => WidgetSpan(
alignment: PlaceholderAlignment.baseline,
baseline: TextBaseline.alphabetic,
child: GestureDetector(
onTap: () {
setState(() {
_lastClickedItem = 'URL: ${candidate.text}';
});
// In a real app, you would launch the URL here
// e.g., launchUrl(Uri.parse(candidate.text));
},
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: Text(
candidate.text,
style: const TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline,
),
),
),
),
),
priority: 2, // Higher priority than phone numbers
),
// Phone number matcher with tap interaction
RegexMatcher(
pattern: RegExp(
r'\+?\d{1,3}[-.\s]?\(?\d{1,4}\)?[-.\s]?\d{1,4}[-.\s]?\d{1,9}',
),
spanBuilder: (candidate) => WidgetSpan(
alignment: PlaceholderAlignment.baseline,
baseline: TextBaseline.alphabetic,
child: GestureDetector(
onTap: () {
setState(() {
_lastClickedItem = 'Phone: ${candidate.text}';
});
// In a real app, you would launch the phone dialer here
// e.g., launchUrl(Uri.parse('tel:${candidate.text}'));
},
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: Text(
candidate.text,
style: const TextStyle(
color: Colors.orange,
fontWeight: FontWeight.w500,
decoration: TextDecoration.underline,
decorationStyle: TextDecorationStyle.dotted,
),
),
),
),
),
priority: 1,
),
],
);
}
@override
void dispose() {
_socialController.dispose();
_emailController.dispose();
_linkController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter Richify Demo'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Social media example
_buildSection(
title: 'Mentions & Hashtags',
subtitle: 'Social media style input',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: _socialController,
decoration: const InputDecoration(
hintText: 'Type @username or #hashtag...',
border: OutlineInputBorder(),
),
maxLines: 3,
),
if (_detectedMentions.isNotEmpty ||
_detectedHashtags.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Wrap(
spacing: 8,
runSpacing: 4,
children: [
if (_detectedMentions.isNotEmpty)
_buildChip(
'Mentions: ${_detectedMentions.join(', ')}',
Colors.blue,
),
if (_detectedHashtags.isNotEmpty)
_buildChip(
'Hashtags: ${_detectedHashtags.join(', ')}',
Colors.green,
),
],
),
),
],
),
),
const SizedBox(height: 24),
// Email input example
_buildSection(
title: 'Email Chips',
subtitle: 'Gmail-style recipient input',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: _emailController,
decoration: const InputDecoration(
hintText: 'Add recipients (e.g., [email protected])...',
border: OutlineInputBorder(),
),
maxLines: 2,
),
if (_detectedEmails.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8),
child: _buildChip(
'Recipients: ${_detectedEmails.length}',
Colors.blue,
),
),
],
),
),
const SizedBox(height: 24),
// URL and phone example
_buildSection(
title: 'Links & Phone Numbers',
subtitle: 'Click to interact',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: _linkController,
decoration: const InputDecoration(
hintText: 'Paste URLs or phone numbers...',
border: OutlineInputBorder(),
),
maxLines: 3,
),
if (_lastClickedItem.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(4),
border: Border.all(color: Colors.green.shade200),
),
child: Row(
children: [
Icon(
Icons.touch_app,
size: 16,
color: Colors.green.shade700,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'Clicked: $_lastClickedItem',
style: TextStyle(
color: Colors.green.shade900,
fontSize: 13,
),
),
),
],
),
),
),
],
),
),
],
),
),
);
}
Widget _buildSection({
required String title,
required String subtitle,
required Widget child,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
subtitle,
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
),
),
const SizedBox(height: 12),
child,
],
);
}
Widget _buildChip(String label, Color color) {
return Chip(
label: Text(label),
backgroundColor: color.withValues(alpha: 0.1),
labelStyle: TextStyle(
color: color,
fontSize: 12,
fontWeight: FontWeight.w500,
),
visualDensity: VisualDensity.compact,
);
}
}