feat: integrate chat voice and diagnostics client
This commit is contained in:
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../services/link_trust_service.dart';
|
||||
import '../services/ts3_server_link.dart';
|
||||
|
||||
final _tagRe = RegExp(
|
||||
r'\[(\/?(?:b|i|u|s'
|
||||
@@ -13,12 +14,14 @@ final _tagRe = RegExp(
|
||||
r'))\]',
|
||||
caseSensitive: false,
|
||||
);
|
||||
final _colorRe = RegExp(r'color=([#\w]+)');
|
||||
final _sizeRe = RegExp(r'size=(\d+)');
|
||||
final _urlRe = RegExp(r'url=(.+)');
|
||||
final _urlRe = RegExp(r'url=(.+)', caseSensitive: false);
|
||||
final _imgRe = RegExp(r'img=(.+)');
|
||||
final _urlAutoRe = RegExp(r'(?:\[url\])?(https?://[^\s\[\]]+)(?:\[/url\])?', caseSensitive: false);
|
||||
final _urlAutoRe = RegExp(
|
||||
r'(?:\[url\])?((?:https?|ts3server)://[^\s\[\]]+)(?:\[/url\])?',
|
||||
caseSensitive: false,
|
||||
);
|
||||
final _closeUrlRe = RegExp(r'\[/url\]', caseSensitive: false);
|
||||
const _linkStyle = TextStyle(color: Colors.blue);
|
||||
|
||||
int? _findCloseUrl(String src, int from) {
|
||||
final m = _closeUrlRe.matchAsPrefix(src, from);
|
||||
@@ -42,10 +45,16 @@ Color? _parseColor(String hex) {
|
||||
}
|
||||
|
||||
class BbCodeText extends StatelessWidget {
|
||||
const BbCodeText(this.text, {super.key, required this.linkTrust});
|
||||
const BbCodeText(
|
||||
this.text, {
|
||||
super.key,
|
||||
required this.linkTrust,
|
||||
this.onTs3ServerLink,
|
||||
});
|
||||
|
||||
final String text;
|
||||
final LinkTrustService linkTrust;
|
||||
final Ts3ServerLinkHandler? onTs3ServerLink;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -63,20 +72,17 @@ class BbCodeText extends StatelessWidget {
|
||||
parts.add(TextSpan(text: src.substring(last, m.start)));
|
||||
}
|
||||
final url = m.group(1)!;
|
||||
parts.add(WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: _LinkTap(
|
||||
url: url,
|
||||
linkTrust: linkTrust,
|
||||
child: Text(
|
||||
url,
|
||||
style: const TextStyle(
|
||||
color: Colors.blue,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
parts.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: _LinkTap(
|
||||
url: url,
|
||||
linkTrust: linkTrust,
|
||||
onTs3ServerLink: onTs3ServerLink,
|
||||
child: Text(url, style: _linkStyle),
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
last = m.end;
|
||||
}
|
||||
if (last < src.length) {
|
||||
@@ -118,19 +124,21 @@ class BbCodeText extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
spans.add(TextSpan(
|
||||
text: t,
|
||||
style: TextStyle(
|
||||
fontWeight: bold ? FontWeight.bold : null,
|
||||
fontStyle: italic ? FontStyle.italic : null,
|
||||
decoration: TextDecoration.combine([
|
||||
if (underline) TextDecoration.underline,
|
||||
if (strikethrough) TextDecoration.lineThrough,
|
||||
]),
|
||||
color: color,
|
||||
fontSize: size,
|
||||
spans.add(
|
||||
TextSpan(
|
||||
text: t,
|
||||
style: TextStyle(
|
||||
fontWeight: bold ? FontWeight.bold : null,
|
||||
fontStyle: italic ? FontStyle.italic : null,
|
||||
decoration: TextDecoration.combine([
|
||||
if (underline) TextDecoration.underline,
|
||||
if (strikethrough) TextDecoration.lineThrough,
|
||||
]),
|
||||
color: color,
|
||||
fontSize: size,
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
}
|
||||
|
||||
final buf = StringBuffer();
|
||||
@@ -150,7 +158,8 @@ class BbCodeText extends StatelessWidget {
|
||||
}
|
||||
|
||||
flush(buf);
|
||||
final raw = m.group(1)!.toLowerCase();
|
||||
final rawTag = m.group(1)!;
|
||||
final raw = rawTag.toLowerCase();
|
||||
i = m.end;
|
||||
|
||||
if (raw.startsWith('/')) {
|
||||
@@ -184,7 +193,7 @@ class BbCodeText extends StatelessWidget {
|
||||
if (raw.startsWith('color=') || raw.startsWith('size=')) {
|
||||
tags.add(raw);
|
||||
} else if (raw.startsWith('url=')) {
|
||||
final url = _urlRe.firstMatch(raw)?.group(1) ?? '';
|
||||
final url = _urlRe.firstMatch(rawTag)?.group(1) ?? '';
|
||||
final closeIdx = _findCloseUrl(src, i);
|
||||
String inner;
|
||||
if (closeIdx != null) {
|
||||
@@ -193,53 +202,49 @@ class BbCodeText extends StatelessWidget {
|
||||
} else {
|
||||
inner = url;
|
||||
}
|
||||
spans.add(WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: _LinkTap(
|
||||
url: url.isNotEmpty ? url : inner,
|
||||
linkTrust: linkTrust,
|
||||
child: Text(
|
||||
inner,
|
||||
style: const TextStyle(
|
||||
color: Colors.blue,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
spans.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: _LinkTap(
|
||||
url: url.isNotEmpty ? url : inner,
|
||||
linkTrust: linkTrust,
|
||||
onTs3ServerLink: onTs3ServerLink,
|
||||
child: Text(inner, style: _linkStyle),
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
} else if (raw.startsWith('img=')) {
|
||||
final src2 = _imgRe.firstMatch(raw)?.group(1) ?? '';
|
||||
if (src2.isNotEmpty) {
|
||||
spans.add(WidgetSpan(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.network(
|
||||
Uri.tryParse(src2)?.toString() ?? src2,
|
||||
fit: BoxFit.scaleDown,
|
||||
errorBuilder: (_, __, ___) => const SizedBox.shrink(),
|
||||
spans.add(
|
||||
WidgetSpan(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.network(
|
||||
Uri.tryParse(src2)?.toString() ?? src2,
|
||||
fit: BoxFit.scaleDown,
|
||||
errorBuilder: (_, _, _) => const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
}
|
||||
} else if (raw == 'url') {
|
||||
final closeIdx = _findCloseUrl(src, i);
|
||||
if (closeIdx != null) {
|
||||
final url = src.substring(i, closeIdx).trim();
|
||||
i = closeIdx + 6;
|
||||
spans.add(WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: _LinkTap(
|
||||
url: url,
|
||||
linkTrust: linkTrust,
|
||||
child: Text(
|
||||
url,
|
||||
style: const TextStyle(
|
||||
color: Colors.blue,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
spans.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: _LinkTap(
|
||||
url: url,
|
||||
linkTrust: linkTrust,
|
||||
onTs3ServerLink: onTs3ServerLink,
|
||||
child: Text(url, style: _linkStyle),
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -258,11 +263,13 @@ class _LinkTap extends StatefulWidget {
|
||||
required this.url,
|
||||
required this.child,
|
||||
required this.linkTrust,
|
||||
this.onTs3ServerLink,
|
||||
});
|
||||
|
||||
final String url;
|
||||
final Widget child;
|
||||
final LinkTrustService linkTrust;
|
||||
final Ts3ServerLinkHandler? onTs3ServerLink;
|
||||
|
||||
@override
|
||||
State<_LinkTap> createState() => _LinkTapState();
|
||||
@@ -284,6 +291,12 @@ class _LinkTapState extends State<_LinkTap> {
|
||||
void _onChanged() => mounted ? setState(() {}) : null;
|
||||
|
||||
Future<void> _open() async {
|
||||
final ts3Link = parseTs3ServerLink(widget.url);
|
||||
if (ts3Link != null) {
|
||||
await widget.onTs3ServerLink?.call(ts3Link);
|
||||
return;
|
||||
}
|
||||
|
||||
final uri = Uri.tryParse(widget.url);
|
||||
if (uri == null) return;
|
||||
final host = uri.host;
|
||||
@@ -305,9 +318,6 @@ class _LinkTapState extends State<_LinkTap> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: _open,
|
||||
child: widget.child,
|
||||
);
|
||||
return GestureDetector(onTap: _open, child: widget.child);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user