Add support for material you and improve message design coherency

This commit is contained in:
Nutcake 2023-05-15 15:45:41 +02:00
parent 042bb5efbc
commit 2b7b4e2dba
17 changed files with 303 additions and 463 deletions

View file

@ -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/friends/friends_list.dart';
import 'package:contacts_plus_plus/widgets/login_screen.dart'; import 'package:contacts_plus_plus/widgets/login_screen.dart';
import 'package:contacts_plus_plus/widgets/update_notifier.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/material.dart';
import 'package:flutter_phoenix/flutter_phoenix.dart'; import 'package:flutter_phoenix/flutter_phoenix.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
@ -111,38 +112,40 @@ class _ContactsPlusPlusState extends State<ContactsPlusPlus> {
return ClientHolder( return ClientHolder(
settingsClient: widget.settingsClient, settingsClient: widget.settingsClient,
authenticationData: _authData, authenticationData: _authData,
child: MaterialApp( child: DynamicColorBuilder(
debugShowCheckedModeBanner: false, builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) => MaterialApp(
title: 'Contacts++', debugShowCheckedModeBanner: false,
theme: ThemeData( title: 'Contacts++',
theme: ThemeData(
useMaterial3: true, useMaterial3: true,
textTheme: _typography.white, textTheme: _typography.white,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.dark) 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 home: Builder( // Builder is necessary here since we need a context which has access to the ClientHolder
builder: (context) { builder: (context) {
showUpdateDialogOnFirstBuild(context); showUpdateDialogOnFirstBuild(context);
final clientHolder = ClientHolder.of(context); final clientHolder = ClientHolder.of(context);
return _authData.isAuthenticated ? return _authData.isAuthenticated ?
ChangeNotifierProvider( // This doesn't need to be a proxy provider since the arguments should never change during it's lifetime. ChangeNotifierProvider( // This doesn't need to be a proxy provider since the arguments should never change during it's lifetime.
create: (context) => create: (context) =>
MessagingClient( MessagingClient(
apiClient: clientHolder.apiClient, apiClient: clientHolder.apiClient,
notificationClient: clientHolder.notificationClient, notificationClient: clientHolder.notificationClient,
), ),
child: const FriendsList(), child: const FriendsList(),
) : ) :
LoginScreen( LoginScreen(
onLoginSuccessful: (AuthenticationData authData) async { onLoginSuccessful: (AuthenticationData authData) async {
if (authData.isAuthenticated) { if (authData.isAuthenticated) {
setState(() { setState(() {
_authData = authData; _authData = authData;
}); });
} }
}, },
); );
} }
) )
),
), ),
); );
} }

View file

@ -29,12 +29,10 @@ class _ExpandingInputFabState extends State<ExpandingInputFab> {
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
reverseDuration: const Duration(milliseconds: 200), reverseDuration: const Duration(milliseconds: 200),
curve: Curves.easeInOut, curve: Curves.easeInOut,
child: Container( child: Material(
decoration: BoxDecoration( elevation: 4,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(16),
color: Theme.of(context).colorScheme.secondaryContainer, color: Theme.of(context).colorScheme.primaryContainer,
),
padding: const EdgeInsets.all(4),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -46,10 +44,11 @@ class _ExpandingInputFabState extends State<ExpandingInputFab> {
onChanged: widget.onInputChanged, onChanged: widget.onInputChanged,
controller: _inputController, controller: _inputController,
decoration: const InputDecoration( decoration: const InputDecoration(
border: OutlineInputBorder( border: OutlineInputBorder(
borderSide: BorderSide.none, borderSide: BorderSide.none,
), ),
isDense: true isDense: true,
contentPadding: EdgeInsets.symmetric(vertical: 0, horizontal: 16),
), ),
) : null, ) : null,
), ),

View file

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:ffi';
import 'package:contacts_plus_plus/apis/user_api.dart'; import 'package:contacts_plus_plus/apis/user_api.dart';
import 'package:contacts_plus_plus/client_holder.dart'; import 'package:contacts_plus_plus/client_holder.dart';

View file

