mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 12:08:41 +00:00
fixing compression issue
This commit is contained in:
parent
e9502c7ce9
commit
59528bf082
11 changed files with 140 additions and 143 deletions
|
|
@ -45,7 +45,7 @@ final lockRetransStore = Mutex();
|
||||||
/// It handles errors and does automatically tries to reconnect on
|
/// It handles errors and does automatically tries to reconnect on
|
||||||
/// errors or network changes.
|
/// errors or network changes.
|
||||||
class ApiService {
|
class ApiService {
|
||||||
final String apiHost = (kDebugMode) ? "192.168.178.89:3030" : "api.twonly.eu";
|
final String apiHost = (kDebugMode) ? "10.99.0.140:3030" : "api.twonly.eu";
|
||||||
final String apiSecure = (kDebugMode) ? "" : "s";
|
final String apiSecure = (kDebugMode) ? "" : "s";
|
||||||
|
|
||||||
bool appIsOutdated = false;
|
bool appIsOutdated = false;
|
||||||
|
|
|
||||||
|
|
@ -299,7 +299,7 @@ Future<File?> getVideoPath(int mediaId) async {
|
||||||
Future<Uint8List?> readMediaFile(int mediaId, String type) async {
|
Future<Uint8List?> readMediaFile(int mediaId, String type) async {
|
||||||
String basePath = await getMediaFilePath(mediaId, "received");
|
String basePath = await getMediaFilePath(mediaId, "received");
|
||||||
File file = File("$basePath.$type");
|
File file = File("$basePath.$type");
|
||||||
Log.info("Reading: ${file}");
|
Log.info("Reading: $file");
|
||||||
if (!await file.exists()) {
|
if (!await file.exists()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -200,17 +200,25 @@ Future<bool> addVideoToUpload(int mediaUploadId, File videoFilePath) async {
|
||||||
Future<Uint8List> addOrModifyImageToUpload(
|
Future<Uint8List> addOrModifyImageToUpload(
|
||||||
int mediaUploadId, Uint8List imageBytes) async {
|
int mediaUploadId, Uint8List imageBytes) async {
|
||||||
Uint8List imageBytesCompressed;
|
Uint8List imageBytesCompressed;
|
||||||
|
|
||||||
|
Stopwatch stopwatch = Stopwatch();
|
||||||
|
stopwatch.start();
|
||||||
|
|
||||||
|
Log.info("Raw images size in bytes: ${imageBytes.length}");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
imageBytesCompressed = await FlutterImageCompress.compressWithList(
|
imageBytesCompressed = await FlutterImageCompress.compressWithList(
|
||||||
format: CompressFormat.png,
|
format: CompressFormat.webp,
|
||||||
|
// minHeight: 0,
|
||||||
|
// minWidth: 0,
|
||||||
imageBytes,
|
imageBytes,
|
||||||
quality: 90,
|
quality: 90,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (imageBytesCompressed.length >= 2 * 1000 * 1000) {
|
if (imageBytesCompressed.length >= 1 * 1000 * 1000) {
|
||||||
// if the media file is over 2MB compress it with 60%
|
// if the media file is over 2MB compress it with 60%
|
||||||
imageBytesCompressed = await FlutterImageCompress.compressWithList(
|
imageBytesCompressed = await FlutterImageCompress.compressWithList(
|
||||||
format: CompressFormat.png,
|
format: CompressFormat.webp,
|
||||||
imageBytes,
|
imageBytes,
|
||||||
quality: 60,
|
quality: 60,
|
||||||
);
|
);
|
||||||
|
|
@ -223,6 +231,26 @@ Future<Uint8List> addOrModifyImageToUpload(
|
||||||
imageBytesCompressed = imageBytes;
|
imageBytesCompressed = imageBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stopwatch.stop();
|
||||||
|
|
||||||
|
Log.info(
|
||||||
|
'Compression the image took: ${stopwatch.elapsedMilliseconds} milliseconds');
|
||||||
|
Log.info("Raw images size in bytes: ${imageBytesCompressed.length}");
|
||||||
|
|
||||||
|
// stopwatch.reset();
|
||||||
|
// stopwatch.start();
|
||||||
|
|
||||||
|
// // var helper = MediaUploadHelper();
|
||||||
|
// try {
|
||||||
|
// final webpBytes =
|
||||||
|
// await convertAndCompressImage(pngRawImageBytes: imageBytes);
|
||||||
|
// Log.info(
|
||||||
|
// 'Compression the image in rust took: ${stopwatch.elapsedMilliseconds} milliseconds');
|
||||||
|
// Log.info("Raw images size in bytes using webp: ${webpBytes.length}");
|
||||||
|
// } catch (e) {
|
||||||
|
// Log.error("$e");
|
||||||
|
// }
|
||||||
|
|
||||||
/// in case the media file was already encrypted of even uploaded
|
/// in case the media file was already encrypted of even uploaded
|
||||||
/// remove the data so it will be done again.
|
/// remove the data so it will be done again.
|
||||||
await twonlyDB.mediaUploadsDao.updateMediaUpload(
|
await twonlyDB.mediaUploadsDao.updateMediaUpload(
|
||||||
|
|
|
||||||
|
|
@ -75,14 +75,13 @@ Future createThumbnailsForVideo(File file) async {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String? thumbnailFile = await VideoThumbnail.thumbnailFile(
|
await VideoThumbnail.thumbnailFile(
|
||||||
video: file.path,
|
video: file.path,
|
||||||
imageFormat: ImageFormat.PNG,
|
imageFormat: ImageFormat.PNG,
|
||||||
|
thumbnailPath: getThumbnailPath(file).path,
|
||||||
maxWidth: 450,
|
maxWidth: 450,
|
||||||
quality: 75,
|
quality: 75,
|
||||||
);
|
);
|
||||||
|
|
||||||
File(thumbnailFile!).rename(getThumbnailPath(file).path);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error("Could not create the video thumbnail: $e");
|
Log.error("Could not create the video thumbnail: $e");
|
||||||
}
|
}
|
||||||
|
|
@ -92,6 +91,10 @@ File getThumbnailPath(File file) {
|
||||||
String originalFileName = file.uri.pathSegments.last;
|
String originalFileName = file.uri.pathSegments.last;
|
||||||
String fileNameWithoutExtension = originalFileName.split('.').first;
|
String fileNameWithoutExtension = originalFileName.split('.').first;
|
||||||
String fileExtension = originalFileName.split('.').last;
|
String fileExtension = originalFileName.split('.').last;
|
||||||
|
if (fileExtension == "mp4") {
|
||||||
|
fileExtension = "png";
|
||||||
|
}
|
||||||
String newFileName = '$fileNameWithoutExtension.thumbnail.$fileExtension';
|
String newFileName = '$fileNameWithoutExtension.thumbnail.$fileExtension';
|
||||||
|
Directory(file.parent.path).createSync();
|
||||||
return File(join(file.parent.path, newFileName));
|
return File(join(file.parent.path, newFileName));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,6 @@ class _ChatListViewState extends State<ChatListView> {
|
||||||
_contacts = contacts.where((x) => !x.pinned).toList();
|
_contacts = contacts.where((x) => !x.pinned).toList();
|
||||||
_pinnedContacts = contacts.where((x) => x.pinned).toList();
|
_pinnedContacts = contacts.where((x) => x.pinned).toList();
|
||||||
});
|
});
|
||||||
;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
tutorial = Timer(Duration(seconds: 1), () async {
|
tutorial = Timer(Duration(seconds: 1), () async {
|
||||||
|
|
|
||||||
|
|
@ -170,9 +170,8 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
||||||
.reversed
|
.reversed
|
||||||
.toList();
|
.toList();
|
||||||
final items = await MemoryItem.convertFromMessages(filteredMediaFiles);
|
final items = await MemoryItem.convertFromMessages(filteredMediaFiles);
|
||||||
setState(() {
|
galleryItems = items.values.toList();
|
||||||
galleryItems = items.values.toList();
|
setState(() {});
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,9 +48,11 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (await received.existsMediaFile(widget.message.messageId, "png")) {
|
if (await received.existsMediaFile(widget.message.messageId, "png")) {
|
||||||
setState(() {
|
if (mounted) {
|
||||||
canBeReopened = true;
|
setState(() {
|
||||||
});
|
canBeReopened = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
Future.delayed(Duration(seconds: 1), () {
|
Future.delayed(Duration(seconds: 1), () {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
showReopenMediaFilesTutorial(context, reopenMediaFile);
|
showReopenMediaFilesTutorial(context, reopenMediaFile);
|
||||||
|
|
|
||||||
|
|
@ -52,59 +52,58 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
||||||
if (content == null) return Container();
|
if (content == null) return Container();
|
||||||
bool right = widget.message.messageOtherId == null;
|
bool right = widget.message.messageOtherId == null;
|
||||||
|
|
||||||
return Hero(
|
return Container(
|
||||||
tag: "${widget.message.mediaUploadId ?? widget.message.messageId}",
|
// tag: "${widget.message.mediaUploadId ?? widget.message.messageId}",
|
||||||
child: Align(
|
child: Align(
|
||||||
alignment: right ? Alignment.centerRight : Alignment.centerLeft,
|
alignment: right ? Alignment.centerRight : Alignment.centerLeft,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: widget.lastMessageFromSameUser
|
padding: widget.lastMessageFromSameUser
|
||||||
? EdgeInsets.only(top: 5, bottom: 0, right: 10, left: 10)
|
? EdgeInsets.only(top: 5, bottom: 0, right: 10, left: 10)
|
||||||
: EdgeInsets.only(top: 5, bottom: 20, right: 10, left: 10),
|
: EdgeInsets.only(top: 5, bottom: 20, right: 10, left: 10),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment:
|
mainAxisAlignment:
|
||||||
right ? MainAxisAlignment.end : MainAxisAlignment.start,
|
right ? MainAxisAlignment.end : MainAxisAlignment.start,
|
||||||
crossAxisAlignment:
|
crossAxisAlignment:
|
||||||
right ? CrossAxisAlignment.end : CrossAxisAlignment.start,
|
right ? CrossAxisAlignment.end : CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
MessageActions(
|
MessageActions(
|
||||||
message: widget.message,
|
message: widget.message,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
alignment:
|
alignment: right ? Alignment.centerRight : Alignment.centerLeft,
|
||||||
right ? Alignment.centerRight : Alignment.centerLeft,
|
children: [
|
||||||
children: [
|
(textMessage != null)
|
||||||
(textMessage != null)
|
? ChatTextEntry(
|
||||||
? ChatTextEntry(
|
|
||||||
message: widget.message,
|
|
||||||
text: textMessage!,
|
|
||||||
)
|
|
||||||
: ChatMediaEntry(
|
|
||||||
message: widget.message,
|
|
||||||
contact: widget.contact,
|
|
||||||
galleryItems: widget.galleryItems,
|
|
||||||
content: content!,
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
bottom: 5,
|
|
||||||
left: 5,
|
|
||||||
right: 5,
|
|
||||||
child: ReactionRow(
|
|
||||||
otherReactions: widget.otherReactions,
|
|
||||||
message: widget.message,
|
message: widget.message,
|
||||||
|
text: textMessage!,
|
||||||
|
)
|
||||||
|
: ChatMediaEntry(
|
||||||
|
message: widget.message,
|
||||||
|
contact: widget.contact,
|
||||||
|
galleryItems: widget.galleryItems,
|
||||||
|
content: content!,
|
||||||
),
|
),
|
||||||
),
|
Positioned(
|
||||||
],
|
bottom: 5,
|
||||||
|
left: 5,
|
||||||
|
right: 5,
|
||||||
|
child: ReactionRow(
|
||||||
|
otherReactions: widget.otherReactions,
|
||||||
|
message: widget.message,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onResponseTriggered: () {
|
],
|
||||||
widget.onResponseTriggered(widget.message);
|
),
|
||||||
},
|
onResponseTriggered: () {
|
||||||
),
|
widget.onResponseTriggered(widget.message);
|
||||||
ChatTextResponseColumns(
|
},
|
||||||
textReactions: widget.textReactions,
|
|
||||||
right: right,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
ChatTextResponseColumns(
|
||||||
));
|
textReactions: widget.textReactions,
|
||||||
|
right: right,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,11 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/src/services/api/media_upload.dart' as send;
|
|
||||||
import 'package:twonly/src/utils/log.dart';
|
|
||||||
import 'package:twonly/src/views/components/message_send_state_icon.dart';
|
import 'package:twonly/src/views/components/message_send_state_icon.dart';
|
||||||
import 'package:twonly/src/database/twonly_database.dart';
|
import 'package:twonly/src/database/twonly_database.dart';
|
||||||
import 'package:twonly/src/database/tables/messages_table.dart';
|
|
||||||
import 'package:twonly/src/model/json/message.dart';
|
|
||||||
import 'package:twonly/src/model/memory_item.model.dart';
|
import 'package:twonly/src/model/memory_item.model.dart';
|
||||||
|
import 'package:twonly/src/views/memories/memories_item_thumbnail.dart';
|
||||||
import 'package:twonly/src/views/memories/memories_photo_slider.view.dart';
|
import 'package:twonly/src/views/memories/memories_photo_slider.view.dart';
|
||||||
import 'package:video_player/video_player.dart';
|
|
||||||
|
|
||||||
class InChatMediaViewer extends StatefulWidget {
|
class InChatMediaViewer extends StatefulWidget {
|
||||||
const InChatMediaViewer({
|
const InChatMediaViewer({
|
||||||
|
|
@ -34,26 +28,48 @@ class InChatMediaViewer extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
||||||
File? image;
|
|
||||||
File? video;
|
|
||||||
bool isMounted = true;
|
|
||||||
bool mirrorVideo = false;
|
bool mirrorVideo = false;
|
||||||
VideoPlayerController? videoController;
|
int? galleryItemIndex;
|
||||||
StreamSubscription<Message?>? messageStream;
|
StreamSubscription<Message?>? messageStream;
|
||||||
|
Timer? _timer;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
initAsync(widget.message);
|
loadIndexAsync();
|
||||||
initStream();
|
initStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future loadIndexAsync() async {
|
||||||
|
if (!widget.message.mediaStored) return;
|
||||||
|
_timer = Timer.periodic(Duration(milliseconds: 10), (timer) {
|
||||||
|
/// when the galleryItems are updated this widget is not reloaded
|
||||||
|
/// so using this timer as a workaround
|
||||||
|
if (loadIndex()) {
|
||||||
|
timer.cancel();
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool loadIndex() {
|
||||||
|
if (widget.message.mediaStored) {
|
||||||
|
final index = widget.galleryItems.indexWhere((x) =>
|
||||||
|
x.id == (widget.message.mediaUploadId ?? widget.message.messageId));
|
||||||
|
if (index != -1) {
|
||||||
|
galleryItemIndex = index;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
isMounted = false;
|
|
||||||
messageStream?.cancel();
|
messageStream?.cancel();
|
||||||
videoController?.dispose();
|
_timer?.cancel();
|
||||||
|
// videoController?.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future initStream() async {
|
Future initStream() async {
|
||||||
|
|
@ -70,60 +86,20 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
||||||
if (updated != null) {
|
if (updated != null) {
|
||||||
if (updated.mediaStored) {
|
if (updated.mediaStored) {
|
||||||
messageStream?.cancel();
|
messageStream?.cancel();
|
||||||
initAsync(updated);
|
loadIndexAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future initAsync(Message message) async {
|
|
||||||
if (!message.mediaStored) return;
|
|
||||||
bool isSend = message.messageOtherId == null;
|
|
||||||
final basePath = await send.getMediaFilePath(
|
|
||||||
isSend ? message.mediaUploadId! : message.messageId,
|
|
||||||
isSend ? "send" : "received",
|
|
||||||
);
|
|
||||||
if (!isMounted) return;
|
|
||||||
final videoPath = File("$basePath.mp4");
|
|
||||||
final imagePath = File("$basePath.png");
|
|
||||||
if (videoPath.existsSync() && message.contentJson != null) {
|
|
||||||
MessageContent? content = MessageContent.fromJson(
|
|
||||||
MessageKind.media, jsonDecode(message.contentJson!));
|
|
||||||
if (content is MediaMessageContent) {
|
|
||||||
mirrorVideo = content.mirrorVideo;
|
|
||||||
}
|
|
||||||
videoController = VideoPlayerController.file(
|
|
||||||
videoPath,
|
|
||||||
videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
|
|
||||||
);
|
|
||||||
videoController?.initialize().then((_) {
|
|
||||||
videoController!.setVolume(0);
|
|
||||||
videoController!.play();
|
|
||||||
videoController!.setLooping(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
image = imagePath;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (imagePath.existsSync()) {
|
|
||||||
setState(() {
|
|
||||||
image = imagePath;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
Log.error("file not found: $imagePath");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future onTap() async {
|
Future onTap() async {
|
||||||
|
if (galleryItemIndex == null) return;
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => MemoriesPhotoSliderView(
|
builder: (context) => MemoriesPhotoSliderView(
|
||||||
galleryItems: widget.galleryItems,
|
galleryItems: widget.galleryItems,
|
||||||
initialIndex: widget.galleryItems.indexWhere((x) =>
|
initialIndex: galleryItemIndex!,
|
||||||
x.id ==
|
|
||||||
(widget.message.mediaUploadId ?? widget.message.messageId)),
|
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -132,7 +108,7 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (image == null && video == null) {
|
if (galleryItemIndex == null) {
|
||||||
return Container(
|
return Container(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
minHeight: 39,
|
minHeight: 39,
|
||||||
|
|
@ -164,20 +140,9 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
borderRadius: BorderRadius.circular(12.0),
|
borderRadius: BorderRadius.circular(12.0),
|
||||||
),
|
),
|
||||||
child: GestureDetector(
|
child: MemoriesItemThumbnail(
|
||||||
onTap: ((image == null && videoController == null)) ? null : onTap,
|
galleryItem: widget.galleryItems[galleryItemIndex!],
|
||||||
child: Stack(
|
onTap: onTap,
|
||||||
children: [
|
|
||||||
if (image != null) Image.file(image!),
|
|
||||||
if (videoController != null)
|
|
||||||
Positioned.fill(
|
|
||||||
child: Transform.flip(
|
|
||||||
flipX: mirrorVideo,
|
|
||||||
child: VideoPlayer(videoController!),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,10 +56,10 @@ class MemoriesViewState extends State<MemoriesView> {
|
||||||
}
|
}
|
||||||
if (fileName.contains(".png")) {
|
if (fileName.contains(".png")) {
|
||||||
imagePath = file;
|
imagePath = file;
|
||||||
thumbnailFile = getThumbnailPath(imagePath);
|
thumbnailFile = file;
|
||||||
if (!await thumbnailFile.exists()) {
|
// if (!await thumbnailFile.exists()) {
|
||||||
await createThumbnailsForImage(imagePath);
|
// await createThumbnailsForImage(imagePath);
|
||||||
}
|
// }
|
||||||
} else if (fileName.contains(".mp4")) {
|
} else if (fileName.contains(".mp4")) {
|
||||||
videoPath = file;
|
videoPath = file;
|
||||||
thumbnailFile = getThumbnailPath(videoPath);
|
thumbnailFile = getThumbnailPath(videoPath);
|
||||||
|
|
|
||||||
|
|
@ -44,9 +44,11 @@ class _MemoriesItemThumbnailState extends State<MemoriesItemThumbnail> {
|
||||||
children: [
|
children: [
|
||||||
Image.file(widget.galleryItem.thumbnailPath),
|
Image.file(widget.galleryItem.thumbnailPath),
|
||||||
if (widget.galleryItem.videoPath != null)
|
if (widget.galleryItem.videoPath != null)
|
||||||
Center(
|
Positioned.fill(
|
||||||
child: FaIcon(FontAwesomeIcons.circlePlay),
|
child: Center(
|
||||||
)
|
child: FaIcon(FontAwesomeIcons.circlePlay),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue