Fix audio player error handling and add session access level display
This commit is contained in:
parent
4e5ac6a8d4
commit
ca198a7cc4
4 changed files with 177 additions and 112 deletions
|
@ -10,10 +10,11 @@ class Session {
|
|||
final List<String> tags;
|
||||
final bool headlessHost;
|
||||
final String hostUsername;
|
||||
final SessionAccessLevel accessLevel;
|
||||
|
||||
Session({required this.id, required this.name, required this.sessionUsers, required this.thumbnail,
|
||||
required this.maxUsers, required this.hasEnded, required this.isValid, required this.description,
|
||||
required this.tags, required this.headlessHost, required this.hostUsername,
|
||||
required this.tags, required this.headlessHost, required this.hostUsername, required this.accessLevel,
|
||||
});
|
||||
|
||||
factory Session.fromMap(Map map) {
|
||||
|
@ -29,6 +30,7 @@ class Session {
|
|||
tags: ((map["tags"] as List?) ?? []).map((e) => e.toString()).toList(),
|
||||
headlessHost: map["headlessHost"] ?? false,
|
||||
hostUsername: map["hostUsername"] ?? "",
|
||||
accessLevel: SessionAccessLevel.fromName(map["accessLevel"]),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -45,12 +47,39 @@ class Session {
|
|||
"tags": shallow ? [] : throw UnimplementedError(),
|
||||
"headlessHost": headlessHost,
|
||||
"hostUsername": hostUsername,
|
||||
"accessLevel": accessLevel.name, // This probably wont work, the API usually expects integers.
|
||||
};
|
||||
}
|
||||
|
||||
bool get isLive => !hasEnded && isValid;
|
||||
}
|
||||
|
||||
enum SessionAccessLevel {
|
||||
unknown,
|
||||
private,
|
||||
friends,
|
||||
friendsOfFriends,
|
||||
anyone;
|
||||
|
||||
static const _readableNamesMap = {
|
||||
SessionAccessLevel.unknown: "Unknown",
|
||||
SessionAccessLevel.private: "Private",
|
||||
SessionAccessLevel.friends: "Contacts",
|
||||
SessionAccessLevel.friendsOfFriends: "Contacts+",
|
||||
SessionAccessLevel.anyone: "Anyone",
|
||||
};
|
||||
|
||||
factory SessionAccessLevel.fromName(String? name) {
|
||||
return SessionAccessLevel.values.firstWhere((element) => element.name.toLowerCase() == name?.toLowerCase(),
|
||||
orElse: () => SessionAccessLevel.unknown,
|
||||
);
|
||||
}
|
||||
|
||||
String toReadableString() {
|
||||
return SessionAccessLevel._readableNamesMap[this] ?? "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
class SessionUser {
|
||||
final String id;
|
||||
final String username;
|
||||
|
@ -61,10 +90,10 @@ class SessionUser {
|
|||
|
||||
factory SessionUser.fromMap(Map map) {
|
||||
return SessionUser(
|
||||
id: map["userID"],
|
||||
username: map["username"],
|
||||
isPresent: map["isPresent"],
|
||||
outputDevice: map["outputDevice"],
|
||||
id: map["userID"] ?? "",
|
||||
username: map["username"] ?? "Unknown",
|
||||
isPresent: map["isPresent"] ?? false,
|
||||
outputDevice: map["outputDevice"] ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -27,125 +27,154 @@ class _MessageAudioPlayerState extends State<MessageAudioPlayer> {
|
|||
void initState() {
|
||||
super.initState();
|
||||
if (Platform.isAndroid) {
|
||||
_audioPlayer.setAudioSource(AudioSource.uri(Uri.parse(
|
||||
Aux.neosDbToHttp(AudioClipContent.fromMap(jsonDecode(widget.message.content)).assetUri)
|
||||
))).whenComplete(() => _audioPlayer.setLoopMode(LoopMode.off));
|
||||
_audioPlayer.setUrl(
|
||||
Aux.neosDbToHttp(AudioClipContent
|
||||
.fromMap(jsonDecode(widget.message.content))
|
||||
.assetUri),
|
||||
preload: true).whenComplete(() => _audioPlayer.setLoopMode(LoopMode.off));
|
||||
}
|
||||
}
|
||||
|
||||
Widget _createErrorWidget(String error) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
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,
|
||||
softWrap: true,
|
||||
maxLines: 3,
|
||||
style: Theme
|
||||
.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(color: Theme
|
||||
.of(context)
|
||||
.colorScheme
|
||||
.error),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!Platform.isAndroid) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.error_outline, color: Theme.of(context).colorScheme.error,),
|
||||
const SizedBox(height: 4,),
|
||||
Text("Sorry, audio-messages are not\n supported on this platform.", textAlign: TextAlign.center,
|
||||
softWrap: true,
|
||||
maxLines: 3,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
return _createErrorWidget("Sorry, audio-messages are not\n supported on this platform.");
|
||||
}
|
||||
return IntrinsicWidth(
|
||||
child: StreamBuilder<PlayerState>(
|
||||
stream: _audioPlayer.playerStateStream,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
final playerState = snapshot.data as PlayerState;
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
switch (playerState.processingState) {
|
||||
case ProcessingState.idle:
|
||||
case ProcessingState.loading:
|
||||
case ProcessingState.buffering:
|
||||
break;
|
||||
case ProcessingState.ready:
|
||||
_audioPlayer.play();
|
||||
break;
|
||||
case ProcessingState.completed:
|
||||
_audioPlayer.seek(Duration.zero);
|
||||
_audioPlayer.play();
|
||||
break;
|
||||
}},
|
||||
icon: playerState.processingState == ProcessingState.loading
|
||||
? const Center(child: CircularProgressIndicator(),)
|
||||
: Icon((_audioPlayer.duration! - _audioPlayer.position).inMilliseconds < 10 ? Icons.replay
|
||||
: (playerState.playing ? Icons.pause : Icons.play_arrow)),
|
||||
),
|
||||
StreamBuilder(
|
||||
stream: _audioPlayer.positionStream,
|
||||
builder: (context, snapshot) {
|
||||
_sliderValue = (_audioPlayer.position.inMilliseconds /
|
||||
(_audioPlayer.duration?.inMilliseconds ?? 0)).clamp(0, 1);
|
||||
return StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
return Slider(
|
||||
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(),
|
||||
));
|
||||
},
|
||||
stream: _audioPlayer.playerStateStream,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
final playerState = snapshot.data as PlayerState;
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
switch (playerState.processingState) {
|
||||
case ProcessingState.idle:
|
||||
case ProcessingState.loading:
|
||||
case ProcessingState.buffering:
|
||||
break;
|
||||
case ProcessingState.ready:
|
||||
_audioPlayer.play();
|
||||
break;
|
||||
case ProcessingState.completed:
|
||||
_audioPlayer.seek(Duration.zero);
|
||||
_audioPlayer.play();
|
||||
break;
|
||||
}
|
||||
},
|
||||
icon: SizedBox(
|
||||
width: 24,
|
||||
height: 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)),
|
||||
),
|
||||
),
|
||||
StreamBuilder(
|
||||
stream: _audioPlayer.positionStream,
|
||||
builder: (context, snapshot) {
|
||||
_sliderValue = (_audioPlayer.position.inMilliseconds /
|
||||
(_audioPlayer.duration?.inMilliseconds ?? 0)).clamp(0, 1);
|
||||
return StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
return Slider(
|
||||
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(),
|
||||
));
|
||||
},
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
)
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const SizedBox(width: 12),
|
||||
StreamBuilder(
|
||||
stream: _audioPlayer.positionStream,
|
||||
builder: (context, snapshot) {
|
||||
return Text("${snapshot.data?.format() ?? "??"}/${_audioPlayer.duration?.format() ?? "??"}");
|
||||
}
|
||||
),
|
||||
const Spacer(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: Text(
|
||||
_dateFormat.format(widget.message.sendTime),
|
||||
style: Theme
|
||||
.of(context)
|
||||
.textTheme
|
||||
.labelMedium
|
||||
?.copyWith(color: Colors.white54),
|
||||
)
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const SizedBox(width: 12),
|
||||
StreamBuilder(
|
||||
stream: _audioPlayer.positionStream,
|
||||
builder: (context, snapshot) {
|
||||
return Text("${snapshot.data?.format() ?? "??"}/${_audioPlayer.duration?.format() ??
|
||||
"??"}");
|
||||
}
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4,),
|
||||
if (widget.message.senderId == ClientHolder.of(context).apiClient.userId) Padding(
|
||||
padding: const EdgeInsets.only(right: 12.0),
|
||||
child: MessageStateIndicator(messageState: widget.message.state),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return const Center(child: CircularProgressIndicator(),);
|
||||
const Spacer(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: Text(
|
||||
_dateFormat.format(widget.message.sendTime),
|
||||
style: Theme
|
||||
.of(context)
|
||||
.textTheme
|
||||
.labelMedium
|
||||
?.copyWith(color: Colors.white54),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4,),
|
||||
if (widget.message.senderId == ClientHolder
|
||||
.of(context)
|
||||
.apiClient
|
||||
.userId) Padding(
|
||||
padding: const EdgeInsets.only(right: 12.0),
|
||||
child: MessageStateIndicator(messageState: widget.message.state),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
} 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(),);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -26,13 +26,19 @@ class MessageSessionInvite extends StatelessWidget {
|
|||
padding: const EdgeInsets.only(left: 10),
|
||||
constraints: const BoxConstraints(maxWidth: 300),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(sessionInfo.name, maxLines: null, softWrap: true, style: Theme.of(context).textTheme.titleMedium,),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: Text(sessionInfo.name, maxLines: null, softWrap: true, style: Theme.of(context).textTheme.titleMedium,),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
|
|
|
@ -583,6 +583,7 @@ class SessionPopup extends StatelessWidget {
|
|||
style: Theme.of(context).textTheme.labelMedium,
|
||||
softWrap: true,
|
||||
),
|
||||
Text("Access: ${session.accessLevel.toReadableString()}"),
|
||||
Text("Users: ${session.sessionUsers.length}", style: Theme.of(context).textTheme.labelMedium),
|
||||
Text("Maximum users: ${session.maxUsers}", style: Theme.of(context).textTheme.labelMedium),
|
||||
Text("Headless: ${session.headlessHost ? "Yes" : "No"}", style: Theme.of(context).textTheme.labelMedium),
|
||||
|
|
Loading…
Reference in a new issue