@ -2,22 +2,25 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class GenericAvatar extends StatelessWidget { 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 String imageUri;
final IconData placeholderIcon; final IconData placeholderIcon;
final double? radius; final double? radius;
final Color? foregroundColor;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return imageUri.isEmpty ? CircleAvatar( return imageUri.isEmpty ? CircleAvatar(
radius: radius, radius: radius,
foregroundColor: foregroundColor,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
child: Icon(placeholderIcon), child: Icon(placeholderIcon, color: foregroundColor,),
) : CachedNetworkImage( ) : CachedNetworkImage(
imageBuilder: (context, imageProvider) { imageBuilder: (context, imageProvider) {
return CircleAvatar( return CircleAvatar(
foregroundImage: imageProvider, foregroundImage: imageProvider,
foregroundColor: foregroundColor,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
radius: radius, radius: radius,
); );
@ -26,17 +29,19 @@ class GenericAvatar extends StatelessWidget {
placeholder: (context, url) { placeholder: (context, url) {
return CircleAvatar( return CircleAvatar(
backgroundColor: Colors.white54, backgroundColor: Colors.white54,
foregroundColor: foregroundColor,
radius: radius, radius: radius,
child: const Padding( child: Padding(
padding: EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: CircularProgressIndicator(color: Colors.black38, strokeWidth: 2), child: CircularProgressIndicator(color: foregroundColor, strokeWidth: 2),
), ),
); );
}, },
errorWidget: (context, error, what) => CircleAvatar( errorWidget: (context, error, what) => CircleAvatar(
radius: radius, radius: radius,
foregroundColor: foregroundColor,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
child: Icon(placeholderIcon), child: Icon(placeholderIcon, color: foregroundColor,),
), ),
); );
} }

View file

@ -3,21 +3,19 @@ import 'dart:convert';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:contacts_plus_plus/auxiliary.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/photo_asset.dart';
import 'package:contacts_plus_plus/models/message.dart'; import 'package:contacts_plus_plus/models/message.dart';
import 'package:contacts_plus_plus/string_formatter.dart'; import 'package:contacts_plus_plus/string_formatter.dart';
import 'package:contacts_plus_plus/widgets/formatted_text.dart'; import 'package:contacts_plus_plus/widgets/formatted_text.dart';
import 'package:contacts_plus_plus/widgets/messages/message_state_indicator.dart'; import 'package:contacts_plus_plus/widgets/messages/message_state_indicator.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:photo_view/photo_view.dart'; import 'package:photo_view/photo_view.dart';
class MessageAsset extends StatelessWidget { class MessageAsset extends StatelessWidget {
MessageAsset({required this.message, super.key}); const MessageAsset({required this.message, this.foregroundColor, super.key});
final Message message; final Message message;
final DateFormat _dateFormat = DateFormat.Hm(); final Color? foregroundColor;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -29,7 +27,6 @@ class MessageAsset extends StatelessWidget {
final formattedName = FormatNode.fromText(content["name"]); final formattedName = FormatNode.fromText(content["name"]);
return Container( return Container(
constraints: const BoxConstraints(maxWidth: 300), constraints: const BoxConstraints(maxWidth: 300),
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
child: Column( child: Column(
children: [ children: [
CachedNetworkImage( CachedNetworkImage(
@ -62,34 +59,20 @@ class MessageAsset extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Expanded( Expanded(
child: FormattedText( child: Padding(
formattedName, padding: const EdgeInsets.symmetric(horizontal: 4.0),
maxLines: null, child: FormattedText(
style: Theme formattedName,
.of(context) maxLines: null,
.textTheme style: Theme
.bodySmall .of(context)
?.copyWith(color: Colors.white60), .textTheme
.bodySmall
?.copyWith(color: foregroundColor),
),
), ),
), ),
Padding( MessageStateIndicator(message: message, foregroundColor: foregroundColor,),
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),
),
], ],
), ),
], ],

View file

@ -1,18 +1,18 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io' show Platform; 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/auxiliary.dart';
import 'package:contacts_plus_plus/models/message.dart'; import 'package:contacts_plus_plus/models/message.dart';
import 'package:contacts_plus_plus/widgets/messages/message_state_indicator.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:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:just_audio/just_audio.dart'; import 'package:just_audio/just_audio.dart';
class MessageAudioPlayer extends StatefulWidget { class MessageAudioPlayer extends StatefulWidget {
const MessageAudioPlayer({required this.message, super.key}); const MessageAudioPlayer({required this.message, this.foregroundColor, super.key});
final Message message; final Message message;
final Color? foregroundColor;
@override @override
State<MessageAudioPlayer> createState() => _MessageAudioPlayerState(); State<MessageAudioPlayer> createState() => _MessageAudioPlayerState();
@ -20,7 +20,6 @@ class MessageAudioPlayer extends StatefulWidget {
class _MessageAudioPlayerState extends State<MessageAudioPlayer> { class _MessageAudioPlayerState extends State<MessageAudioPlayer> {
final AudioPlayer _audioPlayer = AudioPlayer(); final AudioPlayer _audioPlayer = AudioPlayer();
final DateFormat _dateFormat = DateFormat.Hm();
double _sliderValue = 0; double _sliderValue = 0;
@override @override
@ -75,9 +74,12 @@ class _MessageAudioPlayerState extends State<MessageAudioPlayer> {
if (snapshot.hasData) { if (snapshot.hasData) {
final playerState = snapshot.data as PlayerState; final playerState = snapshot.data as PlayerState;
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
IconButton( IconButton(
onPressed: () { onPressed: () {
@ -87,7 +89,11 @@ class _MessageAudioPlayerState extends State<MessageAudioPlayer> {
case ProcessingState.buffering: case ProcessingState.buffering:
break; break;
case ProcessingState.ready: case ProcessingState.ready:
_audioPlayer.play(); if (playerState.playing) {
_audioPlayer.pause();
} else {
_audioPlayer.play();
}
break; break;
case ProcessingState.completed: case ProcessingState.completed:
_audioPlayer.seek(Duration.zero); _audioPlayer.seek(Duration.zero);
@ -95,6 +101,7 @@ class _MessageAudioPlayerState extends State<MessageAudioPlayer> {
break; break;
} }
}, },
color: widget.foregroundColor,
icon: SizedBox( icon: SizedBox(
width: 24, width: 24,
height: 24, height: 24,
@ -110,21 +117,27 @@ class _MessageAudioPlayerState extends State<MessageAudioPlayer> {
builder: (context, snapshot) { builder: (context, snapshot) {
_sliderValue = (_audioPlayer.position.inMilliseconds / _sliderValue = (_audioPlayer.position.inMilliseconds /
(_audioPlayer.duration?.inMilliseconds ?? 0)).clamp(0, 1); (_audioPlayer.duration?.inMilliseconds ?? 0)).clamp(0, 1);
return StatefulBuilder( return StatefulBuilder( // Not sure if this makes sense here...
builder: (context, setState) { builder: (context, setState) {
return Slider( return SliderTheme(
value: _sliderValue, data: SliderThemeData(
min: 0.0, inactiveTrackColor: widget.foregroundColor?.withAlpha(100),
max: 1.0, ),
onChanged: (value) async { child: Slider(
_audioPlayer.pause(); thumbColor: widget.foregroundColor,
setState(() { value: _sliderValue,
_sliderValue = value; min: 0.0,
}); max: 1.0,
_audioPlayer.seek(Duration( onChanged: (value) async {
milliseconds: (value * (_audioPlayer.duration?.inMilliseconds ?? 0)).round(), _audioPlayer.pause();
)); setState(() {
}, _sliderValue = value;
});
_audioPlayer.seek(Duration(
milliseconds: (value * (_audioPlayer.duration?.inMilliseconds ?? 0)).round(),
));
},
),
); );
} }
); );
@ -134,36 +147,24 @@ class _MessageAudioPlayerState extends State<MessageAudioPlayer> {
), ),
Row( Row(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
const SizedBox(width: 12), const SizedBox(width: 4,),
StreamBuilder( StreamBuilder(
stream: _audioPlayer.positionStream, stream: _audioPlayer.positionStream,
builder: (context, snapshot) { builder: (context, snapshot) {
return Text("${snapshot.data?.format() ?? "??"}/${_audioPlayer.duration?.format() ?? return Text("${snapshot.data?.format() ?? "??"}/${_audioPlayer.duration?.format() ??
"??"}"); "??"}",
style: Theme
.of(context)
.textTheme
.bodySmall
?.copyWith(color: widget.foregroundColor?.withAlpha(150)),
);
} }
), ),
const Spacer(), const Spacer(),
Padding( MessageStateIndicator(message: widget.message, foregroundColor: widget.foregroundColor,),
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),
),
], ],
) )
], ],

View file

@ -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/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_asset.dart';
import 'package:contacts_plus_plus/widgets/messages/message_audio_player.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_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: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 class MessageBubble extends StatelessWidget {
// subwidgets with a lot of room for deduplication. Should probably redo this some day. const MessageBubble({required this.message, super.key});
class MyMessageBubble extends StatelessWidget {
MyMessageBubble({required this.message, super.key});
final Message message; final Message message;
final DateFormat _dateFormat = DateFormat.Hm();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
switch (message.type) { final bool mine = message.senderId == ClientHolder.of(context).apiClient.userId;
case MessageType.sessionInvite: final colorScheme = Theme.of(context).colorScheme;
return Row( final foregroundColor = mine ? colorScheme.onPrimaryContainer : colorScheme.onSurfaceVariant;
mainAxisAlignment: MainAxisAlignment.end, final backgroundColor = mine ? colorScheme.primaryContainer : colorScheme.surfaceVariant;
mainAxisSize: MainAxisSize.min, return Padding(
children: [ padding: EdgeInsets.only(left: mine ? 32 : 12, bottom: 16, right: mine ? 12 : 32),
Card( child: Row(
shape: RoundedRectangleBorder( mainAxisAlignment: mine ? MainAxisAlignment.end : MainAxisAlignment.start,
borderRadius: BorderRadius.circular(16), children: [
), Material(
color: Theme borderRadius: BorderRadius.circular(16),
.of(context) color: backgroundColor,
.colorScheme textStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(color: foregroundColor),
.primaryContainer, child: Container(
margin: const EdgeInsets.only(left: 32, bottom: 16, right: 12), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
child: Padding( child: switch (message.type) {
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 12), MessageType.sessionInvite =>
child: MessageSessionInvite(message: message,), 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,),
),
),
],
);
}
} }
} }

View file

@ -1,6 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'package:contacts_plus_plus/client_holder.dart';
import 'package:contacts_plus_plus/auxiliary.dart'; import 'package:contacts_plus_plus/auxiliary.dart';
import 'package:contacts_plus_plus/models/message.dart'; import 'package:contacts_plus_plus/models/message.dart';
import 'package:contacts_plus_plus/models/session.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/messages_session_header.dart';
import 'package:contacts_plus_plus/widgets/messages/message_state_indicator.dart'; import 'package:contacts_plus_plus/widgets/messages/message_state_indicator.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
class MessageSessionInvite extends StatelessWidget { class MessageSessionInvite extends StatelessWidget {
MessageSessionInvite({required this.message, super.key}); const MessageSessionInvite({required this.message, this.foregroundColor, super.key});
final DateFormat _dateFormat = DateFormat.Hm();
final Color? foregroundColor;
final Message message; final Message message;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final sessionInfo = Session.fromMap(jsonDecode(message.content)); final sessionInfo = Session.fromMap(jsonDecode(message.content));
return TextButton( return Container(
onPressed: () { constraints: const BoxConstraints(maxWidth: 300),
showDialog(context: context, builder: (context) => SessionPopup(session: sessionInfo)); child: TextButton(
}, onPressed: () {
style: TextButton.styleFrom(padding: EdgeInsets.zero), showDialog(context: context, builder: (context) => SessionPopup(session: sessionInfo));
child: Container( },
padding: const EdgeInsets.only(left: 10), style: TextButton.styleFrom(padding: EdgeInsets.zero),
constraints: const BoxConstraints(maxWidth: 300), child: Container(
child: Column( padding: const EdgeInsets.only(left: 4),
mainAxisAlignment: MainAxisAlignment.spaceBetween, child: Column(
mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ mainAxisSize: MainAxisSize.min,
Row( crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, children: [
crossAxisAlignment: CrossAxisAlignment.start, Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisSize: MainAxisSize.max,
children: [ crossAxisAlignment: CrossAxisAlignment.start,
Expanded( mainAxisAlignment: MainAxisAlignment.start,
child: Padding( children: [
padding: const EdgeInsets.only(top: 4), Padding(
child: FormattedText(sessionInfo.formattedName, maxLines: null, softWrap: true, style: Theme.of(context).textTheme.titleMedium,), 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),)
],
),
), ),
), Expanded(
Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0), padding: const EdgeInsets.all(8),
child: Column( child: FormattedText(sessionInfo.formattedName, maxLines: null, softWrap: true, textAlign: TextAlign.start, style: Theme
mainAxisSize: MainAxisSize.max, .of(context)
mainAxisAlignment: MainAxisAlignment.center, .textTheme
children: [ .titleMedium?.copyWith(color: foregroundColor),),
GenericAvatar( ),
imageUri: Aux.neosDbToHttp(Aux.neosDbToHttp(sessionInfo.thumbnail)),
placeholderIcon: Icons.no_photography,
),
const SizedBox(height: 4,),
Text("${sessionInfo.sessionUsers.length}/${sessionInfo.maxUsers}")
],
), ),
), ],
], ),
), const SizedBox(height: 8,),
const SizedBox(height: 8,), Row(
Row( mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Expanded(child: Text("Hosted by ${sessionInfo.hostUsername}", overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.white60),)), Text("Hosted by ${sessionInfo.hostUsername}", overflow: TextOverflow.ellipsis, style: Theme
Padding( .of(context)
padding: const EdgeInsets.symmetric(horizontal: 4.0), .textTheme
child: Text( .bodySmall
_dateFormat.format(message.sendTime.toLocal()), ?.copyWith(color: foregroundColor?.withAlpha(150)),),
style: Theme MessageStateIndicator(message: message, foregroundColor: foregroundColor,),
.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),
),
],
)
],
), ),
), ),
); );
} }
} }

View file

@ -1,29 +1,45 @@
import 'package:contacts_plus_plus/client_holder.dart';
import 'package:contacts_plus_plus/models/message.dart'; import 'package:contacts_plus_plus/models/message.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
class MessageStateIndicator extends StatelessWidget { 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
late final IconData icon; final color = foregroundColor?.withAlpha(150);
switch (messageState) { return Row(
case MessageState.local: children: [
icon = Icons.alarm; Padding(
break; padding: const EdgeInsets.symmetric(horizontal: 4.0),
case MessageState.sent: child: Text(
icon = Icons.done; _dateFormat.format(message.sendTime.toLocal()),
break; style: Theme
case MessageState.read: .of(context)
icon = Icons.done_all; .textTheme
break; .labelMedium
} ?.copyWith(color: color),
return Icon( ),
icon, ),
size: 12, 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,
),
],
); );
} }
} }

View file

@ -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,),
],
),
],
);
}
}

