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 List<String> tags;
|
||||||
final bool headlessHost;
|
final bool headlessHost;
|
||||||
final String hostUsername;
|
final String hostUsername;
|
||||||
|
final SessionAccessLevel accessLevel;
|
||||||
|
|
||||||
Session({required this.id, required this.name, required this.sessionUsers, required this.thumbnail,
|
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.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) {
|
factory Session.fromMap(Map map) {
|
||||||
|
@ -29,6 +30,7 @@ class Session {
|
||||||
tags: ((map["tags"] as List?) ?? []).map((e) => e.toString()).toList(),
|
tags: ((map["tags"] as List?) ?? []).map((e) => e.toString()).toList(),
|
||||||
headlessHost: map["headlessHost"] ?? false,
|
headlessHost: map["headlessHost"] ?? false,
|
||||||
hostUsername: map["hostUsername"] ?? "",
|
hostUsername: map["hostUsername"] ?? "",
|
||||||
|
accessLevel: SessionAccessLevel.fromName(map["accessLevel"]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,12 +47,39 @@ class Session {
|
||||||
"tags": shallow ? [] : throw UnimplementedError(),
|
"tags": shallow ? [] : throw UnimplementedError(),
|
||||||
"headlessHost": headlessHost,
|
"headlessHost": headlessHost,
|
||||||
"hostUsername": hostUsername,
|
"hostUsername": hostUsername,
|
||||||
|
"accessLevel": accessLevel.name, // This probably wont work, the API usually expects integers.
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get isLive => !hasEnded && isValid;
|
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 {
|
class SessionUser {
|
||||||
final String id;
|
final String id;
|
||||||
final String username;
|
final String username;
|
||||||
|
@ -61,10 +90,10 @@ class SessionUser {
|
||||||
|
|
||||||
factory SessionUser.fromMap(Map map) {
|
factory SessionUser.fromMap(Map map) {
|
||||||
return SessionUser(
|
return SessionUser(
|
||||||
id: map["userID"],
|
id: map["userID"] ?? "",
|
||||||
username: map["username"],
|
username: map["username"] ?? "Unknown",
|
||||||
isPresent: map["isPresent"],
|
isPresent: map["isPresent"] ?? false,
|
||||||
outputDevice: map["outputDevice"],
|
outputDevice: map["outputDevice"] ?? 0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -27,125 +27,154 @@ class _MessageAudioPlayerState extends State<MessageAudioPlayer> {
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
_audioPlayer.setAudioSource(AudioSource.uri(Uri.parse(
|
_audioPlayer.setUrl(
|
||||||
Aux.neosDbToHttp(AudioClipContent.fromMap(jsonDecode(widget.message.content)).assetUri)
|
Aux.neosDbToHttp(AudioClipContent
|
||||||
))).whenComplete(() => _audioPlayer.setLoopMode(LoopMode.off));
|
.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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (!Platform.isAndroid) {
|
if (!Platform.isAndroid) {
|
||||||
return Padding(
|
return _createErrorWidget("Sorry, audio-messages are not\n supported on this platform.");
|
||||||
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 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.hasData) {
|
||||||
final playerState = snapshot.data as PlayerState;
|
final playerState = snapshot.data as PlayerState;
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
switch (playerState.processingState) {
|
switch (playerState.processingState) {
|
||||||
case ProcessingState.idle:
|
case ProcessingState.idle:
|
||||||
case ProcessingState.loading:
|
case ProcessingState.loading:
|
||||||
case ProcessingState.buffering:
|
case ProcessingState.buffering:
|
||||||
break;
|
break;
|
||||||
case ProcessingState.ready:
|
case ProcessingState.ready:
|
||||||
_audioPlayer.play();
|
_audioPlayer.play();
|
||||||
break;
|
break;
|
||||||
case ProcessingState.completed:
|
case ProcessingState.completed:
|
||||||
_audioPlayer.seek(Duration.zero);
|
_audioPlayer.seek(Duration.zero);
|
||||||
_audioPlayer.play();
|
_audioPlayer.play();
|
||||||
break;
|
break;
|
||||||
}},
|
}
|
||||||
icon: playerState.processingState == ProcessingState.loading
|
},
|
||||||
? const Center(child: CircularProgressIndicator(),)
|
icon: SizedBox(
|
||||||
: Icon((_audioPlayer.duration! - _audioPlayer.position).inMilliseconds < 10 ? Icons.replay
|
width: 24,
|
||||||
: (playerState.playing ? Icons.pause : Icons.play_arrow)),
|
height: 24,
|
||||||
),
|
child: playerState.processingState == ProcessingState.loading
|
||||||
StreamBuilder(
|
? const Center(child: CircularProgressIndicator(),)
|
||||||
stream: _audioPlayer.positionStream,
|
: Icon(((_audioPlayer.duration ?? Duration.zero) - _audioPlayer.position).inMilliseconds <
|
||||||
builder: (context, snapshot) {
|
10 ? Icons.replay
|
||||||
_sliderValue = (_audioPlayer.position.inMilliseconds /
|
: (playerState.playing ? Icons.pause : Icons.play_arrow)),
|
||||||
(_audioPlayer.duration?.inMilliseconds ?? 0)).clamp(0, 1);
|
),
|
||||||
return StatefulBuilder(
|
),
|
||||||
builder: (context, setState) {
|
StreamBuilder(
|
||||||
return Slider(
|
stream: _audioPlayer.positionStream,
|
||||||
value: _sliderValue,
|
builder: (context, snapshot) {
|
||||||
min: 0.0,
|
_sliderValue = (_audioPlayer.position.inMilliseconds /
|
||||||
max: 1.0,
|
(_audioPlayer.duration?.inMilliseconds ?? 0)).clamp(0, 1);
|
||||||
onChanged: (value) async {
|
return StatefulBuilder(
|
||||||
_audioPlayer.pause();
|
builder: (context, setState) {
|
||||||
setState(() {
|
return Slider(
|
||||||
_sliderValue = value;
|
value: _sliderValue,
|
||||||
});
|
min: 0.0,
|
||||||
_audioPlayer.seek(Duration(
|
max: 1.0,
|
||||||
milliseconds: (value * (_audioPlayer.duration?.inMilliseconds ?? 0)).round(),
|
onChanged: (value) async {
|
||||||
));
|
_audioPlayer.pause();
|
||||||
},
|
setState(() {
|
||||||
|
_sliderValue = value;
|
||||||
|
});
|
||||||
|
_audioPlayer.seek(Duration(
|
||||||
|
milliseconds: (value * (_audioPlayer.duration?.inMilliseconds ?? 0)).round(),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
}
|
],
|
||||||
)
|
),
|
||||||
],
|
Row(
|
||||||
),
|
mainAxisSize: MainAxisSize.max,
|
||||||
Row(
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
mainAxisSize: MainAxisSize.max,
|
children: [
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
const SizedBox(width: 12),
|
||||||
children: [
|
StreamBuilder(
|
||||||
const SizedBox(width: 12),
|
stream: _audioPlayer.positionStream,
|
||||||
StreamBuilder(
|
builder: (context, snapshot) {
|
||||||
stream: _audioPlayer.positionStream,
|
return Text("${snapshot.data?.format() ?? "??"}/${_audioPlayer.duration?.format() ??
|
||||||
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),
|
|
||||||
),
|
),
|
||||||
),
|
const Spacer(),
|
||||||
const SizedBox(width: 4,),
|
Padding(
|
||||||
if (widget.message.senderId == ClientHolder.of(context).apiClient.userId) Padding(
|
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||||
padding: const EdgeInsets.only(right: 12.0),
|
child: Text(
|
||||||
child: MessageStateIndicator(messageState: widget.message.state),
|
_dateFormat.format(widget.message.sendTime),
|
||||||
),
|
style: Theme
|
||||||
],
|
.of(context)
|
||||||
)
|
.textTheme
|
||||||
],
|
.labelMedium
|
||||||
);
|
?.copyWith(color: Colors.white54),
|
||||||
} else {
|
),
|
||||||
return const Center(child: CircularProgressIndicator(),);
|
),
|
||||||
|
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),
|
padding: const EdgeInsets.only(left: 10),
|
||||||
constraints: const BoxConstraints(maxWidth: 300),
|
constraints: const BoxConstraints(maxWidth: 300),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
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(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
|
|
|
@ -583,6 +583,7 @@ class SessionPopup extends StatelessWidget {
|
||||||
style: Theme.of(context).textTheme.labelMedium,
|
style: Theme.of(context).textTheme.labelMedium,
|
||||||
softWrap: true,
|
softWrap: true,
|
||||||
),
|
),
|
||||||
|
Text("Access: ${session.accessLevel.toReadableString()}"),
|
||||||
Text("Users: ${session.sessionUsers.length}", style: Theme.of(context).textTheme.labelMedium),
|
Text("Users: ${session.sessionUsers.length}", style: Theme.of(context).textTheme.labelMedium),
|
||||||
Text("Maximum users: ${session.maxUsers}", 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),
|
Text("Headless: ${session.headlessHost ? "Yes" : "No"}", style: Theme.of(context).textTheme.labelMedium),
|
||||||
|
|
Loading…
Reference in a new issue