diff --git a/lib/models/session.dart b/lib/models/session.dart index 991c493..d71d2b0 100644 --- a/lib/models/session.dart +++ b/lib/models/session.dart @@ -10,10 +10,11 @@ class Session { final List 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, ); } } \ No newline at end of file diff --git a/lib/widgets/message_audio_player.dart b/lib/widgets/message_audio_player.dart index 2680b29..08042cf 100644 --- a/lib/widgets/message_audio_player.dart +++ b/lib/widgets/message_audio_player.dart @@ -27,125 +27,154 @@ class _MessageAudioPlayerState extends State { 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( - 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(),); + } } - }, ), ); } diff --git a/lib/widgets/message_session_invite.dart b/lib/widgets/message_session_invite.dart index 24e39b5..a86152b 100644 --- a/lib/widgets/message_session_invite.dart +++ b/lib/widgets/message_session_invite.dart @@ -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), diff --git a/lib/widgets/messages_list.dart b/lib/widgets/messages_list.dart index 5aef62e..baa501f 100644 --- a/lib/widgets/messages_list.dart +++ b/lib/widgets/messages_list.dart @@ -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),