Refactor audio-message error handling
This commit is contained in:
parent
9ab4774f34
commit
aa882a13ae
3 changed files with 340 additions and 333 deletions
|
@ -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];
|
||||
|
|
|
@ -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,147 +97,140 @@ 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;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
snapshot.hasData ? IconButton(
|
||||
onPressed: () {
|
||||
switch (playerState.processingState) {
|
||||
case ProcessingState.idle:
|
||||
case ProcessingState.loading:
|
||||
case ProcessingState.buffering:
|
||||
break;
|
||||
case ProcessingState.ready:
|
||||
if (playerState.playing) {
|
||||
_audioPlayer.pause();
|
||||
} else {
|
||||
_audioPlayer.play();
|
||||
}
|
||||
break;
|
||||
case ProcessingState.completed:
|
||||
_audioPlayer.seek(Duration.zero);
|
||||
_audioPlayer.play();
|
||||
break;
|
||||
}
|
||||
},
|
||||
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)),
|
||||
),
|
||||
) : 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...
|
||||
builder: (context, setState) {
|
||||
return SliderTheme(
|
||||
data: SliderThemeData(
|
||||
inactiveTrackColor: widget.foregroundColor?.withAlpha(100),
|
||||
),
|
||||
child: Slider(
|
||||
thumbColor: widget.foregroundColor,
|
||||
value: _sliderValue,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
onChanged: (value) async {
|
||||
_audioPlayer.pause();
|
||||
setState(() {
|
||||
_sliderValue = value;
|
||||
});
|
||||
_audioPlayer.seek(Duration(
|
||||
milliseconds: (value * (_audioPlayer.duration?.inMilliseconds ?? 0)).round(),
|
||||
));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return IntrinsicWidth(
|
||||
child: StreamBuilder<PlayerState>(
|
||||
stream: _audioPlayer.playerStateStream,
|
||||
builder: (context, snapshot) {
|
||||
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,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
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:
|
||||
case ProcessingState.buffering:
|
||||
break;
|
||||
case ProcessingState.ready:
|
||||
if (playerState.playing) {
|
||||
_audioPlayer.pause();
|
||||
} else {
|
||||
_audioPlayer.play();
|
||||
}
|
||||
);
|
||||
break;
|
||||
case ProcessingState.completed:
|
||||
_audioPlayer.seek(Duration.zero);
|
||||
_audioPlayer.play();
|
||||
break;
|
||||
}
|
||||
}
|
||||
)
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
const SizedBox(width: 4,),
|
||||
StreamBuilder(
|
||||
stream: _audioPlayer.positionStream,
|
||||
builder: (context, snapshot) {
|
||||
return Text("${snapshot.data?.format() ?? "??"}/${_audioPlayer.duration?.format() ??
|
||||
"??"}",
|
||||
style: Theme
|
||||
.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(color: widget.foregroundColor?.withAlpha(150)),
|
||||
: null,
|
||||
color: widget.foregroundColor,
|
||||
icon: Icon(
|
||||
((_audioPlayer.duration ?? const Duration(days: 9999)) - _audioPlayer.position)
|
||||
.inMilliseconds <
|
||||
10
|
||||
? Icons.replay
|
||||
: ((playerState?.playing ?? false) ? Icons.pause : Icons.play_arrow),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
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...
|
||||
builder: (context, setState) {
|
||||
return SliderTheme(
|
||||
data: SliderThemeData(
|
||||
inactiveTrackColor: widget.foregroundColor?.withAlpha(100),
|
||||
),
|
||||
child: Slider(
|
||||
thumbColor: widget.foregroundColor,
|
||||
value: _sliderValue,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
onChanged: (value) async {
|
||||
_audioPlayer.pause();
|
||||
setState(() {
|
||||
_sliderValue = value;
|
||||
});
|
||||
_audioPlayer.seek(
|
||||
Duration(
|
||||
milliseconds: (value * (_audioPlayer.duration?.inMilliseconds ?? 0)).round(),
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
const Spacer(),
|
||||
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(),);
|
||||
}
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
StreamBuilder(
|
||||
stream: _audioPlayer.positionStream,
|
||||
builder: (context, snapshot) {
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
// TODO: implement wantKeepAlive
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,185 +55,191 @@ 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 cache = mClient.getUserMessageCache(widget.friend.id);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
final appBarColor = Theme.of(context).colorScheme.surfaceVariant;
|
||||
return Consumer<MessagingClient>(builder: (context, mClient, _) {
|
||||
final cache = mClient.getUserMessageCache(widget.friend.id);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
FriendOnlineStatusIndicator(userStatus: widget.friend.userStatus),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
Text(widget.friend.username),
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
bottom: sessions.isNotEmpty && _sessionListOpen
|
||||
? null
|
||||
: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(1),
|
||||
child: Container(
|
||||
height: 1,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
if (sessions.isNotEmpty)
|
||||
AnimatedRotation(
|
||||
turns: _sessionListOpen ? -1 / 4 : 1 / 4,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_sessionListOpen = !_sessionListOpen;
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
)
|
||||
],
|
||||
scrolledUnderElevation: 0.0,
|
||||
backgroundColor: appBarColor,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
shadowColor: Colors.transparent,
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
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(
|
||||
constraints: const BoxConstraints(maxHeight: 64),
|
||||
decoration: BoxDecoration(
|
||||
color: appBarColor,
|
||||
border: const Border(
|
||||
bottom: BorderSide(width: 1, color: Colors.black),
|
||||
)),
|
||||
child: Stack(
|
||||
children: [
|
||||
ListView.builder(
|
||||
controller: _sessionListScrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: sessions.length,
|
||||
itemBuilder: (context, index) => SessionTile(session: sessions[index]),
|
||||
),
|
||||
AnimatedOpacity(
|
||||
opacity: _shevronOpacity,
|
||||
curve: Curves.easeOut,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(left: 16, right: 4, top: 1, bottom: 1),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
colors: [
|
||||
appBarColor.withOpacity(0),
|
||||
appBarColor,
|
||||
appBarColor,
|
||||
],
|
||||
),
|
||||
),
|
||||
height: double.infinity,
|
||||
child: const Icon(Icons.chevron_right),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
FriendOnlineStatusIndicator(userStatus: widget.friend.userStatus),
|
||||
const SizedBox(width: 8,),
|
||||
Text(widget.friend.username),
|
||||
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),
|
||||
),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
if (cache == null) {
|
||||
return const Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [LinearProgressIndicator()],
|
||||
);
|
||||
}
|
||||
if (cache.error != null) {
|
||||
return DefaultErrorWidget(
|
||||
message: cache.error.toString(),
|
||||
onRetry: () {
|
||||
setState(() {
|
||||
mClient.deleteUserMessageCache(widget.friend.id);
|
||||
});
|
||||
mClient.loadUserMessageCache(widget.friend.id);
|
||||
},
|
||||
);
|
||||
}
|
||||
if (cache.messages.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.message_outlined),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 24),
|
||||
child: Text(
|
||||
"There are no messages here\nWhy not say hello?",
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return Provider(
|
||||
create: (BuildContext context) => AudioCacheClient(),
|
||||
child: ListView.builder(
|
||||
reverse: true,
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
return MessageBubble(
|
||||
message: entry,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
bottom: sessions.isNotEmpty && _sessionListOpen ? null : PreferredSize(
|
||||
preferredSize: const Size.fromHeight(1),
|
||||
child: Container(
|
||||
height: 1,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
if (sessions.isNotEmpty) AnimatedRotation(
|
||||
turns: _sessionListOpen ? -1/4 : 1/4,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_sessionListOpen = !_sessionListOpen;
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4,)
|
||||
],
|
||||
scrolledUnderElevation: 0.0,
|
||||
backgroundColor: appBarColor,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
shadowColor: Colors.transparent,
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
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(
|
||||
constraints: const BoxConstraints(maxHeight: 64),
|
||||
decoration: BoxDecoration(
|
||||
color: appBarColor,
|
||||
border: const Border(bottom: BorderSide(width: 1, color: Colors.black),)
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
ListView.builder(
|
||||
controller: _sessionListScrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: sessions.length,
|
||||
itemBuilder: (context, index) => SessionTile(session: sessions[index]),
|
||||
),
|
||||
AnimatedOpacity(
|
||||
opacity: _shevronOpacity,
|
||||
curve: Curves.easeOut,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(left: 16, right: 4, top: 1, bottom: 1),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
colors: [
|
||||
appBarColor.withOpacity(0),
|
||||
appBarColor,
|
||||
appBarColor,
|
||||
],
|
||||
),
|
||||
),
|
||||
height: double.infinity,
|
||||
child: const Icon(Icons.chevron_right),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
Builder(
|
||||
builder: (context) {
|
||||
if (cache == null) {
|
||||
return const Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
LinearProgressIndicator()
|
||||
],
|
||||
);
|
||||
}
|
||||
if (cache.error != null) {
|
||||
return DefaultErrorWidget(
|
||||
message: cache.error.toString(),
|
||||
onRetry: () {
|
||||
setState(() {
|
||||
mClient.deleteUserMessageCache(widget.friend.id);
|
||||
});
|
||||
mClient.loadUserMessageCache(widget.friend.id);
|
||||
},
|
||||
);
|
||||
}
|
||||
if (cache.messages.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.message_outlined),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 24),
|
||||
child: Text(
|
||||
"There are no messages here\nWhy not say hello?",
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme
|
||||
.of(context)
|
||||
.textTheme
|
||||
.titleMedium,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return Provider(
|
||||
create: (BuildContext context) => AudioCacheClient(),
|
||||
child: ListView.builder(
|
||||
reverse: true,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
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,),
|
||||
);
|
||||
}
|
||||
return MessageBubble(message: entry,);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
MessageInputBar(
|
||||
recipient: widget.friend,
|
||||
disabled: cache == null || cache.error != null,
|
||||
onMessageSent: () {
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
],
|
||||
MessageInputBar(
|
||||
recipient: widget.friend,
|
||||
disabled: cache == null || cache.error != null,
|
||||
onMessageSent: () {
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue