449 lines
13 KiB
Dart
449 lines
13 KiB
Dart
import 'dart:async';
|
|
import 'dart:math' as math;
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:stream_chat_flutter/src/stream_attachment_package.dart';
|
|
import 'package:stream_chat_flutter/src/extension.dart';
|
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
|
|
/// Launch URL
|
|
Future<void> launchURL(BuildContext context, String url) async {
|
|
if (await canLaunch(url)) {
|
|
await launch(url);
|
|
} else {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text(context.translations.launchUrlError)),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Shows confirmation dialog
|
|
Future<bool?> showConfirmationDialog(
|
|
BuildContext context, {
|
|
required String title,
|
|
required String okText,
|
|
Widget? icon,
|
|
String? question,
|
|
String? cancelText,
|
|
}) {
|
|
final chatThemeData = StreamChatTheme.of(context);
|
|
return showModalBottomSheet(
|
|
useRootNavigator: false,
|
|
backgroundColor: chatThemeData.colorTheme.barsBg,
|
|
context: context,
|
|
shape: const RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.only(
|
|
topLeft: Radius.circular(16),
|
|
topRight: Radius.circular(16),
|
|
),
|
|
),
|
|
builder: (context) {
|
|
final effect = chatThemeData.colorTheme.borderTop;
|
|
return SafeArea(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const SizedBox(height: 26),
|
|
if (icon != null) icon,
|
|
const SizedBox(height: 26),
|
|
Text(
|
|
title,
|
|
style: chatThemeData.textTheme.headlineBold,
|
|
),
|
|
const SizedBox(height: 7),
|
|
if (question != null)
|
|
Text(
|
|
question,
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 36),
|
|
Container(
|
|
color: effect.color!.withOpacity(effect.alpha ?? 1),
|
|
height: 1,
|
|
),
|
|
Row(
|
|
children: [
|
|
if (cancelText != null)
|
|
Flexible(
|
|
child: Container(
|
|
alignment: Alignment.center,
|
|
child: TextButton(
|
|
onPressed: () {
|
|
Navigator.of(context).pop(false);
|
|
},
|
|
child: Text(
|
|
cancelText,
|
|
style: chatThemeData.textTheme.bodyBold.copyWith(
|
|
color: chatThemeData.colorTheme.textHighEmphasis
|
|
.withOpacity(0.5),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Flexible(
|
|
child: Container(
|
|
alignment: Alignment.center,
|
|
child: TextButton(
|
|
onPressed: () {
|
|
Navigator.pop(context, true);
|
|
},
|
|
child: Text(
|
|
okText,
|
|
style: chatThemeData.textTheme.bodyBold.copyWith(
|
|
color: chatThemeData.colorTheme.accentError,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Shows info dialog
|
|
Future<bool?> showInfoDialog(
|
|
BuildContext context, {
|
|
required String title,
|
|
required String okText,
|
|
Widget? icon,
|
|
String? details,
|
|
StreamChatThemeData? theme,
|
|
}) {
|
|
final chatThemeData = StreamChatTheme.of(context);
|
|
return showModalBottomSheet(
|
|
useRootNavigator: false,
|
|
backgroundColor:
|
|
theme?.colorTheme.barsBg ?? chatThemeData.colorTheme.barsBg,
|
|
context: context,
|
|
shape: const RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.only(
|
|
topLeft: Radius.circular(16),
|
|
topRight: Radius.circular(16),
|
|
),
|
|
),
|
|
builder: (context) => SafeArea(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const SizedBox(
|
|
height: 26,
|
|
),
|
|
if (icon != null) icon,
|
|
const SizedBox(
|
|
height: 26,
|
|
),
|
|
Text(
|
|
title,
|
|
style: theme?.textTheme.headlineBold ??
|
|
chatThemeData.textTheme.headlineBold,
|
|
),
|
|
const SizedBox(
|
|
height: 7,
|
|
),
|
|
if (details != null) Text(details),
|
|
const SizedBox(
|
|
height: 36,
|
|
),
|
|
Container(
|
|
color: theme?.colorTheme.textHighEmphasis.withOpacity(0.08) ??
|
|
chatThemeData.colorTheme.textHighEmphasis.withOpacity(0.08),
|
|
height: 1,
|
|
),
|
|
Center(
|
|
child: TextButton(
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
},
|
|
child: Text(
|
|
okText,
|
|
style: TextStyle(
|
|
color: theme?.colorTheme.textHighEmphasis.withOpacity(0.5) ??
|
|
chatThemeData.colorTheme.accentPrimary,
|
|
fontWeight: FontWeight.w400,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Get random png with initials
|
|
String getRandomPicUrl(User user) =>
|
|
'https://getstream.io/random_png/?id=${user.id}&name=${user.name}';
|
|
|
|
/// Get websiteName from [hostName]
|
|
String? getWebsiteName(String hostName) {
|
|
switch (hostName) {
|
|
case 'reddit':
|
|
return 'Reddit';
|
|
case 'youtube':
|
|
return 'Youtube';
|
|
case 'wikipedia':
|
|
return 'Wikipedia';
|
|
case 'twitter':
|
|
return 'Twitter';
|
|
case 'facebook':
|
|
return 'Facebook';
|
|
case 'amazon':
|
|
return 'Amazon';
|
|
case 'yelp':
|
|
return 'Yelp';
|
|
case 'imdb':
|
|
return 'IMDB';
|
|
case 'pinterest':
|
|
return 'Pinterest';
|
|
case 'tripadvisor':
|
|
return 'TripAdvisor';
|
|
case 'instagram':
|
|
return 'Instagram';
|
|
case 'walmart':
|
|
return 'Walmart';
|
|
case 'craigslist':
|
|
return 'Craigslist';
|
|
case 'ebay':
|
|
return 'eBay';
|
|
case 'linkedin':
|
|
return 'LinkedIn';
|
|
case 'google':
|
|
return 'Google';
|
|
case 'apple':
|
|
return 'Apple';
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// A method returns a human readable string representing a file _size
|
|
String fileSize(dynamic size, [int round = 2]) {
|
|
if (size == null) return 'Size N/A';
|
|
|
|
/**
|
|
* [size] can be passed as number or as string
|
|
*
|
|
* the optional parameter [round] specifies the number
|
|
* of digits after comma/point (default is 2)
|
|
*/
|
|
const divider = 1024;
|
|
int _size;
|
|
try {
|
|
_size = int.parse(size.toString());
|
|
} catch (e) {
|
|
throw ArgumentError('Can not parse the size parameter: $e');
|
|
}
|
|
|
|
if (_size < divider) {
|
|
return '$_size B';
|
|
}
|
|
|
|
if (_size < divider * divider && _size % divider == 0) {
|
|
return '${(_size / divider).toStringAsFixed(0)} KB';
|
|
}
|
|
|
|
if (_size < divider * divider) {
|
|
return '${(_size / divider).toStringAsFixed(round)} KB';
|
|
}
|
|
|
|
if (_size < divider * divider * divider && _size % divider == 0) {
|
|
return '${(_size / (divider * divider)).toStringAsFixed(0)} MB';
|
|
}
|
|
|
|
if (_size < divider * divider * divider) {
|
|
return '${(_size / divider / divider).toStringAsFixed(round)} MB';
|
|
}
|
|
|
|
if (_size < divider * divider * divider * divider && _size % divider == 0) {
|
|
return '${(_size / (divider * divider * divider)).toStringAsFixed(0)} GB';
|
|
}
|
|
|
|
if (_size < divider * divider * divider * divider) {
|
|
return '${(_size / divider / divider / divider).toStringAsFixed(round)} GB';
|
|
}
|
|
|
|
if (_size < divider * divider * divider * divider * divider &&
|
|
_size % divider == 0) {
|
|
final num r = _size / divider / divider / divider / divider;
|
|
return '${r.toStringAsFixed(0)} TB';
|
|
}
|
|
|
|
if (_size < divider * divider * divider * divider * divider) {
|
|
final num r = _size / divider / divider / divider / divider;
|
|
return '${r.toStringAsFixed(round)} TB';
|
|
}
|
|
|
|
if (_size < divider * divider * divider * divider * divider * divider &&
|
|
_size % divider == 0) {
|
|
final num r = _size / divider / divider / divider / divider / divider;
|
|
return '${r.toStringAsFixed(0)} PB';
|
|
} else {
|
|
final num r = _size / divider / divider / divider / divider / divider;
|
|
return '${r.toStringAsFixed(round)} PB';
|
|
}
|
|
}
|
|
|
|
///
|
|
StreamSvgIcon getFileTypeImage(String? type) {
|
|
switch (type) {
|
|
case '7z':
|
|
return StreamSvgIcon.filetype7z();
|
|
case 'csv':
|
|
return StreamSvgIcon.filetypeCsv();
|
|
case 'doc':
|
|
return StreamSvgIcon.filetypeDoc();
|
|
case 'docx':
|
|
return StreamSvgIcon.filetypeDocx();
|
|
case 'html':
|
|
return StreamSvgIcon.filetypeHtml();
|
|
case 'md':
|
|
return StreamSvgIcon.filetypeMd();
|
|
case 'odt':
|
|
return StreamSvgIcon.filetypeOdt();
|
|
case 'pdf':
|
|
return StreamSvgIcon.filetypePdf();
|
|
case 'ppt':
|
|
return StreamSvgIcon.filetypePpt();
|
|
case 'pptx':
|
|
return StreamSvgIcon.filetypePptx();
|
|
case 'rar':
|
|
return StreamSvgIcon.filetypeRar();
|
|
case 'rtf':
|
|
return StreamSvgIcon.filetypeRtf();
|
|
case 'tar':
|
|
return StreamSvgIcon.filetypeTar();
|
|
case 'txt':
|
|
return StreamSvgIcon.filetypeTxt();
|
|
case 'xls':
|
|
return StreamSvgIcon.filetypeXls();
|
|
case 'xlsx':
|
|
return StreamSvgIcon.filetypeXlsx();
|
|
case 'zip':
|
|
return StreamSvgIcon.filetypeZip();
|
|
default:
|
|
return StreamSvgIcon.filetypeGeneric();
|
|
}
|
|
}
|
|
|
|
/// Wraps attachment widget with custom shape
|
|
Widget wrapAttachmentWidget(
|
|
BuildContext context,
|
|
Widget attachmentWidget,
|
|
ShapeBorder attachmentShape,
|
|
// ignore: avoid_positional_boolean_parameters
|
|
bool reverse,
|
|
) =>
|
|
Material(
|
|
clipBehavior: Clip.hardEdge,
|
|
shape: attachmentShape,
|
|
type: MaterialType.transparency,
|
|
child: attachmentWidget,
|
|
);
|
|
|
|
/// Represents a 2-tuple, or pair.
|
|
class Tuple2<T1, T2> {
|
|
/// Creates a new tuple value with the specified items.
|
|
const Tuple2(this.item1, this.item2);
|
|
|
|
/// Create a new tuple value with the specified list [items].
|
|
factory Tuple2.fromList(List items) {
|
|
if (items.length != 2) {
|
|
throw ArgumentError('items must have length 2');
|
|
}
|
|
|
|
return Tuple2<T1, T2>(items[0] as T1, items[1] as T2);
|
|
}
|
|
|
|
/// Returns the first item of the tuple
|
|
final T1 item1;
|
|
|
|
/// Returns the second item of the tuple
|
|
final T2 item2;
|
|
|
|
/// Returns a tuple with the first item set to the specified value.
|
|
Tuple2<T1, T2> withItem1(T1 v) => Tuple2<T1, T2>(v, item2);
|
|
|
|
/// Returns a tuple with the second item set to the specified value.
|
|
Tuple2<T1, T2> withItem2(T2 v) => Tuple2<T1, T2>(item1, v);
|
|
|
|
/// Creates a [List] containing the items of this [Tuple2].
|
|
///
|
|
/// The elements are in item order. The list is variable-length
|
|
/// if [growable] is true.
|
|
List toList({bool growable = false}) =>
|
|
List.from([item1, item2], growable: growable);
|
|
|
|
@override
|
|
String toString() => '[$item1, $item2]';
|
|
|
|
@override
|
|
bool operator ==(Object other) =>
|
|
identical(this, other) ||
|
|
other is Tuple2 &&
|
|
runtimeType == other.runtimeType &&
|
|
item1 == other.item1 &&
|
|
item2 == other.item2;
|
|
|
|
@override
|
|
int get hashCode => item1.hashCode ^ item2.hashCode;
|
|
}
|
|
|
|
/// Levenshtein algorithm implementation based on:
|
|
/// http://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows
|
|
int levenshtein(String s, String t, {bool caseSensitive = true}) {
|
|
if (!caseSensitive) {
|
|
// ignore: parameter_assignments
|
|
s = s.toLowerCase();
|
|
// ignore: parameter_assignments
|
|
t = t.toLowerCase();
|
|
}
|
|
if (s == t) return 0;
|
|
if (s.isEmpty) return t.length;
|
|
if (t.isEmpty) return s.length;
|
|
|
|
final v0 = List<int>.filled(t.length + 1, 0);
|
|
final v1 = List<int>.filled(t.length + 1, 0);
|
|
|
|
for (var i = 0; i < t.length + 1; i < i++) {
|
|
v0[i] = i;
|
|
}
|
|
|
|
for (var i = 0; i < s.length; i++) {
|
|
v1[0] = i + 1;
|
|
|
|
for (var j = 0; j < t.length; j++) {
|
|
final cost = (s[i] == t[j]) ? 0 : 1;
|
|
v1[j + 1] = math.min(v1[j] + 1, math.min(v0[j + 1] + 1, v0[j] + cost));
|
|
}
|
|
|
|
for (var j = 0; j < t.length + 1; j++) {
|
|
v0[j] = v1[j];
|
|
}
|
|
}
|
|
|
|
return v1[t.length];
|
|
}
|
|
|
|
/// An easy way to handle attachment related operations on a message
|
|
extension AttachmentPackagesX on Message {
|
|
/// This extension will return a List of type [StreamAttachmentPackage] from the
|
|
/// existing attachments of the message
|
|
List<StreamAttachmentPackage> getAttachmentPackageList() {
|
|
final _attachmentPackages = List<StreamAttachmentPackage>.generate(
|
|
attachments.length,
|
|
(index) => StreamAttachmentPackage(
|
|
attachment: attachments[index],
|
|
message: this,
|
|
),
|
|
);
|
|
return _attachmentPackages;
|
|
}
|
|
}
|