View file

@ -163,16 +163,13 @@ class _MessagesListState extends State<MessagesList> {
itemCount: cache.messages.length, itemCount: cache.messages.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final entry = cache.messages[index]; final entry = cache.messages[index];
final widget = entry.senderId == apiClient.userId
? MyMessageBubble(message: entry)
: OtherMessageBubble(message: entry);
if (index == cache.messages.length - 1) { if (index == cache.messages.length - 1) {
return Padding( return Padding(
padding: const EdgeInsets.only(top: 12), padding: const EdgeInsets.only(top: 12),
child: widget, child: MessageBubble(message: entry,),
); );
} }
return widget; return MessageBubble(message: entry,);
}, },
); );
}, },

View file

@ -6,10 +6,14 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <dynamic_color/dynamic_color_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h> #include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h> #include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { 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 = g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);

View file

@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
dynamic_color
flutter_secure_storage_linux flutter_secure_storage_linux
url_launcher_linux url_launcher_linux
) )

View file

@ -121,6 +121,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.8" 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: fake_async:
dependency: transitive dependency: transitive
description: description:
@ -767,4 +775,4 @@ packages:
version: "6.3.0" version: "6.3.0"
sdks: sdks:
dart: ">=3.0.0 <4.0.0" dart: ">=3.0.0 <4.0.0"
flutter: ">=3.3.0" flutter: ">=3.4.0-17.0.pre"

View file

@ -55,6 +55,7 @@ dependencies:
provider: ^6.0.5 provider: ^6.0.5
photo_view: ^0.14.0 photo_view: ^0.14.0
color: ^3.0.0 color: ^3.0.0
dynamic_color: ^1.6.5
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View file

@ -6,10 +6,13 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <dynamic_color/dynamic_color_plugin_c_api.h>
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h> #include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h> #include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
DynamicColorPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("DynamicColorPluginCApi"));
FlutterSecureStorageWindowsPluginRegisterWithRegistrar( FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
UrlLauncherWindowsRegisterWithRegistrar( UrlLauncherWindowsRegisterWithRegistrar(

View file

@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
dynamic_color
flutter_secure_storage_windows flutter_secure_storage_windows
url_launcher_windows url_launcher_windows
) )