feat: New audio message design with displayed body

This commit is contained in:
krille-chan 2024-09-11 11:45:46 +02:00
commit a732ea62e2
No known key found for this signature in database
2 changed files with 150 additions and 86 deletions

View file

@ -4,24 +4,33 @@ import 'dart:io';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:just_audio/just_audio.dart'; import 'package:just_audio/just_audio.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'package:opus_caf_converter_dart/opus_caf_converter_dart.dart'; import 'package:opus_caf_converter_dart/opus_caf_converter_dart.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/utils/error_reporter.dart'; import 'package:fluffychat/utils/error_reporter.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/utils/url_launcher.dart';
import '../../../utils/matrix_sdk_extensions/event_extension.dart'; import '../../../utils/matrix_sdk_extensions/event_extension.dart';
class AudioPlayerWidget extends StatefulWidget { class AudioPlayerWidget extends StatefulWidget {
final Color color; final Color color;
final double fontSize;
final Event event; final Event event;
static String? currentId; static String? currentId;
static const int wavesCount = 40; static const int wavesCount = 40;
const AudioPlayerWidget(this.event, {this.color = Colors.black, super.key}); const AudioPlayerWidget(
this.event, {
this.color = Colors.black,
required this.fontSize,
super.key,
});
@override @override
AudioPlayerState createState() => AudioPlayerState(); AudioPlayerState createState() => AudioPlayerState();
@ -39,8 +48,8 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
StreamSubscription? onPlayerError; StreamSubscription? onPlayerError;
String? statusText; String? statusText;
int currentPosition = 0; double currentPosition = 0;
double maxPosition = 0; double maxPosition = 1;
MatrixFile? matrixFile; MatrixFile? matrixFile;
File? audioFile; File? audioFile;
@ -58,6 +67,14 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
super.dispose(); super.dispose();
} }
void _startAction() {
if (status == AudioPlayerStatus.downloaded) {
_playAction();
} else {
_downloadAction();
}
}
Future<void> _downloadAction() async { Future<void> _downloadAction() async {
if (status != AudioPlayerStatus.notDownloaded) return; if (status != AudioPlayerStatus.notDownloaded) return;
setState(() => status = AudioPlayerStatus.downloading); setState(() => status = AudioPlayerStatus.downloading);
@ -125,9 +142,7 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
setState(() { setState(() {
statusText = statusText =
'${state.inMinutes.toString().padLeft(2, '0')}:${(state.inSeconds % 60).toString().padLeft(2, '0')}'; '${state.inMinutes.toString().padLeft(2, '0')}:${(state.inSeconds % 60).toString().padLeft(2, '0')}';
currentPosition = ((state.inMilliseconds.toDouble() / maxPosition) * currentPosition = state.inMilliseconds.toDouble();
AudioPlayerWidget.wavesCount)
.round();
}); });
if (state.inMilliseconds.toDouble() == maxPosition) { if (state.inMilliseconds.toDouble() == maxPosition) {
audioPlayer.stop(); audioPlayer.stop();
@ -222,8 +237,22 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
final statusText = this.statusText ??= _durationString ?? '00:00'; final statusText = this.statusText ??= _durationString ?? '00:00';
final audioPlayer = this.audioPlayer; final audioPlayer = this.audioPlayer;
final body = widget.event.content.tryGet<String>('body') ??
widget.event.content.tryGet<String>('filename');
final displayBody = body != null &&
body.isNotEmpty &&
widget.event.content['org.matrix.msc1767.audio'] == null;
return Padding( return Padding(
padding: const EdgeInsets.all(12.0), padding: const EdgeInsets.all(12.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ConstrainedBox(
constraints:
const BoxConstraints(maxWidth: FluffyThemes.columnWidth),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
@ -231,9 +260,14 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
width: buttonSize, width: buttonSize,
height: buttonSize, height: buttonSize,
child: status == AudioPlayerStatus.downloading child: status == AudioPlayerStatus.downloading
? CircularProgressIndicator(strokeWidth: 2, color: widget.color) ? CircularProgressIndicator(
strokeWidth: 2,
color: widget.color,
)
: InkWell( : InkWell(
borderRadius: BorderRadius.circular(64), borderRadius: BorderRadius.circular(64),
onLongPress: () => widget.event.saveFile(context),
onTap: _startAction,
child: Material( child: Material(
color: widget.color.withAlpha(64), color: widget.color.withAlpha(64),
borderRadius: BorderRadius.circular(64), borderRadius: BorderRadius.circular(64),
@ -244,49 +278,59 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
color: widget.color, color: widget.color,
), ),
), ),
onLongPress: () => widget.event.saveFile(context),
onTap: () {
if (status == AudioPlayerStatus.downloaded) {
_playAction();
} else {
_downloadAction();
}
},
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Row( Expanded(
mainAxisSize: MainAxisSize.min, child: Stack(
children: [ children: [
for (var i = 0; i < AudioPlayerWidget.wavesCount; i++) Padding(
GestureDetector( padding: const EdgeInsets.symmetric(horizontal: 16.0),
onTapDown: (_) => audioPlayer?.seek( child: Row(
Duration( children: [
milliseconds: for (var i = 0;
(maxPosition / AudioPlayerWidget.wavesCount).round() * i < AudioPlayerWidget.wavesCount;
i, i++)
), Expanded(
),
child: Container( child: Container(
height: 32, height: 32,
color: widget.color.withAlpha(0),
alignment: Alignment.center, alignment: Alignment.center,
child: Opacity(
opacity: currentPosition > i ? 1 : 0.5,
child: Container( child: Container(
margin: const EdgeInsets.symmetric(horizontal: 1), margin: const EdgeInsets.symmetric(
decoration: BoxDecoration( horizontal: 1,
color: widget.color, ),
borderRadius: BorderRadius.circular(2), decoration: BoxDecoration(
color: widget.color.withAlpha(96),
borderRadius: BorderRadius.circular(32),
), ),
width: 2,
height: 32 * (waveform[i] / 1024), height: 32 * (waveform[i] / 1024),
), ),
), ),
), ),
],
),
),
SizedBox(
height: 32,
child: Slider(
thumbColor: widget.event.senderId ==
widget.event.room.client.userID
? theme.colorScheme.onPrimary
: theme.colorScheme.primary,
activeColor: Colors.transparent,
inactiveColor: Colors.transparent,
max: maxPosition,
value: currentPosition,
onChanged: (position) => audioPlayer == null
? _startAction()
: audioPlayer.seek(
Duration(milliseconds: position.round()),
),
),
), ),
], ],
), ),
),
const SizedBox(width: 8), const SizedBox(width: 8),
SizedBox( SizedBox(
width: 36, width: 36,
@ -321,6 +365,25 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
const SizedBox(width: 8), const SizedBox(width: 8),
], ],
), ),
),
if (displayBody)
Linkify(
text: body,
style: TextStyle(
color: widget.color,
fontSize: widget.fontSize,
),
options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle(
color: widget.color.withAlpha(150),
fontSize: widget.fontSize,
decoration: TextDecoration.underline,
decorationColor: widget.color.withAlpha(150),
),
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
),
],
),
); );
} }
} }

View file

@ -151,6 +151,7 @@ class MessageContent extends StatelessWidget {
return AudioPlayerWidget( return AudioPlayerWidget(
event, event,
color: textColor, color: textColor,
fontSize: fontSize,
); );
} }
return MessageDownloadContent(event, textColor); return MessageDownloadContent(event, textColor);