Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class MatrixTimelineEventMessage extends MatrixTimelineEvent
String _getPlaintextBody({Timeline? timeline}) {
var e = getDisplayEvent(timeline);

if (["m.file", "m.image", "m.video"].contains(e.messageType)) {
if (["m.file", "m.image", "m.video", "m.audio"].contains(e.messageType)) {
var file = e.content["file"] is Map<String, dynamic>
? e.content['file'] as Map<String, dynamic>
: null;
Expand All @@ -78,7 +78,7 @@ class MatrixTimelineEventMessage extends MatrixTimelineEvent
String _getFormattedBody({Timeline? timeline}) {
var e = getDisplayEvent(timeline);

if (["m.file", "m.image", "m.video"].contains(e.messageType)) {
if (["m.file", "m.image", "m.video", "m.audio"].contains(e.messageType)) {
return e.formattedText;
}

Expand Down
44 changes: 31 additions & 13 deletions commet/lib/ui/atoms/message_attachment.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:commet/client/attachment.dart';
import 'package:commet/config/build_config.dart';
import 'package:commet/ui/atoms/lightbox.dart';
import 'package:commet/ui/molecules/audio_player/audio_player.dart';
import 'package:commet/ui/molecules/video_player/video_player.dart';
import 'package:commet/utils/background_tasks/background_task_manager.dart';
import 'package:commet/utils/download_utils.dart';
Expand Down Expand Up @@ -43,6 +44,11 @@ class _MessageAttachmentState extends State<MessageAttachment> {

final attachment = widget.attachment;
if (attachment is FileAttachment) {
if (attachment.mimeType != null &&
Mime.playableAudioTypes.contains(attachment.mimeType!)) {
return buildAudio(attachment);
}

return buildFile(Mime.toIcon(attachment.mimeType), attachment.name,
attachment.fileSize);
}
Expand Down Expand Up @@ -151,11 +157,11 @@ class _MessageAttachmentState extends State<MessageAttachment> {
Widget buildFile(IconData icon, String fileName, int? fileSize) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Theme.of(context).colorScheme.surfaceContainer,
border: Border.all(color: Theme.of(context).colorScheme.outline)),
borderRadius: BorderRadius.circular(10),
color: Theme.of(context).colorScheme.surfaceContainerLow,
),
child: Padding(
padding: const EdgeInsets.all(2.0),
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
Expand Down Expand Up @@ -198,15 +204,19 @@ class _MessageAttachmentState extends State<MessageAttachment> {
)
else
Padding(
padding: const EdgeInsets.fromLTRB(8, 0, 8, 0),
child: tiamat.IconButton(
size: 20,
icon: Icons.download,
onPressed: () async {
if (widget.attachment is FileAttachment) {
downloadAttachment(widget.attachment as FileAttachment);
}
},
padding: const EdgeInsets.fromLTRB(12, 0, 4, 0),
child: SizedBox(
width: 40,
height: 40,
child: tiamat.IconButton(
size: 20,
icon: Icons.download,
onPressed: () async {
if (widget.attachment is FileAttachment) {
downloadAttachment(widget.attachment as FileAttachment);
}
},
),
),
)
],
Expand All @@ -225,4 +235,12 @@ class _MessageAttachmentState extends State<MessageAttachment> {

return BackgroundTaskStatus.completed;
}

Widget buildAudio(FileAttachment attachment) {
return AudioPlayer(
file: attachment.file,
fileName: attachment.name,
fileSize: attachment.fileSize,
);
}
}
217 changes: 217 additions & 0 deletions commet/lib/ui/molecules/audio_player/audio_player.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import 'dart:async';

import 'package:commet/cache/file_provider.dart';
import 'package:commet/utils/text_utils.dart';
import 'package:flutter/material.dart';
import 'package:media_kit/media_kit.dart';
import 'package:tiamat/tiamat.dart' as tiamat;

class AudioPlayer extends StatefulWidget {
const AudioPlayer(
{required this.file, this.fileName, this.fileSize, super.key});

final String? fileName;
final int? fileSize;
final FileProvider file;

@override
State<AudioPlayer> createState() => _AudioPlayerState();
}

enum AudioPlayerState {
paused,
loading,
playing,
}

class _AudioPlayerState extends State<AudioPlayer> {
Player player = Player();
late List<StreamSubscription> subs;

@override
void initState() {
super.initState();

subs = [
player.stream.playing.listen(onPlayingChanged),
player.stream.position.listen(onPositionChanged),
if (widget.file.onProgressChanged != null)
widget.file.onProgressChanged!.listen(onDownloadProgressChanged),
];
}

@override
void dispose() {
for (var sub in subs) {
sub.cancel();
}

player.dispose();

super.dispose();
}

var state = AudioPlayerState.paused;

bool dragging = false;

double displayPosition = 0;
double? downloadProgress;

@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(8)),
child: SizedBox(
width: 500,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.fileName != null)
Padding(
padding: const EdgeInsets.fromLTRB(12, 12, 0, 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(
Icons.audio_file,
size: 20,
),
SizedBox(
width: 8,
),
tiamat.Text.labelLow(widget.fileName!),
],
),
if (widget.fileSize != null)
Padding(
padding: const EdgeInsets.fromLTRB(0, 0, 12, 0),
child: tiamat.Text.labelLow(
TextUtils.readableFileSize(widget.fileSize!)),
),
],
),
),
Padding(
padding: const EdgeInsets.fromLTRB(0, 8, 0, 4),
child: tiamat.Seperator(
padding: 0,
),
),
Row(
children: [
SizedBox(
width: 8,
),
SizedBox(
width: 40,
height: 40,
child: state == AudioPlayerState.loading
? Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
value: downloadProgress,
)),
)
: tiamat.IconButton(
icon: state == AudioPlayerState.paused
? Icons.play_arrow
: Icons.pause,
onPressed: onPlayButtonPressed,
)),
Expanded(
child: tiamat.Slider(
value: displayPosition,
onChangeStart: (value) {
dragging = true;
},
onChangeEnd: (value) {
dragging = false;

var position = player.state.duration * value;

player.seek(position);
},
onChanged: (value) => setState(() {
displayPosition = value;
}),
))
],
),
],
),
),
);
}

