Refactor audio-message error handling

This commit is contained in:
Nutcake 2023-05-28 18:28:04 +02:00
parent 9ab4774f34
commit aa882a13ae
3 changed files with 340 additions and 333 deletions

View file

@ -252,7 +252,7 @@ class _FriendsListState extends State<FriendsList> {
friends.sort((a, b) => a.username.length.compareTo(b.username.length)); friends.sort((a, b) => a.username.length.compareTo(b.username.length));
} }
return ListView.builder( return ListView.builder(
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(decelerationRate: ScrollDecelerationRate.fast),
itemCount: friends.length, itemCount: friends.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final friend = friends[index]; final friend = friends[index];

View file

@ -19,7 +19,7 @@ class MessageAudioPlayer extends StatefulWidget {
State<MessageAudioPlayer> createState() => _MessageAudioPlayerState(); State<MessageAudioPlayer> createState() => _MessageAudioPlayerState();
} }
class _MessageAudioPlayerState extends State<MessageAudioPlayer> with WidgetsBindingObserver, AutomaticKeepAliveClientMixin { class _MessageAudioPlayerState extends State<MessageAudioPlayer> with WidgetsBindingObserver {
final AudioPlayer _audioPlayer = AudioPlayer(); final AudioPlayer _audioPlayer = AudioPlayer();
Future? _audioFileFuture; Future? _audioFileFuture;
double _sliderValue = 0; double _sliderValue = 0;
@ -41,22 +41,32 @@ class _MessageAudioPlayerState extends State<MessageAudioPlayer> with WidgetsBin
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
final audioCache = Provider.of<AudioCacheClient>(context); final audioCache = Provider.of<AudioCacheClient>(context);
_audioFileFuture = audioCache.cachedNetworkAudioFile(AudioClipContent.fromMap(jsonDecode(widget.message.content))) _audioFileFuture = audioCache
.then((value) => _audioPlayer.setFilePath(value.path)).whenComplete(() => _audioPlayer.setLoopMode(LoopMode.off)); .cachedNetworkAudioFile(AudioClipContent.fromMap(jsonDecode(widget.message.content)))
.then((value) => _audioPlayer.setFilePath(value.path))
.whenComplete(() => _audioPlayer.setLoopMode(LoopMode.off));
} }
@override @override
void didUpdateWidget(covariant MessageAudioPlayer oldWidget) { void didUpdateWidget(covariant MessageAudioPlayer oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (oldWidget.message.id == widget.message.id) return;
final audioCache = Provider.of<AudioCacheClient>(context); final audioCache = Provider.of<AudioCacheClient>(context);
_audioFileFuture = audioCache.cachedNetworkAudioFile(AudioClipContent.fromMap(jsonDecode(widget.message.content))) _audioFileFuture = audioCache
.then((value) => _audioPlayer.setFilePath(value.path)).whenComplete(() => _audioPlayer.setLoopMode(LoopMode.off)); .cachedNetworkAudioFile(AudioClipContent.fromMap(jsonDecode(widget.message.content)))
.then((value) async {
final path = _audioPlayer.setFilePath(value.path);
await _audioPlayer.setLoopMode(LoopMode.off);
await _audioPlayer.pause();
await _audioPlayer.seek(Duration.zero);
return path;
});
} }
@override @override
void dispose() { void dispose() {
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
_audioPlayer.dispose(); _audioPlayer.dispose().onError((error, stackTrace) {});
super.dispose(); super.dispose();
} }
@ -66,22 +76,19 @@ class _MessageAudioPlayerState extends State<MessageAudioPlayer> with WidgetsBin
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Icon(Icons.error_outline, color: Theme Icon(
.of(context) Icons.error_outline,
.colorScheme color: Theme.of(context).colorScheme.error,
.error,), ),
const SizedBox(height: 4,), const SizedBox(
Text(error, textAlign: TextAlign.center, height: 4,
),
Text(
error,
textAlign: TextAlign.center,
softWrap: true, softWrap: true,
maxLines: 3, maxLines: 3,
style: Theme style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.error),
.of(context)
.textTheme
.bodySmall
?.copyWith(color: Theme
.of(context)
.colorScheme
.error),
), ),
], ],
), ),
@ -90,38 +97,19 @@ class _MessageAudioPlayerState extends State<MessageAudioPlayer> with WidgetsBin
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context);
if (!Platform.isAndroid) { if (!Platform.isAndroid) {
return _createErrorWidget("Sorry, audio-messages are not\n supported on this platform."); return _createErrorWidget("Sorry, audio-messages are not\n supported on this platform.");
} }
return FutureBuilder(
future: _audioFileFuture,
builder: (context, snapshot) {
if (snapshot.hasError) {
return SizedBox(
width: 300,
child: Row(
children: [
const Icon(Icons.volume_off),
const SizedBox(width: 8,),
Expanded(
child: Text(
"Failed to load voice message: ${snapshot.error}",
maxLines: 4,
overflow: TextOverflow.ellipsis,
softWrap: true,
),
),
],
),
);
}
return IntrinsicWidth( return IntrinsicWidth(
child: StreamBuilder<PlayerState>( child: StreamBuilder<PlayerState>(
stream: _audioPlayer.playerStateStream, stream: _audioPlayer.playerStateStream,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasData) { if (snapshot.hasError) {
final playerState = snapshot.data as PlayerState; FlutterError.reportError(FlutterErrorDetails(exception: snapshot.error!, stack: snapshot.stackTrace));
return _createErrorWidget("Failed to load audio-message.");
}
final playerState = snapshot.data;
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@ -130,8 +118,21 @@ class _MessageAudioPlayerState extends State<MessageAudioPlayer> with WidgetsBin
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
snapshot.hasData ? IconButton( FutureBuilder(
onPressed: () { future: _audioFileFuture,
builder: (context, fileSnapshot) {
if (fileSnapshot.hasError) {
return const IconButton(
icon: Icon(Icons.warning),
onPressed: null,
);
}
return IconButton(
onPressed: fileSnapshot.hasData &&
snapshot.hasData &&
playerState != null &&
playerState.processingState != ProcessingState.loading
? () {
switch (playerState.processingState) { switch (playerState.processingState) {
case ProcessingState.idle: case ProcessingState.idle:
case ProcessingState.loading: case ProcessingState.loading:
@ -149,23 +150,28 @@ class _MessageAudioPlayerState extends State<MessageAudioPlayer> with WidgetsBin
_audioPlayer.play(); _audioPlayer.play();
break; break;
} }
}, }
: null,
color: widget.foregroundColor, color: widget.foregroundColor,
icon: SizedBox.square( icon: Icon(
dimension: 24, ((_audioPlayer.duration ?? const Duration(days: 9999)) - _audioPlayer.position)
child: playerState.processingState == ProcessingState.loading .inMilliseconds <
? const Center(child: CircularProgressIndicator(),) 10
: Icon(((_audioPlayer.duration ?? Duration.zero) - _audioPlayer.position).inMilliseconds < ? Icons.replay
10 ? Icons.replay : ((playerState?.playing ?? false) ? Icons.pause : Icons.play_arrow),
: (playerState.playing ? Icons.pause : Icons.play_arrow)), ),
);
},
), ),
) : const SizedBox.square(dimension: 24, child: CircularProgressIndicator(),),
StreamBuilder( StreamBuilder(
stream: _audioPlayer.positionStream, stream: _audioPlayer.positionStream,
builder: (context, snapshot) { builder: (context, snapshot) {
_sliderValue = _audioPlayer.duration == null ? 0 : (_audioPlayer.position.inMilliseconds / _sliderValue = _audioPlayer.duration == null
(_audioPlayer.duration!.inMilliseconds)).clamp(0, 1); ? 0
return StatefulBuilder( // Not sure if this makes sense here... : (_audioPlayer.position.inMilliseconds / (_audioPlayer.duration!.inMilliseconds))
.clamp(0, 1);
return StatefulBuilder(
// Not sure if this makes sense here...
builder: (context, setState) { builder: (context, setState) {
return SliderTheme( return SliderTheme(
data: SliderThemeData( data: SliderThemeData(
@ -181,15 +187,17 @@ class _MessageAudioPlayerState extends State<MessageAudioPlayer> with WidgetsBin
setState(() { setState(() {
_sliderValue = value; _sliderValue = value;
}); });
_audioPlayer.seek(Duration( _audioPlayer.seek(
Duration(
milliseconds: (value * (_audioPlayer.duration?.inMilliseconds ?? 0)).round(), milliseconds: (value * (_audioPlayer.duration?.inMilliseconds ?? 0)).round(),
)); ),
);
}, },
), ),
); );
} },
); );
} },
) )
], ],
), ),
@ -197,40 +205,32 @@ class _MessageAudioPlayerState extends State<MessageAudioPlayer> with WidgetsBin
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
const SizedBox(width: 4,), const SizedBox(
width: 4,
),
StreamBuilder( StreamBuilder(
stream: _audioPlayer.positionStream, stream: _audioPlayer.positionStream,
builder: (context, snapshot) { builder: (context, snapshot) {
return Text("${snapshot.data?.format() ?? "??"}/${_audioPlayer.duration?.format() ?? return Text(
"??"}", "${snapshot.data?.format() ?? "??"}/${_audioPlayer.duration?.format() ?? "??"}",
style: Theme style: Theme.of(context)
.of(context)
.textTheme .textTheme
.bodySmall .bodySmall
?.copyWith(color: widget.foregroundColor?.withAlpha(150)), ?.copyWith(color: widget.foregroundColor?.withAlpha(150)),
); );
} },
), ),
const Spacer(), const Spacer(),
MessageStateIndicator(message: widget.message, foregroundColor: widget.foregroundColor,), MessageStateIndicator(
message: widget.message,
foregroundColor: widget.foregroundColor,
),
], ],
) ),
], ],
); );
} else if (snapshot.hasError) { },
FlutterError.reportError(FlutterErrorDetails(exception: snapshot.error!, stack: snapshot.stackTrace));
return _createErrorWidget("Failed to load audio-message.");
} else {
return const Center(child: CircularProgressIndicator(),);
}
}
), ),
); );
} }
);
}
@override
// TODO: implement wantKeepAlive
bool get wantKeepAlive => true;
} }

