I am trying to make a wats-app like link preview feature , it has two parts ,
Detect URL from text field
Showing preview of that URL
Part 2 has so many plugins to show the preview , but I am stuck with part 1 , how to detect and parse a URL on user typing on textfield .
Also is there a plugin serving both ?
Detect URLs in String/Paragraph and convert them in links in DART:
//
String convertStringToLink(String textData) {
//
final urlRegExp = new RegExp(
r"((https?:www\.)|(https?:\/\/)|(www\.))[-a-zA-Z0-9#:%._\+~#=]{1,256}\.[a-zA-Z0-9]{1,6}(\/[-a-zA-Z0-9()#:%_\+.~#?&\/=]*)?");
final urlMatches = urlRegExp.allMatches(textData);
List<String> urls = urlMatches.map(
(urlMatch) => textData.substring(urlMatch.start, urlMatch.end))
.toList();
List linksString = [];
urls.forEach((String linkText){
linksString.add(linkText);
});
if (linksString.length > 0) {
linksString.forEach((linkTextData) {
textData = textData.replaceAll(
linkTextData,
'<a href="' +
linkTextData +
'" target="_blank">' +
linkTextData +
'</a>');
});
}
return textData;
}
Demo and how to call
String text = "This is my website url: https://github.com/ Google search using: www.google.com, Flutter url: http://example.com/method?param=flutter stackoverflow website url is https://www.stackoverflow.com is greatest website and also check this hashed url https://support.google.com/firebase?authuser=0#topic=6399725";
print(convertStringToLink(text));
Output:
This is my website url: https://github.com/ Google search using: www.google.com, Flutter url: http://example.com/method?param=flutter stackoverflow website url is https://www.stackoverflow.com is greatest website and also check this hashed url https://support.google.com/firebase?authuser=0#topic=6399725
It worked for me, will definitely help my friends :)
I hope that this could help others, talking about step 1:
To detect a URL from the text view, I do the following (taking into consideration that my use case is a chat message in which in the middle of the text could be 1 or several links)
First, having a function that given a String, identify that are URLs:
bool hasURLs(String text) {
const pattern =
r"(https?:\/\/(www.)?|www.)([\w-]+.([\w-]+.)?[\w]+)([\w./?=%-]*)";
final regExp = RegExp(pattern);
return regExp.hasMatch(text);
}
Then a logic to display the text message with a link or without links:
final hasUrls = formatter.hasURLs(stringMessage);
In a widget:
return hasUrls
? UrlMessage(
textContent: messageContents,
textColor: textColor,
isMyMessage: isMyMessage,
)
: Text(
messageContents,
style: TextStyle(color: textColor, fontSize: 13),
);
For UrlMessage widget the code as follows:
class UrlMessage extends StatefulWidget {
const UrlMessage({
Key? key,
required this.textContent,
required this.textColor,
required this.isMyMessage,
}) : super(key: key);
final String textContent;
final Color textColor;
final bool isMyMessage;
#override
State<UrlMessage> createState() => _UrlMessageState();
}
class _UrlMessageState extends State<UrlMessage> {
final formatter = Formatter();
#override
Widget build(BuildContext context) {
final text = widget.textContent;
final textColor = widget.textColor;
final isMyMessage = widget.isMyMessage;
final linkTextStyle = TextStyle(
color: isMyMessage ? Colors.blueGrey : Colors.blue,
fontSize: 13,
fontWeight: FontWeight.bold,
decoration: TextDecoration.underline,
);
return RichText(
// map each word of the message, ask if it is a URL then set it with each
// TextSpan as a link or not. If it's a link use launchUrl from `url_launcher`
// package to open it
text: TextSpan(
children: text.split(' ').map((word) {
// Check for URLs
if (formatter.hasURLs(word)) {
return TextSpan(
text: word,
style: linkTextStyle,
recognizer: TapGestureRecognizer()
..onTap = () {
// Handle link - here we use `url_launcher`
launchUrl(Uri.parse(word));
},
);
} else {
return TextSpan(
text: '$word ',
style: TextStyle(color: textColor, fontSize: 13),
);
}
}).toList(),
),
);
}
}
Regarding step 2, there are several options to work with the preview, in our case Any Link Preview does what we need
You could try Uri.parse(). Do check the link https://www.geeksforgeeks.org/dart-uris/
Get the value from the textfield Using onChanged function and controller
https://medium.com/flutter-community/a-deep-dive-into-flutter-textfields-f0e676aaab7a
Related
I'm looking to implement dynamic text color with flutter-markdown using a custom syntax and parser. While the implementation below does successfully color text, I lose inner-text styling.
For example, the string
"not blue <span data-color='rgb(0,0,255)'>**blue text**</span> not blue"
shows up as
rather than showing up blue and bold.
While dynamic color isn't really the purpose of markdown, it would make my project considerably easier (I would use flutter-html, but the limits on selectable text make me hesitant)
My current implementation is as follows:
class FontColorSyntax extends md.InlineSyntax {
//Hideous => matches groups as follows: <span data-color="rgb(1,2,3)">4</span>
FontColorSyntax()
: super(
r'''<span(?:.*?)data-color=['"]rgb *\((?: *([0-9]{1,3}))[, ]+(?: *([0-9]{1,3}))[, ]+(?: *([0-9]{1,3}))[, ]*\)['"](?:.*?)>(.*?)</span>''');
#override
bool onMatch(md.InlineParser parse, Match match) {
md.Element colorTag = md.Element.text('fontcolor', match.group(4) ?? "matched text");
colorTag.attributes["fontColorRed"] = match.group(1) ?? "0";
colorTag.attributes["fontColorGreen"] = match.group(2) ?? "0";
colorTag.attributes["fontColorBlue"] = match.group(3) ?? "0";
parse.addNode(colorTag);
return true;
}
}
class FontColorBuilder extends MarkdownElementBuilder {
int getColor(md.Element el, String color) =>
int.tryParse(el.attributes["fontColor$color"] ?? "0") ?? 0;
#override
Widget visitElementAfter(element, style) {
final red = getColor(element, "Red");
final green = getColor(element, "Green");
final blue = getColor(element, "Blue");
return SelectableText.rich(
TextSpan(
text: element.textContent,
style: TextStyle(
color: Color.fromRGBO(red, green, blue, 1),
)),
);
}
I'm still quite new to flutter/dart, so please forgive (but feel free to point out) any oddities in my code.
Instead of returning a SelectableText returning another MarkdownBody widget should do the trick:
return MarkdownBody(
styleSheet: MarkdownStyleSheet.fromTheme(ThemeData(
textTheme:
const TextTheme(bodyText2: TextStyle(
color: Color.fromRGBO(red, green, blue, 1)
)))),
data: element.textContent);
I wanted to display hashtags and valid mentions with different colors in the text.
I got helped with this code which works only for hashtags
RichText _convertHashtag(String text) {
List<String> split = text.split(RegExp("#"));
List<String> hashtags = split.getRange(1, split.length).fold([], (t, e) {
var texts = e.split(" ");
if (texts.length > 1) {
return List.from(t)
..addAll(["#${texts.first}", "${e.substring(texts.first.length)}"]);
}
return List.from(t)..add("#${texts.first}");
});
return RichText(
text: TextSpan(
children: [TextSpan(text: split.first)]..addAll(hashtags
.map((text) => text.contains("#")
? TextSpan(text: text, style: TextStyle(color: Colors.blue))
: TextSpan(text: text))
.toList()),
),
);
}
I modified it like:
List valid_mentions = ['#mention1', '#mention2'];//these are the valid mention
RichText _convertHashtag(String text) {
List<String> split = text.split(RegExp("#|#"));
List<String> hashtags = split.getRange(1, split.length).fold([], (t, e) {
var texts = e.split(" ");
//here adding `#` sign and `#` sign to the given texts and storing them in the `hashtags` list
if (texts.length > 1) {
if (valid_mentions.contains(texts.first))
return List.from(t)
..addAll(["#${texts.first}", "${e.substring(texts.first.length)}"]);
else if (text.contains('#${texts.first}')) {
return List.from(t)
..addAll(["#${texts.first}", "${e.substring(texts.first.length)}"]);
} else
return List.from(t)
..addAll(["#${texts.first}", "${e.substring(texts.first.length)}"]);
} else {
if (valid_mentions.contains(texts.first))
return List.from(t)..addAll(["#${texts.first}"]);
else if (text.contains('#${texts.first}')) {
return List.from(t)..addAll(["#${texts.first}"]);
} else
return List.from(t)..addAll(["#${texts.first}"]);
}
});
return RichText(
text: TextSpan(
children: [TextSpan(text: split.first)]..addAll(hashtags.map((text) {
return text.contains("#")
? valid_mentions.contains(text)
? //checking if the mention is valid
TextSpan(
text: text,
recognizer: TapGestureRecognizer()
..onTap = () {
print(text);
},
style: TextStyle(color: Colors.blue))
: TextSpan(
text: text,
)
: text.contains("#")
? TextSpan(
text: text,
recognizer: TapGestureRecognizer()
..onTap = () {
print(text);
},
style: TextStyle(color: Colors.blue))
: TextSpan(
text: text,
);
}).toList()),
),
);
}
I am able to make the required changes, but i belive its not an optimized way and there is a lot of boiler plate code. How can i optimize it?
input:"I love #flutter #android #mention1 #mention2 #mention3 "
output:"I love #flutter #android #mention1 #mention2 #mention3 "
here #mention3 is not hyperlinked because its not an valid mention.
From what I've understood, you want to highlight hashtags and mentions (pre stored in a list) in a String.
Let me break this into 2 parts, the first would be to extract the hashtags & mentions from the text.
List<String> getAllHashtags(String text) {
final regexp = RegExp(r'\#[a-zA-Z0-9]+\b()');
List<String> hashtags = [];
regexp.allMatches(text).forEach((element) {
if (element.group(0) != null) {
hashtags.add(element.group(0).toString());
}
});
return hashtags;
}
List<String> getAllMentions(String text) {
final regexp = RegExp(r'\#[a-zA-Z0-9]+\b()');
List<String> mentions = [];
regexp.allMatches(text).forEach((element) {
if (element.group(0) != null) {
mentions.add(element.group(0).toString());
}
});
return mentions;
}
The above code snippet will successfully extract hashtags & mentions from the given sentence and return it as a list.
The next step would be to build the RichText with the different TextSpans.
RichText buildHighlightedText(String text) {
// clean the text
text = cleanText(text);
List<String> validMentions = ["#mention1", "#mention2"];
List<String> hashtags = getAllHashtags(text);
List<String> mentions = getAllMentions(text);
List<TextSpan> textSpans = [];
text.split(" ").forEach((value) {
if (hashtags.contains(value)) {
textSpans.add(TextSpan(
text: '$value ',
style: TextStyle(color: Colors.amber, fontWeight: FontWeight.bold),
));
} else if (mentions.contains(value) && validMentions.contains(value)) {
textSpans.add(TextSpan(
text: '$value ',
style: TextStyle(color: Colors.blue, fontWeight: FontWeight.bold),
));
} else {
textSpans.add(TextSpan(text: '$value '));
}
});
return RichText(text: TextSpan(children: textSpans));
}
The text has been split by empty spaces, and filters hashtags/mentions and returns differently styled TextSpans for each. This is a more concise and cleaner way of doing what you're looking for.
Here's an example:
Edit:
In order to parse hashtags without spaces, we need to add each instance with a space in front.
String cleanText(String text) {
text = text.replaceAllMapped(
RegExp(r'\w#+'), (Match m) => "${m[0]?.split('').join(" ")}");
text = text.replaceAllMapped(
RegExp(r'\w#+'), (Match m) => "${m[0]?.split('').join(" ")}");
return text;
}
Android has horrid emojis. Can I use Apple's emoji font (like Telegram does and WhatsApp used to) in Flutter for Android?
Edit: My first guess would be to add a font, but as I do not want to change all other fonts I am not sure if this is possible.
There is currently no way to define a custom font in Flutter just for emojis (or just for specified characters). There's an open Github issue about this.
There is however a trick to mix your font and another one for your emojis in a Text widget. I found an answer here to do that.
Here are the steps you can follow :
You need to choose the emoji font you want to use. My favorite one is this Joypixels :
https://www.joypixels.com/fonts
Download the font .ttf and add it to your project, following the documentation :
https://flutter.dev/docs/cookbook/design/fonts
Add this code to your project :
class TextWithEmojis extends StatelessWidget {
const TextWithEmojis({
Key? key,
required this.text,
required this.fontSize,
required this.color,
}) : super(key: key);
final String text;
final double fontSize;
final Color color;
#override
Widget build(BuildContext context) {
return RichText(
text: _buildText(),
);
}
TextSpan _buildText() {
final children = <TextSpan>[];
final runes = text.runes;
for (int i = 0; i < runes.length; /* empty */) {
int current = runes.elementAt(i);
// we assume that everything that is not
// in Extended-ASCII set is an emoji...
final isEmoji = current > 255;
final shouldBreak = isEmoji ? (x) => x <= 255 : (x) => x > 255;
final chunk = <int>[];
while (!shouldBreak(current)) {
chunk.add(current);
if (++i >= runes.length) break;
current = runes.elementAt(i);
}
children.add(
TextSpan(
text: String.fromCharCodes(chunk),
style: TextStyle(
fontSize: fontSize,
color: color,
fontFamily: isEmoji ? 'Joypixels' : null,
),
),
);
}
return TextSpan(children: children);
}
}
You can now use TextWithEmojis whenever you need :
TextWithEmojis(
text: "Hello 👋 😄",
fontSize: 12,
color: Colors.white
)
Hot reloading or Hot restaring won't make the Text go to the incorrect state. Only running the app again.
I wonder how to create a recognizer to detect # in the text & change its color to something like a blue one and also add onTap functionality to it :) like this picture.
I think this should work for anything like #someone:
You need to import this: import 'package:flutter/gestures.dart';
This is your input String: String input = "This is a long text but in between there should be this: #someone The text continues here, but there is #another.";
This is the widget I made for you : textSplitter(input);
Widget textSplitter(String input) {
List<TextSpan> textSpans = [];
for (var i = 0; i <= '#'.allMatches(input).length; i++) {
String part = input.substring(0, input.indexOf('#')==-1? input.length : input.indexOf('#'));
textSpans.add(TextSpan(text: part, style: TextStyle(color: Colors.white)));
input = input.substring(input.indexOf('#'));
String clickable = input.substring(0, input.indexOf(' ')==-1? input.length : input.indexOf(' '));
textSpans.add(
TextSpan(
text: clickable,
style: TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline,
),
recognizer: new TapGestureRecognizer()
..onTap = () {
//do stuff here with string clickable
print(clickable);
},
),
);
input = input.substring(input.indexOf(' ')==-1? input.length : input.indexOf(' '));
}
return RichText(
text: TextSpan(
children: textSpans,
),
);
}
The result should look like this:
Note that anything after the # is included until a space.
I'm trying to display a PDF file in Flutter which I have previously downloaded from a server.
I have tried both flutter_full_pdf_viewer and advance_pdf_viewer. Both libs show me the correct number of pages, but the pages are all white.
Does anybody have an idea why? makes no difference if I run it on iOS or Android or in emulator or real device.
class _PdfPageState extends State<PdfPage> {
String pathPDF = "";
File file;
PDFDocument doc = null;
#override
void initState() {
super.initState();
WeeklyReportsRepository( userRepository: UserRepository()).loadWeeklyReport(widget.weeklyReport.filename).then((file) {
setDoc(file);
});
}
Future<void> setDoc(File file) async {
var doc1 = await PDFDocument.fromFile(file);
setState(() {
doc = doc1;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
iconTheme: IconThemeData(color: Colors.white),
title: Text(
"Wochenbericht",
style: TextStyle(
color: Colors.white,
),
),
),
body: Container(
color: Theme
.of(context)
.backgroundColor,
child: (doc == null) ? Center(child: CircularProgressIndicator()) :
PDFViewer(document: doc,
scrollDirection: Axis.vertical,),
),
);
}
}
Everything seems right but can you try to view a pdf from your computer or a link and also try to view specific page of the pdf. Package document try load from assets, URL, or file as shown in the link if these ways work you have a problem from server side.
In my case, it is the file name which was in UTF-8 characters. Once I changed the pdf file name to english letters, advance_pdf_viewer can read it with or without .pdf extension.
Make sure the Pdf File is alphabet file, not a pic converted to a pdf.