diff --git a/lib/main.dart b/lib/main.dart index fc84aa7..6dfae85 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,6 +9,7 @@ import 'package:contacts_plus_plus/models/sem_ver.dart'; import 'package:contacts_plus_plus/widgets/friends/friends_list.dart'; import 'package:contacts_plus_plus/widgets/login_screen.dart'; import 'package:contacts_plus_plus/widgets/update_notifier.dart'; +import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; import 'package:flutter_phoenix/flutter_phoenix.dart'; import 'package:logging/logging.dart'; @@ -111,38 +112,40 @@ class _ContactsPlusPlusState extends State { return ClientHolder( settingsClient: widget.settingsClient, authenticationData: _authData, - child: MaterialApp( - debugShowCheckedModeBanner: false, - title: 'Contacts++', - theme: ThemeData( + child: DynamicColorBuilder( + builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) => MaterialApp( + debugShowCheckedModeBanner: false, + title: 'Contacts++', + theme: ThemeData( useMaterial3: true, textTheme: _typography.white, - colorScheme: ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.dark) - ), - home: Builder( // Builder is necessary here since we need a context which has access to the ClientHolder - builder: (context) { - showUpdateDialogOnFirstBuild(context); - final clientHolder = ClientHolder.of(context); - return _authData.isAuthenticated ? - ChangeNotifierProvider( // This doesn't need to be a proxy provider since the arguments should never change during it's lifetime. - create: (context) => - MessagingClient( - apiClient: clientHolder.apiClient, - notificationClient: clientHolder.notificationClient, - ), - child: const FriendsList(), - ) : - LoginScreen( - onLoginSuccessful: (AuthenticationData authData) async { - if (authData.isAuthenticated) { - setState(() { - _authData = authData; - }); - } - }, - ); - } - ) + colorScheme: darkDynamic ?? ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.dark), + ), + home: Builder( // Builder is necessary here since we need a context which has access to the ClientHolder + builder: (context) { + showUpdateDialogOnFirstBuild(context); + final clientHolder = ClientHolder.of(context); + return _authData.isAuthenticated ? + ChangeNotifierProvider( // This doesn't need to be a proxy provider since the arguments should never change during it's lifetime. + create: (context) => + MessagingClient( + apiClient: clientHolder.apiClient, + notificationClient: clientHolder.notificationClient, + ), + child: const FriendsList(), + ) : + LoginScreen( + onLoginSuccessful: (AuthenticationData authData) async { + if (authData.isAuthenticated) { + setState(() { + _authData = authData; + }); + } + }, + ); + } + ) + ), ), ); } diff --git a/lib/widgets/friends/expanding_input_fab.dart b/lib/widgets/friends/expanding_input_fab.dart index d11025b..fffd932 100644 --- a/lib/widgets/friends/expanding_input_fab.dart +++ b/lib/widgets/friends/expanding_input_fab.dart @@ -29,12 +29,10 @@ class _ExpandingInputFabState extends State { duration: const Duration(milliseconds: 200), reverseDuration: const Duration(milliseconds: 200), curve: Curves.easeInOut, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: Theme.of(context).colorScheme.secondaryContainer, - ), - padding: const EdgeInsets.all(4), + child: Material( + elevation: 4, + borderRadius: BorderRadius.circular(16), + color: Theme.of(context).colorScheme.primaryContainer, child: Row( mainAxisAlignment: MainAxisAlignment.end, mainAxisSize: MainAxisSize.min, @@ -46,10 +44,11 @@ class _ExpandingInputFabState extends State { onChanged: widget.onInputChanged, controller: _inputController, decoration: const InputDecoration( - border: OutlineInputBorder( - borderSide: BorderSide.none, - ), - isDense: true + border: OutlineInputBorder( + borderSide: BorderSide.none, + ), + isDense: true, + contentPadding: EdgeInsets.symmetric(vertical: 0, horizontal: 16), ), ) : null, ), diff --git a/lib/widgets/friends/friends_list.dart b/lib/widgets/friends/friends_list.dart index f4591e9..f27b5d4 100644 --- a/lib/widgets/friends/friends_list.dart +++ b/lib/widgets/friends/friends_list.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:ffi'; import 'package:contacts_plus_plus/apis/user_api.dart'; import 'package:contacts_plus_plus/client_holder.dart'; diff --git a/lib/widgets/generic_avatar.dart b/lib/widgets/generic_avatar.dart index 21a29fc..fa30337 100644 --- a/lib/widgets/generic_avatar.dart +++ b/lib/widgets/generic_avatar.dart @@ -2,22 +2,25 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; class GenericAvatar extends StatelessWidget { - const GenericAvatar({this.imageUri="", super.key, this.placeholderIcon=Icons.person, this.radius}); + const GenericAvatar({this.imageUri="", super.key, this.placeholderIcon=Icons.person, this.radius, this.foregroundColor}); final String imageUri; final IconData placeholderIcon; final double? radius; + final Color? foregroundColor; @override Widget build(BuildContext context) { return imageUri.isEmpty ? CircleAvatar( radius: radius, + foregroundColor: foregroundColor, backgroundColor: Colors.transparent, - child: Icon(placeholderIcon), + child: Icon(placeholderIcon, color: foregroundColor,), ) : CachedNetworkImage( imageBuilder: (context, imageProvider) { return CircleAvatar( foregroundImage: imageProvider, + foregroundColor: foregroundColor, backgroundColor: Colors.transparent, radius: radius, ); @@ -26,17 +29,19 @@ class GenericAvatar extends StatelessWidget { placeholder: (context, url) { return CircleAvatar( backgroundColor: Colors.white54, + foregroundColor: foregroundColor, radius: radius, - child: const Padding( - padding: EdgeInsets.all(8.0), - child: CircularProgressIndicator(color: Colors.black38, strokeWidth: 2), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: CircularProgressIndicator(color: foregroundColor, strokeWidth: 2), ), ); }, errorWidget: (context, error, what) => CircleAvatar( radius: radius, + foregroundColor: foregroundColor, backgroundColor: Colors.transparent, - child: Icon(placeholderIcon), + child: Icon(placeholderIcon, color: foregroundColor,), ), ); } diff --git a/lib/widgets/messages/message_asset.dart b/lib/widgets/messages/message_asset.dart index 489cc81..f1f0fba 100644 --- a/lib/widgets/messages/message_asset.dart +++ b/lib/widgets/messages/message_asset.dart @@ -3,21 +3,19 @@ import 'dart:convert'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:contacts_plus_plus/auxiliary.dart'; -import 'package:contacts_plus_plus/client_holder.dart'; import 'package:contacts_plus_plus/models/photo_asset.dart'; import 'package:contacts_plus_plus/models/message.dart'; import 'package:contacts_plus_plus/string_formatter.dart'; import 'package:contacts_plus_plus/widgets/formatted_text.dart'; import 'package:contacts_plus_plus/widgets/messages/message_state_indicator.dart'; import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; import 'package:photo_view/photo_view.dart'; class MessageAsset extends StatelessWidget { - MessageAsset({required this.message, super.key}); + const MessageAsset({required this.message, this.foregroundColor, super.key}); final Message message; - final DateFormat _dateFormat = DateFormat.Hm(); + final Color? foregroundColor; @override Widget build(BuildContext context) { @@ -29,7 +27,6 @@ class MessageAsset extends StatelessWidget { final formattedName = FormatNode.fromText(content["name"]); return Container( constraints: const BoxConstraints(maxWidth: 300), - padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), child: Column( children: [ CachedNetworkImage( @@ -62,34 +59,20 @@ class MessageAsset extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Expanded( - child: FormattedText( - formattedName, - maxLines: null, - style: Theme - .of(context) - .textTheme - .bodySmall - ?.copyWith(color: Colors.white60), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: FormattedText( + formattedName, + maxLines: null, + style: Theme + .of(context) + .textTheme + .bodySmall + ?.copyWith(color: foregroundColor), + ), ), ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), - child: Text( - _dateFormat.format(message.sendTime.toLocal()), - style: Theme - .of(context) - .textTheme - .labelMedium - ?.copyWith(color: Colors.white54), - ), - ), - if (message.senderId == ClientHolder - .of(context) - .apiClient - .userId) Padding( - padding: const EdgeInsets.only(left: 4.0), - child: MessageStateIndicator(messageState: message.state), - ), + MessageStateIndicator(message: message, foregroundColor: foregroundColor,), ], ), ], diff --git a/lib/widgets/messages/message_audio_player.dart b/lib/widgets/messages/message_audio_player.dart index 488b5c8..5a864a9 100644 --- a/lib/widgets/messages/message_audio_player.dart +++ b/lib/widgets/messages/message_audio_player.dart @@ -1,18 +1,18 @@ import 'dart:convert'; import 'dart:io' show Platform; -import 'package:contacts_plus_plus/client_holder.dart'; import 'package:contacts_plus_plus/auxiliary.dart'; import 'package:contacts_plus_plus/models/message.dart'; import 'package:contacts_plus_plus/widgets/messages/message_state_indicator.dart'; +import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; import 'package:just_audio/just_audio.dart'; class MessageAudioPlayer extends StatefulWidget { - const MessageAudioPlayer({required this.message, super.key}); + const MessageAudioPlayer({required this.message, this.foregroundColor, super.key}); final Message message; + final Color? foregroundColor; @override State createState() => _MessageAudioPlayerState(); @@ -20,7 +20,6 @@ class MessageAudioPlayer extends StatefulWidget { class _MessageAudioPlayerState extends State { final AudioPlayer _audioPlayer = AudioPlayer(); - final DateFormat _dateFormat = DateFormat.Hm(); double _sliderValue = 0; @override @@ -75,9 +74,12 @@ class _MessageAudioPlayerState extends State { if (snapshot.hasData) { final playerState = snapshot.data as PlayerState; return Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, children: [ Row( - mainAxisSize: MainAxisSize.min, + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ IconButton( onPressed: () { @@ -87,7 +89,11 @@ class _MessageAudioPlayerState extends State { case ProcessingState.buffering: break; case ProcessingState.ready: - _audioPlayer.play(); + if (playerState.playing) { + _audioPlayer.pause(); + } else { + _audioPlayer.play(); + } break; case ProcessingState.completed: _audioPlayer.seek(Duration.zero); @@ -95,6 +101,7 @@ class _MessageAudioPlayerState extends State { break; } }, + color: widget.foregroundColor, icon: SizedBox( width: 24, height: 24, @@ -110,21 +117,27 @@ class _MessageAudioPlayerState extends State { builder: (context, snapshot) { _sliderValue = (_audioPlayer.position.inMilliseconds / (_audioPlayer.duration?.inMilliseconds ?? 0)).clamp(0, 1); - return StatefulBuilder( + return StatefulBuilder( // Not sure if this makes sense here... 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(), - )); - }, + 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(), + )); + }, + ), ); } ); @@ -134,36 +147,24 @@ class _MessageAudioPlayerState extends State { ), Row( mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - const SizedBox(width: 12), + 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(), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), - child: Text( - _dateFormat.format(widget.message.sendTime.toLocal()), - 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), - ), + MessageStateIndicator(message: widget.message, foregroundColor: widget.foregroundColor,), ], ) ], diff --git a/lib/widgets/messages/message_bubble.dart b/lib/widgets/messages/message_bubble.dart index a4d7d1d..95c45d0 100644 --- a/lib/widgets/messages/message_bubble.dart +++ b/lib/widgets/messages/message_bubble.dart @@ -1,267 +1,44 @@ +import 'package:contacts_plus_plus/client_holder.dart'; import 'package:contacts_plus_plus/models/message.dart'; -import 'package:contacts_plus_plus/widgets/formatted_text.dart'; import 'package:contacts_plus_plus/widgets/messages/message_asset.dart'; import 'package:contacts_plus_plus/widgets/messages/message_audio_player.dart'; import 'package:contacts_plus_plus/widgets/messages/message_session_invite.dart'; -import 'package:contacts_plus_plus/widgets/messages/message_state_indicator.dart'; +import 'package:contacts_plus_plus/widgets/messages/message_text.dart'; import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; -// The way these classes are laid out is pretty unclean, there's a lot of stuff that's shared between the different -// subwidgets with a lot of room for deduplication. Should probably redo this some day. - -class MyMessageBubble extends StatelessWidget { - MyMessageBubble({required this.message, super.key}); +class MessageBubble extends StatelessWidget { + const MessageBubble({required this.message, super.key}); final Message message; - final DateFormat _dateFormat = DateFormat.Hm(); @override Widget build(BuildContext context) { - switch (message.type) { - case MessageType.sessionInvite: - return Row( - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - color: Theme - .of(context) - .colorScheme - .primaryContainer, - margin: const EdgeInsets.only(left: 32, bottom: 16, right: 12), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 12), - child: MessageSessionInvite(message: message,), - ), + final bool mine = message.senderId == ClientHolder.of(context).apiClient.userId; + final colorScheme = Theme.of(context).colorScheme; + final foregroundColor = mine ? colorScheme.onPrimaryContainer : colorScheme.onSurfaceVariant; + final backgroundColor = mine ? colorScheme.primaryContainer : colorScheme.surfaceVariant; + return Padding( + padding: EdgeInsets.only(left: mine ? 32 : 12, bottom: 16, right: mine ? 12 : 32), + child: Row( + mainAxisAlignment: mine ? MainAxisAlignment.end : MainAxisAlignment.start, + children: [ + Material( + borderRadius: BorderRadius.circular(16), + color: backgroundColor, + textStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(color: foregroundColor), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + child: switch (message.type) { + MessageType.sessionInvite => + MessageSessionInvite(message: message, foregroundColor: foregroundColor,), + MessageType.object => MessageAsset(message: message, foregroundColor: foregroundColor,), + MessageType.sound => MessageAudioPlayer(message: message, foregroundColor: foregroundColor,), + MessageType.unknown || MessageType.text => MessageText(message: message, foregroundColor: foregroundColor,) + }, ), - ], - ); - case MessageType.object: - return Row( - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - color: Theme - .of(context) - .colorScheme - .primaryContainer, - margin: const EdgeInsets.only(left: 32, bottom: 16, right: 12), - child: Container( - constraints: const BoxConstraints(maxWidth: 300), - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12), - child: MessageAsset(message: message,), - ), - ), - ], - ); - case MessageType.unknown: - case MessageType.text: - return Row( - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - color: Theme - .of(context) - .colorScheme - .primaryContainer, - margin: const EdgeInsets.only(left: 32, bottom: 16, right: 12), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - FormattedText( - message.formattedContent, - softWrap: true, - maxLines: null, - style: Theme - .of(context) - .textTheme - .bodyLarge, - ), - const SizedBox(height: 6,), - Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), - child: Text( - _dateFormat.format(message.sendTime.toLocal()), - style: Theme - .of(context) - .textTheme - .labelMedium - ?.copyWith(color: Colors.white54), - ), - ), - MessageStateIndicator(messageState: message.state), - ], - ), - ], - ), - ), - ), - ), - ], - ); - case MessageType.sound: - return Row( - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - color: Theme - .of(context) - .colorScheme - .primaryContainer, - margin: const EdgeInsets.only(left: 32, bottom: 16, right: 12), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12), - child: MessageAudioPlayer(message: message,), - ), - ), - ], - ); - } - } -} - - -class OtherMessageBubble extends StatelessWidget { - OtherMessageBubble({required this.message, super.key}); - - final Message message; - final DateFormat _dateFormat = DateFormat.Hm(); - - @override - Widget build(BuildContext context) { - switch (message.type) { - case MessageType.sessionInvite: - return Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - color: Theme - .of(context) - .colorScheme - .secondaryContainer, - margin: const EdgeInsets.only(right: 32, bottom: 16, left: 12), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: MessageSessionInvite(message: message,), - ), - ), - ], - ); - case MessageType.object: - return Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - color: Theme - .of(context) - .colorScheme - .secondaryContainer, - margin: const EdgeInsets.only(right: 32, bottom: 16, left: 12), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: MessageAsset(message: message,), - ), - ), - ], - ); - case MessageType.unknown: - case MessageType.text: - return Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Flexible( - child: Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - color: Theme - .of(context) - .colorScheme - .secondaryContainer, - margin: const EdgeInsets.only(right: 32, bottom: 16, left: 12), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FormattedText( - message.formattedContent, - softWrap: true, - maxLines: null, - style: Theme - .of(context) - .textTheme - .bodyLarge, - ), - const SizedBox(height: 6,), - Text( - _dateFormat.format(message.sendTime.toLocal()), - style: Theme - .of(context) - .textTheme - .labelMedium - ?.copyWith(color: Colors.white54), - ), - ], - ), - ), - ), - ), - ], - ); - case MessageType.sound: - return Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - color: Theme - .of(context) - .colorScheme - .secondaryContainer, - margin: const EdgeInsets.only(right: 32, bottom: 16, left: 12), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: MessageAudioPlayer(message: message,), - ), - ), - ], - ); - } + ), + ], + ), + ); } } diff --git a/lib/widgets/messages/message_session_invite.dart b/lib/widgets/messages/message_session_invite.dart index 327bb06..f0fc9b3 100644 --- a/lib/widgets/messages/message_session_invite.dart +++ b/lib/widgets/messages/message_session_invite.dart @@ -1,6 +1,5 @@ import 'dart:convert'; -import 'package:contacts_plus_plus/client_holder.dart'; import 'package:contacts_plus_plus/auxiliary.dart'; import 'package:contacts_plus_plus/models/message.dart'; import 'package:contacts_plus_plus/models/session.dart'; @@ -9,83 +8,83 @@ import 'package:contacts_plus_plus/widgets/generic_avatar.dart'; import 'package:contacts_plus_plus/widgets/messages/messages_session_header.dart'; import 'package:contacts_plus_plus/widgets/messages/message_state_indicator.dart'; import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; class MessageSessionInvite extends StatelessWidget { - MessageSessionInvite({required this.message, super.key}); - final DateFormat _dateFormat = DateFormat.Hm(); + const MessageSessionInvite({required this.message, this.foregroundColor, super.key}); + + final Color? foregroundColor; final Message message; @override Widget build(BuildContext context) { final sessionInfo = Session.fromMap(jsonDecode(message.content)); - return TextButton( - onPressed: () { - showDialog(context: context, builder: (context) => SessionPopup(session: sessionInfo)); - }, - style: TextButton.styleFrom(padding: EdgeInsets.zero), - child: Container( - 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: Padding( - padding: const EdgeInsets.only(top: 4), - child: FormattedText(sessionInfo.formattedName, maxLines: null, softWrap: true, style: Theme.of(context).textTheme.titleMedium,), + return Container( + constraints: const BoxConstraints(maxWidth: 300), + child: TextButton( + onPressed: () { + showDialog(context: context, builder: (context) => SessionPopup(session: sessionInfo)); + }, + style: TextButton.styleFrom(padding: EdgeInsets.zero), + child: Container( + padding: const EdgeInsets.only(left: 4), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GenericAvatar( + imageUri: Aux.neosDbToHttp(Aux.neosDbToHttp(sessionInfo.thumbnail)), + placeholderIcon: Icons.no_photography, + foregroundColor: foregroundColor, + ), + const SizedBox(height: 4,), + Text("${sessionInfo.sessionUsers.length}/${sessionInfo.maxUsers}", style: Theme + .of(context) + .textTheme + .bodyMedium + ?.copyWith(color: foregroundColor),) + ], + ), ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - GenericAvatar( - imageUri: Aux.neosDbToHttp(Aux.neosDbToHttp(sessionInfo.thumbnail)), - placeholderIcon: Icons.no_photography, - ), - const SizedBox(height: 4,), - Text("${sessionInfo.sessionUsers.length}/${sessionInfo.maxUsers}") - ], + Expanded( + child: Padding( + padding: const EdgeInsets.all(8), + child: FormattedText(sessionInfo.formattedName, maxLines: null, softWrap: true, textAlign: TextAlign.start, style: Theme + .of(context) + .textTheme + .titleMedium?.copyWith(color: foregroundColor),), + ), ), - ), - ], - ), - const SizedBox(height: 8,), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded(child: Text("Hosted by ${sessionInfo.hostUsername}", overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.white60),)), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), - child: Text( - _dateFormat.format(message.sendTime.toLocal()), - style: Theme - .of(context) - .textTheme - .labelMedium - ?.copyWith(color: Colors.white54), - ), - ), - const SizedBox(width: 4,), - if (message.senderId == ClientHolder.of(context).apiClient.userId) Padding( - padding: const EdgeInsets.only(right: 12.0), - child: MessageStateIndicator(messageState: message.state), - ), - ], - ) - ], + ], + ), + const SizedBox(height: 8,), + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Hosted by ${sessionInfo.hostUsername}", overflow: TextOverflow.ellipsis, style: Theme + .of(context) + .textTheme + .bodySmall + ?.copyWith(color: foregroundColor?.withAlpha(150)),), + MessageStateIndicator(message: message, foregroundColor: foregroundColor,), + ], + ) + ], + ), ), ), ); } - } \ No newline at end of file diff --git a/lib/widgets/messages/message_state_indicator.dart b/lib/widgets/messages/message_state_indicator.dart index a9f85be..10ed11a 100644 --- a/lib/widgets/messages/message_state_indicator.dart +++ b/lib/widgets/messages/message_state_indicator.dart @@ -1,29 +1,45 @@ - +import 'package:contacts_plus_plus/client_holder.dart'; import 'package:contacts_plus_plus/models/message.dart'; import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; class MessageStateIndicator extends StatelessWidget { - const MessageStateIndicator({required this.messageState, super.key}); + MessageStateIndicator({required this.message, this.foregroundColor, super.key}); - final MessageState messageState; + final DateFormat _dateFormat = DateFormat.Hm(); + final Message message; + final Color? foregroundColor; @override Widget build(BuildContext context) { - late final IconData icon; - switch (messageState) { - case MessageState.local: - icon = Icons.alarm; - break; - case MessageState.sent: - icon = Icons.done; - break; - case MessageState.read: - icon = Icons.done_all; - break; - } - return Icon( - icon, - size: 12, + final color = foregroundColor?.withAlpha(150); + return Row( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: Text( + _dateFormat.format(message.sendTime.toLocal()), + style: Theme + .of(context) + .textTheme + .labelMedium + ?.copyWith(color: color), + ), + ), + if (message.senderId == ClientHolder + .of(context) + .apiClient + .userId) + Icon( + switch (message.state) { + MessageState.local => Icons.alarm, + MessageState.sent => Icons.done, + MessageState.read => Icons.done_all, + }, + size: 12, + color: color, + ), + ], ); } } diff --git a/lib/widgets/messages/message_text.dart b/lib/widgets/messages/message_text.dart new file mode 100644 index 0000000..944da42 --- /dev/null +++ b/lib/widgets/messages/message_text.dart @@ -0,0 +1,41 @@ +import 'package:contacts_plus_plus/models/message.dart'; +import 'package:contacts_plus_plus/widgets/formatted_text.dart'; +import 'package:contacts_plus_plus/widgets/messages/message_state_indicator.dart'; +import 'package:flutter/material.dart'; + +class MessageText extends StatelessWidget { + const MessageText({required this.message, this.foregroundColor, super.key}); + + final Message message; + final Color? foregroundColor; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Container( + constraints: const BoxConstraints(maxWidth: 300), + padding: const EdgeInsets.symmetric(horizontal: 8), + child: FormattedText( + message.formattedContent, + softWrap: true, + maxLines: null, + style: Theme + .of(context) + .textTheme + .bodyLarge + ?.copyWith(color: foregroundColor), + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + MessageStateIndicator(message: message, foregroundColor: foregroundColor,), + ], + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/widgets/messages/messages_list.dart b/lib/widgets/messages/messages_list.dart index 345834e..8a892d2 100644 --- a/lib/widgets/messages/messages_list.dart +++ b/lib/widgets/messages/messages_list.dart @@ -163,16 +163,13 @@ class _MessagesListState extends State { itemCount: cache.messages.length, itemBuilder: (context, index) { final entry = cache.messages[index]; - final widget = entry.senderId == apiClient.userId - ? MyMessageBubble(message: entry) - : OtherMessageBubble(message: entry); if (index == cache.messages.length - 1) { return Padding( padding: const EdgeInsets.only(top: 12), - child: widget, + child: MessageBubble(message: entry,), ); } - return widget; + return MessageBubble(message: entry,); }, ); }, diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 38dd0bc..075ecba 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,10 +6,14 @@ #include "generated_plugin_registrant.h" +#include #include #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) dynamic_color_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin"); + dynamic_color_plugin_register_with_registrar(dynamic_color_registrar); g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 65240e9..6fd458b 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + dynamic_color flutter_secure_storage_linux url_launcher_linux ) diff --git a/pubspec.lock b/pubspec.lock index 0337cfb..d6bb090 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -121,6 +121,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.8" + dynamic_color: + dependency: "direct main" + description: + name: dynamic_color + sha256: "74dff1435a695887ca64899b8990004f8d1232b0e84bfc4faa1fdda7c6f57cc1" + url: "https://pub.dev" + source: hosted + version: "1.6.5" fake_async: dependency: transitive description: @@ -767,4 +775,4 @@ packages: version: "6.3.0" sdks: dart: ">=3.0.0 <4.0.0" - flutter: ">=3.3.0" + flutter: ">=3.4.0-17.0.pre" diff --git a/pubspec.yaml b/pubspec.yaml index 58534f4..23d94dd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -55,6 +55,7 @@ dependencies: provider: ^6.0.5 photo_view: ^0.14.0 color: ^3.0.0 + dynamic_color: ^1.6.5 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 2048c45..7d8bb4d 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,10 +6,13 @@ #include "generated_plugin_registrant.h" +#include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + DynamicColorPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); FlutterSecureStorageWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index de626cc..2d0eeb9 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + dynamic_color flutter_secure_storage_windows url_launcher_windows )