View file

@ -42,8 +42,9 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
_showSessionListScrollChevron = true; _showSessionListScrollChevron = true;
}); });
} }
if (_sessionListScrollController.position.atEdge && _sessionListScrollController.position.pixels > 0 if (_sessionListScrollController.position.atEdge &&
&& _showSessionListScrollChevron) { _sessionListScrollController.position.pixels > 0 &&
_showSessionListScrollChevron) {
setState(() { setState(() {
_showSessionListScrollChevron = false; _showSessionListScrollChevron = false;
}); });
@ -54,12 +55,8 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final sessions = widget.friend.userStatus.activeSessions; final sessions = widget.friend.userStatus.activeSessions;
final appBarColor = Theme final appBarColor = Theme.of(context).colorScheme.surfaceVariant;
.of(context) return Consumer<MessagingClient>(builder: (context, mClient, _) {
.colorScheme
.surfaceVariant;
return Consumer<MessagingClient>(
builder: (context, mClient, _) {
final cache = mClient.getUserMessageCache(widget.friend.id); final cache = mClient.getUserMessageCache(widget.friend.id);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
@ -67,20 +64,24 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
FriendOnlineStatusIndicator(userStatus: widget.friend.userStatus), FriendOnlineStatusIndicator(userStatus: widget.friend.userStatus),
const SizedBox(width: 8,), const SizedBox(
width: 8,
),
Text(widget.friend.username), Text(widget.friend.username),
if (widget.friend.isHeadless) Padding( if (widget.friend.isHeadless)
Padding(
padding: const EdgeInsets.only(left: 12), padding: const EdgeInsets.only(left: 12),
child: Icon(Icons.dns, size: 18, color: Theme child: Icon(
.of(context) Icons.dns,
.colorScheme size: 18,
.onSecondaryContainer color: Theme.of(context).colorScheme.onSecondaryContainer.withAlpha(150),
.withAlpha(150),
), ),
), ),
], ],
), ),
bottom: sessions.isNotEmpty && _sessionListOpen ? null : PreferredSize( bottom: sessions.isNotEmpty && _sessionListOpen
? null
: PreferredSize(
preferredSize: const Size.fromHeight(1), preferredSize: const Size.fromHeight(1),
child: Container( child: Container(
height: 1, height: 1,
@ -88,7 +89,8 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
), ),
), ),
actions: [ actions: [
if (sessions.isNotEmpty) AnimatedRotation( if (sessions.isNotEmpty)
AnimatedRotation(
turns: _sessionListOpen ? -1 / 4 : 1 / 4, turns: _sessionListOpen ? -1 / 4 : 1 / 4,
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
child: IconButton( child: IconButton(
@ -100,7 +102,9 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
icon: const Icon(Icons.chevron_right), icon: const Icon(Icons.chevron_right),
), ),
), ),
const SizedBox(width: 4,) const SizedBox(
width: 4,
)
], ],
scrolledUnderElevation: 0.0, scrolledUnderElevation: 0.0,
backgroundColor: appBarColor, backgroundColor: appBarColor,
@ -109,15 +113,20 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
), ),
body: Column( body: Column(
children: [ children: [
if (sessions.isNotEmpty) AnimatedSwitcher( if (sessions.isNotEmpty)
AnimatedSwitcher(
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
transitionBuilder: (child, animation) => SizeTransition(sizeFactor: animation, axis: Axis.vertical, child: child), transitionBuilder: (child, animation) =>
child: sessions.isEmpty || !_sessionListOpen ? null : Container( SizeTransition(sizeFactor: animation, axis: Axis.vertical, child: child),
child: sessions.isEmpty || !_sessionListOpen
? null
: Container(
constraints: const BoxConstraints(maxHeight: 64), constraints: const BoxConstraints(maxHeight: 64),
decoration: BoxDecoration( decoration: BoxDecoration(
color: appBarColor, color: appBarColor,
border: const Border(bottom: BorderSide(width: 1, color: Colors.black),) border: const Border(
), bottom: BorderSide(width: 1, color: Colors.black),
)),
child: Stack( child: Stack(
children: [ children: [
ListView.builder( ListView.builder(
@ -162,9 +171,7 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
if (cache == null) { if (cache == null) {
return const Column( return const Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [LinearProgressIndicator()],
LinearProgressIndicator()
],
); );
} }
if (cache.error != null) { if (cache.error != null) {
@ -189,10 +196,7 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
child: Text( child: Text(
"There are no messages here\nWhy not say hello?", "There are no messages here\nWhy not say hello?",
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: Theme style: Theme.of(context).textTheme.titleMedium,
.of(context)
.textTheme
.titleMedium,
), ),
) )
], ],
@ -203,17 +207,21 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
create: (BuildContext context) => AudioCacheClient(), create: (BuildContext context) => AudioCacheClient(),
child: ListView.builder( child: ListView.builder(
reverse: true, reverse: true,
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(decelerationRate: ScrollDecelerationRate.fast),
itemCount: cache.messages.length, itemCount: cache.messages.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final entry = cache.messages[index]; final entry = cache.messages[index];
if (index == cache.messages.length - 1) { if (index == cache.messages.length - 1) {
return Padding( return Padding(
padding: const EdgeInsets.only(top: 12), padding: const EdgeInsets.only(top: 12),
child: MessageBubble(message: entry,), child: MessageBubble(
message: entry,
),
); );
} }
return MessageBubble(message: entry,); return MessageBubble(
message: entry,
);
}, },
), ),
); );
@ -232,7 +240,6 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
], ],
), ),
); );
} });
);
} }
} }