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));
}
return ListView.builder(
physics: const BouncingScrollPhysics(),
physics: const BouncingScrollPhysics(decelerationRate: ScrollDecelerationRate.fast),
itemCount: friends.length,
itemBuilder: (context, index) {
final friend = friends[index];

View file

@ -19,7 +19,7 @@ class MessageAudioPlayer extends StatefulWidget {
State<MessageAudioPlayer> createState() => _MessageAudioPlayerState();
}
class _MessageAudioPlayerState extends State<MessageAudioPlayer> with WidgetsBindingObserver, AutomaticKeepAliveClientMixin {
class _MessageAudioPlayerState extends State<MessageAudioPlayer> with WidgetsBindingObserver {
final AudioPlayer _audioPlayer = AudioPlayer();
Future? _audioFileFuture;
double _sliderValue = 0;
@ -41,22 +41,32 @@ class _MessageAudioPlayerState extends State<MessageAudioPlayer> with WidgetsBin
void didChangeDependencies() {
super.didChangeDependencies();
final audioCache = Provider.of<AudioCacheClient>(context);
_audioFileFuture = audioCache.cachedNetworkAudioFile(AudioClipContent.fromMap(jsonDecode(widget.message.content)))
.then((value) => _audioPlayer.setFilePath(value.path)).whenComplete(() => _audioPlayer.setLoopMode(LoopMode.off));
_audioFileFuture = audioCache
.cachedNetworkAudioFile(AudioClipContent.fromMap(jsonDecode(widget.message.content)))
.then((value) => _audioPlayer.setFilePath(value.path))
.whenComplete(() => _audioPlayer.setLoopMode(LoopMode.off));
}
@override
void didUpdateWidget(covariant MessageAudioPlayer oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.message.id == widget.message.id) return;
final audioCache = Provider.of<AudioCacheClient>(context);
_audioFileFuture = audioCache.cachedNetworkAudioFile(AudioClipContent.fromMap(jsonDecode(widget.message.content)))
.then((value) => _audioPlayer.setFilePath(value.path)).whenComplete(() => _audioPlayer.setLoopMode(LoopMode.off));
_audioFileFuture = audioCache
.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
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_audioPlayer.dispose();
_audioPlayer.dispose().onError((error, stackTrace) {});
super.dispose();
}
@ -66,22 +76,19 @@ class _MessageAudioPlayerState extends State<MessageAudioPlayer> with WidgetsBin
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(Icons.error_outline, color: Theme
.of(context)
.colorScheme
.error,),
const SizedBox(height: 4,),
Text(error, textAlign: TextAlign.center,
Icon(
Icons.error_outline,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(
height: 4,
),
Text(
error,
textAlign: TextAlign.center,
softWrap: true,
maxLines: 3,
style: Theme
.of(context)
.textTheme
.bodySmall
?.copyWith(color: Theme
.of(context)
.colorScheme
.error),
style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.error),
),
],
),
@ -90,38 +97,19 @@ class _MessageAudioPlayerState extends State<MessageAudioPlayer> with WidgetsBin
@override
Widget build(BuildContext context) {
super.build(context);
if (!Platform.isAndroid) {
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(
child: StreamBuilder<PlayerState>(
stream: _audioPlayer.playerStateStream,
builder: (context, snapshot) {
if (snapshot.hasData) {
final playerState = snapshot.data as PlayerState;
if (snapshot.hasError) {
FlutterError.reportError(FlutterErrorDetails(exception: snapshot.error!, stack: snapshot.stackTrace));
return _createErrorWidget("Failed to load audio-message.");
}
final playerState = snapshot.data;
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
@ -130,8 +118,21 @@ class _MessageAudioPlayerState extends State<MessageAudioPlayer> with WidgetsBin
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
snapshot.hasData ? IconButton(
onPressed: () {
FutureBuilder(
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) {
case ProcessingState.idle:
case ProcessingState.loading:
@ -149,23 +150,28 @@ class _MessageAudioPlayerState extends State<MessageAudioPlayer> with WidgetsBin
_audioPlayer.play();
break;
}
},
}
: null,
color: widget.foregroundColor,
icon: SizedBox.square(
dimension: 24,
child: playerState.processingState == ProcessingState.loading
? const Center(child: CircularProgressIndicator(),)
: Icon(((_audioPlayer.duration ?? Duration.zero) - _audioPlayer.position).inMilliseconds <
10 ? Icons.replay
: (playerState.playing ? Icons.pause : Icons.play_arrow)),
icon: Icon(
((_audioPlayer.duration ?? const Duration(days: 9999)) - _audioPlayer.position)
.inMilliseconds <
10
? Icons.replay
: ((playerState?.playing ?? false) ? Icons.pause : Icons.play_arrow),
),
);
},
),
) : const SizedBox.square(dimension: 24, child: CircularProgressIndicator(),),
StreamBuilder(
stream: _audioPlayer.positionStream,
builder: (context, snapshot) {
_sliderValue = _audioPlayer.duration == null ? 0 : (_audioPlayer.position.inMilliseconds /
(_audioPlayer.duration!.inMilliseconds)).clamp(0, 1);
return StatefulBuilder( // Not sure if this makes sense here...
_sliderValue = _audioPlayer.duration == null
? 0
: (_audioPlayer.position.inMilliseconds / (_audioPlayer.duration!.inMilliseconds))
.clamp(0, 1);
return StatefulBuilder(
// Not sure if this makes sense here...
builder: (context, setState) {
return SliderTheme(
data: SliderThemeData(
@ -181,15 +187,17 @@ class _MessageAudioPlayerState extends State<MessageAudioPlayer> with WidgetsBin
setState(() {
_sliderValue = value;
});
_audioPlayer.seek(Duration(
_audioPlayer.seek(
Duration(
milliseconds: (value * (_audioPlayer.duration?.inMilliseconds ?? 0)).round(),
));
),
);
},
),
);
}
},
);
}
},
)
],
),
@ -197,40 +205,32 @@ class _MessageAudioPlayerState extends State<MessageAudioPlayer> with WidgetsBin
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
const SizedBox(width: 4,),
const SizedBox(
width: 4,
),
StreamBuilder(
stream: _audioPlayer.positionStream,
builder: (context, snapshot) {
return Text("${snapshot.data?.format() ?? "??"}/${_audioPlayer.duration?.format() ??
"??"}",
style: Theme
.of(context)
return Text(
"${snapshot.data?.format() ?? "??"}/${_audioPlayer.duration?.format() ?? "??"}",
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: widget.foregroundColor?.withAlpha(150)),
);
}
},
),
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;
});
}
if (_sessionListScrollController.position.atEdge && _sessionListScrollController.position.pixels > 0
&& _showSessionListScrollChevron) {
if (_sessionListScrollController.position.atEdge &&
_sessionListScrollController.position.pixels > 0 &&
_showSessionListScrollChevron) {
setState(() {
_showSessionListScrollChevron = false;
});
@ -54,12 +55,8 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
@override
Widget build(BuildContext context) {
final sessions = widget.friend.userStatus.activeSessions;
final appBarColor = Theme
.of(context)
.colorScheme
.surfaceVariant;
return Consumer<MessagingClient>(
builder: (context, mClient, _) {
final appBarColor = Theme.of(context).colorScheme.surfaceVariant;
return Consumer<MessagingClient>(builder: (context, mClient, _) {
final cache = mClient.getUserMessageCache(widget.friend.id);
return Scaffold(
appBar: AppBar(
@ -67,20 +64,24 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
crossAxisAlignment: CrossAxisAlignment.center,
children: [
FriendOnlineStatusIndicator(userStatus: widget.friend.userStatus),
const SizedBox(width: 8,),
const SizedBox(
width: 8,
),
Text(widget.friend.username),
if (widget.friend.isHeadless) Padding(
if (widget.friend.isHeadless)
Padding(
padding: const EdgeInsets.only(left: 12),
child: Icon(Icons.dns, size: 18, color: Theme
.of(context)
.colorScheme
.onSecondaryContainer
.withAlpha(150),
child: Icon(
Icons.dns,
size: 18,
color: Theme.of(context).colorScheme.onSecondaryContainer.withAlpha(150),
),
),
],
),
bottom: sessions.isNotEmpty && _sessionListOpen ? null : PreferredSize(
bottom: sessions.isNotEmpty && _sessionListOpen
? null
: PreferredSize(
preferredSize: const Size.fromHeight(1),
child: Container(
height: 1,
@ -88,8 +89,9 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
),
),
actions: [
if (sessions.isNotEmpty) AnimatedRotation(
turns: _sessionListOpen ? -1/4 : 1/4,
if (sessions.isNotEmpty)
AnimatedRotation(
turns: _sessionListOpen ? -1 / 4 : 1 / 4,
duration: const Duration(milliseconds: 200),
child: IconButton(
onPressed: () {
@ -100,7 +102,9 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
icon: const Icon(Icons.chevron_right),
),
),
const SizedBox(width: 4,)
const SizedBox(
width: 4,
)
],
scrolledUnderElevation: 0.0,
backgroundColor: appBarColor,
@ -109,15 +113,20 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
),
body: Column(
children: [
if (sessions.isNotEmpty) AnimatedSwitcher(
if (sessions.isNotEmpty)
AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
transitionBuilder: (child, animation) => SizeTransition(sizeFactor: animation, axis: Axis.vertical, child: child),
child: sessions.isEmpty || !_sessionListOpen ? null : Container(
transitionBuilder: (child, animation) =>
SizeTransition(sizeFactor: animation, axis: Axis.vertical, child: child),
child: sessions.isEmpty || !_sessionListOpen
? null
: Container(
constraints: const BoxConstraints(maxHeight: 64),
decoration: BoxDecoration(
color: appBarColor,
border: const Border(bottom: BorderSide(width: 1, color: Colors.black),)
),
border: const Border(
bottom: BorderSide(width: 1, color: Colors.black),
)),
child: Stack(
children: [
ListView.builder(
@ -162,9 +171,7 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
if (cache == null) {
return const Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
LinearProgressIndicator()
],
children: [LinearProgressIndicator()],
);
}
if (cache.error != null) {
@ -189,10 +196,7 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
child: Text(
"There are no messages here\nWhy not say hello?",
textAlign: TextAlign.center,
style: Theme
.of(context)
.textTheme
.titleMedium,
style: Theme.of(context).textTheme.titleMedium,
),
)
],
@ -203,17 +207,21 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
create: (BuildContext context) => AudioCacheClient(),
child: ListView.builder(
reverse: true,
physics: const BouncingScrollPhysics(),
physics: const BouncingScrollPhysics(decelerationRate: ScrollDecelerationRate.fast),
itemCount: cache.messages.length,
itemBuilder: (context, index) {
final entry = cache.messages[index];
if (index == cache.messages.length - 1) {
return Padding(
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
],
),
);
}
);
});
}
}