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,103 +237,151 @@ 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: Row( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ crossAxisAlignment: CrossAxisAlignment.start,
SizedBox( children: [
width: buttonSize, ConstrainedBox(
height: buttonSize, constraints:
child: status == AudioPlayerStatus.downloading const BoxConstraints(maxWidth: FluffyThemes.columnWidth),
? CircularProgressIndicator(strokeWidth: 2, color: widget.color) child: Row(
: InkWell( mainAxisSize: MainAxisSize.min,
borderRadius: BorderRadius.circular(64), children: <Widget>[
child: Material( SizedBox(
color: widget.color.withAlpha(64), width: buttonSize,
borderRadius: BorderRadius.circular(64), height: buttonSize,
child: Icon( child: status == AudioPlayerStatus.downloading
audioPlayer?.playerState.playing == true ? CircularProgressIndicator(
? Icons.pause_outlined strokeWidth: 2,
: Icons.play_arrow_outlined,
color: widget.color,
),
),
onLongPress: () => widget.event.saveFile(context),
onTap: () {
if (status == AudioPlayerStatus.downloaded) {
_playAction();
} else {
_downloadAction();
}
},
),
),
const SizedBox(width: 8),
Row(
mainAxisSize: MainAxisSize.min,
children: [
for (var i = 0; i < AudioPlayerWidget.wavesCount; i++)
GestureDetector(
onTapDown: (_) => audioPlayer?.seek(
Duration(
milliseconds:
(maxPosition / AudioPlayerWidget.wavesCount).round() *
i,
),
),
child: Container(
height: 32,
color: widget.color.withAlpha(0),
alignment: Alignment.center,
child: Opacity(
opacity: currentPosition > i ? 1 : 0.5,
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 1),
decoration: BoxDecoration(
color: widget.color, color: widget.color,
borderRadius: BorderRadius.circular(2), )
: InkWell(
borderRadius: BorderRadius.circular(64),
onLongPress: () => widget.event.saveFile(context),
onTap: _startAction,
child: Material(
color: widget.color.withAlpha(64),
borderRadius: BorderRadius.circular(64),
child: Icon(
audioPlayer?.playerState.playing == true
? Icons.pause_outlined
: Icons.play_arrow_outlined,
color: widget.color,
),
),
),
),
const SizedBox(width: 8),
Expanded(
child: Stack(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
children: [
for (var i = 0;
i < AudioPlayerWidget.wavesCount;
i++)
Expanded(
child: Container(
height: 32,
alignment: Alignment.center,
child: Container(
margin: const EdgeInsets.symmetric(
horizontal: 1,
),
decoration: BoxDecoration(
color: widget.color.withAlpha(96),
borderRadius: BorderRadius.circular(32),
),
height: 32 * (waveform[i] / 1024),
),
),
),
],
), ),
width: 2,
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),
SizedBox(
width: 36,
child: Text(
statusText,
style: TextStyle(
color: widget.color,
fontSize: 12,
), ),
), ),
), ),
], const SizedBox(width: 8),
Badge(
isLabelVisible: audioPlayer != null,
label: audioPlayer == null
? null
: Text(
'${audioPlayer.speed.toString()}x',
),
backgroundColor: theme.colorScheme.secondary,
textColor: theme.colorScheme.onSecondary,
child: InkWell(
splashColor: widget.color.withAlpha(128),
borderRadius: BorderRadius.circular(64),
onTap: audioPlayer == null ? null : _toggleSpeed,
child: Icon(
Icons.mic_none_outlined,
color: widget.color,
),
),
),
const SizedBox(width: 8),
],
),
), ),
const SizedBox(width: 8), if (displayBody)
SizedBox( Linkify(
width: 36, text: body,
child: Text(
statusText,
style: TextStyle( style: TextStyle(
color: widget.color, color: widget.color,
fontSize: 12, fontSize: widget.fontSize,
), ),
), options: const LinkifyOptions(humanize: false),
), linkStyle: TextStyle(
const SizedBox(width: 8), color: widget.color.withAlpha(150),
Badge( fontSize: widget.fontSize,
isLabelVisible: audioPlayer != null, decoration: TextDecoration.underline,
label: audioPlayer == null decorationColor: widget.color.withAlpha(150),
? null
: Text(
'${audioPlayer.speed.toString()}x',
),
backgroundColor: theme.colorScheme.secondary,
textColor: theme.colorScheme.onSecondary,
child: InkWell(
splashColor: widget.color.withAlpha(128),
borderRadius: BorderRadius.circular(64),
onTap: audioPlayer == null ? null : _toggleSpeed,
child: Icon(
Icons.mic_none_outlined,
color: widget.color,
), ),
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
), ),
),
const SizedBox(width: 8),
], ],
), ),
); );

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);