onPlayButtonPressed() {
if (state == AudioPlayerState.paused) {
if (player.state.playlist.medias.isEmpty) {
setState(() {
state = AudioPlayerState.loading;
loadAudio();
});
} else {
player.play();
}
}

if (state == AudioPlayerState.playing) {
player.pause();
setState(() {
state = AudioPlayerState.paused;
});
}
}

void loadAudio() async {
var uri = await widget.file.resolve();

if (uri != null) {
player.open(Media(uri.toString()));
player.setPlaylistMode(PlaylistMode.none);
}

setState(() {
state = AudioPlayerState.playing;
});
}

void onPlayingChanged(bool event) {
if (event) {
setState(() {
state = AudioPlayerState.playing;
});
} else {
setState(() {
state = AudioPlayerState.paused;
});
}
}

void onPositionChanged(Duration event) {
if (!dragging) {
setState(() {
var pos = event.inMilliseconds.toDouble() /
player.state.duration.inMilliseconds.toDouble();

if (pos >= 0 && pos <= 1) {
displayPosition = pos;
}
});
}
}

void onDownloadProgressChanged(DownloadProgress event) {
print(event);
setState(() {
downloadProgress = event.downloaded.toDouble() / event.total.toDouble();
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ class TimelineViewEntryState extends State<TimelineViewEntry>

if (selected) {
result = Container(
color: Theme.of(context).hoverColor,
color: Theme.of(context).hoverColor.withAlpha(5),
child: result,
);
}
Expand Down
12 changes: 10 additions & 2 deletions commet/lib/utils/mime.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ class Mime {
"image/bmp",
};

static const playableAudioTypes = {
"audio/x-wav",
"audio/ogg",
"audio/wav",
"audio/mp3",
"audio/mpeg",
};

static bool isText(String mime) => mime.startsWith("text/");

static const videoTypes = {
Expand Down Expand Up @@ -64,8 +72,8 @@ class Mime {
static IconData toIcon(String? mimeType) {
if (imageTypes.contains(mimeType)) return Icons.image;
if (videoTypes.contains(mimeType)) return Icons.video_file_rounded;
if (archiveTypes.contains(mimeType)) return Icons.folder_zip_outlined;

if (archiveTypes.contains(mimeType)) return Icons.folder_zip;
if (playableAudioTypes.contains(mimeType)) return Icons.audio_file;
return Icons.file_present;
}

Expand Down