Skip to content

Commit 36ec96d

Browse files
committed
feat: add video player support and improve message attachment handling
1 parent c779a20 commit 36ec96d

File tree

5 files changed

+171
-54
lines changed

5 files changed

+171
-54
lines changed

lib/imports.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ export 'widgets/bottom_sheet.dart';
101101
export 'widgets/button.dart';
102102
export 'widgets/common.dart';
103103
export 'widgets/image.dart';
104+
export 'widgets/video.dart';
104105

105106
export 'screens/custom_attributes/controllers/index.dart';
106107
export 'screens/labels/controllers/index.dart';

lib/models/message.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ class MessageAttachmentInfo {
66
final AttachmentType file_type;
77
final int account_id;
88
final dynamic extension; // TODO: unk type
9-
final String? data_url;
9+
final String data_url;
1010
final String? thumbnail;
1111
final String? thumb_url;
12-
final int? file_size;
12+
final int file_size;
1313
final int? width;
1414
final int? height;
1515

@@ -37,7 +37,7 @@ class MessageAttachmentInfo {
3737
data_url: json['data_url'],
3838
thumbnail: json['thumbnail'],
3939
thumb_url: json['thumb_url'],
40-
file_size: json['file_size'],
40+
file_size: json['file_size'] ?? 0,
4141
width: json['width'],
4242
height: json['height'],
4343
);

lib/screens/conversations/widgets/message.dart

Lines changed: 66 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -225,24 +225,58 @@ class Message extends StatelessWidget {
225225
final length = info.attachments.length;
226226

227227
final first = info.attachments.first;
228-
if (length == 1 && first.data_url != null) {
228+
if (length == 1) {
229229
switch (first.file_type) {
230+
case AttachmentType.video:
231+
return buildAttachmentVideo(context, info: info.attachments.first);
230232
case AttachmentType.audio:
231-
return AudioPlayer(
232-
id: first.id,
233-
url: first.data_url!,
234-
);
233+
return buildAttachmentAudio(context, info: info.attachments.first);
235234
case AttachmentType.image:
236-
return buildImageAttachment(context, info: info.attachments.first);
235+
return buildAttachmentImage(context, info: info.attachments.first);
237236
default:
238-
return Text('failed to parse attachment type: ${first.file_type}');
237+
return buildAttachmentFile(context, info: info.attachments.first);
239238
}
240239
}
241240

242-
return Text('failed to parse $length attachments');
241+
return ListView.builder(
242+
shrinkWrap: true,
243+
physics: NeverScrollableScrollPhysics(),
244+
itemCount: info.attachments.length,
245+
itemBuilder: (_, i) {
246+
final item = info.attachments[i];
247+
return buildAttachmentFile(context, info: item);
248+
},
249+
);
243250
}
244251

245-
Widget buildImageAttachment(
252+
Widget buildAttachmentVideo(
253+
BuildContext context, {
254+
required MessageAttachmentInfo info,
255+
}) {
256+
return Card(
257+
child: VideoPlayer(
258+
id: info.id,
259+
url: info.data_url,
260+
aspectRatio: info.width != null && info.height != null
261+
? info.width! / info.height!
262+
: null,
263+
),
264+
);
265+
}
266+
267+
Widget buildAttachmentAudio(
268+
BuildContext context, {
269+
required MessageAttachmentInfo info,
270+
}) {
271+
return Card(
272+
child: AudioPlayer(
273+
id: info.id,
274+
url: info.data_url,
275+
),
276+
);
277+
}
278+
279+
Widget buildAttachmentImage(
246280
BuildContext context, {
247281
required MessageAttachmentInfo info,
248282
}) {
@@ -252,7 +286,7 @@ class Message extends StatelessWidget {
252286
return InkWell(
253287
onTap: () => Get.to(
254288
() => imageViewer(
255-
url: info.data_url!,
289+
url: info.data_url,
256290
title: '#${info.id} (${formatBytes(info.file_size!)})',
257291
),
258292
),
@@ -273,7 +307,7 @@ class Message extends StatelessWidget {
273307
bottomRight: Radius.circular(0),
274308
),
275309
child: ExtendedImage.network(
276-
info.data_url!,
310+
info.data_url,
277311
loadStateChanged: (state) {
278312
switch (state.extendedImageLoadState) {
279313
case LoadState.failed:
@@ -292,4 +326,25 @@ class Message extends StatelessWidget {
292326
),
293327
);
294328
}
329+
330+
Widget buildAttachmentFile(
331+
BuildContext context, {
332+
required MessageAttachmentInfo info,
333+
}) {
334+
final name = info.data_url.split('/').last;
335+
return Card(
336+
child: CustomListTile(
337+
leading: CircleAvatar(
338+
child: Icon(Icons.download_outlined),
339+
),
340+
title: Text(
341+
name,
342+
maxLines: 1,
343+
overflow: TextOverflow.ellipsis,
344+
),
345+
subtitle: Text(formatBytes(info.file_size)),
346+
onTap: () => openInExternalBrowser(info.data_url),
347+
),
348+
);
349+
}
295350
}

lib/widgets/audio.dart

Lines changed: 32 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -103,50 +103,42 @@ class AudioPlayer extends StatelessWidget {
103103
final positionInMilliseconds =
104104
min(durationInMilliseconds, position.inMilliseconds.toDouble());
105105

106-
return Container(
107-
decoration: BoxDecoration(
108-
color: context.theme.colorScheme.tertiaryContainer,
106+
Widget icon;
107+
if (isEnded) {
108+
icon = IconButton(
109+
onPressed: controller.restart,
110+
icon: Icon(Icons.restart_alt_outlined),
111+
);
112+
} else if (isPlaying) {
113+
icon = IconButton(
114+
onPressed: controller.pause,
115+
icon: Icon(Icons.pause_circle_filled_outlined),
116+
);
117+
} else {
118+
icon = IconButton(
119+
onPressed: controller.play,
120+
icon: Icon(Icons.play_circle_fill_outlined),
121+
);
122+
}
123+
124+
return CustomListTile(
125+
leading: CircleAvatar(child: icon),
126+
title: Slider(
127+
value: positionInMilliseconds,
128+
max: durationInMilliseconds,
129+
onChanged: (next) {},
109130
),
110-
child: Row(
131+
subtitle: Row(
111132
children: [
112-
if (isEnded)
113-
IconButton(
114-
onPressed: controller.restart,
115-
icon: Icon(Icons.restart_alt_outlined),
116-
)
117-
else if (isPlaying)
118-
IconButton(
119-
onPressed: controller.pause,
120-
icon: Icon(Icons.pause_circle_filled_outlined),
121-
)
122-
else
123-
IconButton(
124-
onPressed: controller.play,
125-
icon: Icon(Icons.play_circle_fill_outlined),
126-
),
127-
Expanded(
128-
child: Column(
129-
children: [
130-
Slider(
131-
value: positionInMilliseconds,
132-
max: durationInMilliseconds,
133-
onChanged: (next) {},
134-
),
135-
Padding(
136-
padding: const EdgeInsets.only(left: 16, right: 16),
137-
child: Row(
138-
children: [
139-
Text(formatDuration(position)),
140-
Spacer(),
141-
Text(formatDuration(duration)),
142-
],
143-
),
144-
),
145-
],
146-
),
147-
),
133+
Text(formatDuration(position)),
134+
Spacer(),
135+
Text(formatDuration(duration)),
148136
],
149137
),
138+
// trailing: IconButton(
139+
// icon: Icon(Icons.transcribe),
140+
// onPressed: () {},
141+
// ),
150142
);
151143
});
152144
},

lib/widgets/video.dart

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import '/imports.dart';
2+
import 'package:chewie/chewie.dart';
3+
import 'package:video_player/video_player.dart' as video_player;
4+
5+
class VideoPlayerController extends GetxController {
6+
final String url;
7+
VideoPlayerController({
8+
required this.url,
9+
});
10+
11+
final duration = Duration.zero.obs;
12+
final position = Duration.zero.obs;
13+
final isPlaying = false.obs;
14+
final isLoaded = false.obs;
15+
final isEnded = false.obs;
16+
17+
late video_player.VideoPlayerController _videoPlayerController;
18+
late ChewieController chewieController;
19+
20+
@override
21+
void onInit() {
22+
super.onInit();
23+
24+
_videoPlayerController =
25+
video_player.VideoPlayerController.networkUrl(Uri.parse(url));
26+
chewieController = ChewieController(
27+
videoPlayerController: _videoPlayerController,
28+
autoPlay: false,
29+
looping: false,
30+
);
31+
}
32+
33+
@override
34+
void onClose() {
35+
chewieController.dispose();
36+
_videoPlayerController.dispose();
37+
38+
super.onClose();
39+
}
40+
}
41+
42+
class VideoPlayer extends StatelessWidget {
43+
final int id;
44+
final String url;
45+
final double aspectRatio;
46+
47+
const VideoPlayer({
48+
super.key,
49+
required this.id,
50+
required this.url,
51+
double? aspectRatio,
52+
}) : aspectRatio = aspectRatio ?? 16 / 9;
53+
54+
@override
55+
Widget build(BuildContext context) {
56+
return GetBuilder<VideoPlayerController>(
57+
init: VideoPlayerController(url: url),
58+
tag: id.toString(),
59+
builder: (controller) {
60+
return AspectRatio(
61+
aspectRatio: aspectRatio,
62+
child: Chewie(
63+
controller: controller.chewieController,
64+
),
65+
);
66+
},
67+
);
68+
}
69+
}

0 commit comments

Comments
 (0)