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/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,13 +112,14 @@ class _ContactsPlusPlusState extends State<ContactsPlusPlus> {
return ClientHolder(
settingsClient: widget.settingsClient,
authenticationData: _authData,
child: MaterialApp(
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)
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) {
@ -144,6 +146,7 @@ class _ContactsPlusPlusState extends State<ContactsPlusPlus> {
}
)
),
),
);
}
}

View file

@ -29,12 +29,10 @@ class _ExpandingInputFabState extends State<ExpandingInputFab> {
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,
@ -49,7 +47,8 @@ class _ExpandingInputFabState extends State<ExpandingInputFab> {
border: OutlineInputBorder(
borderSide: BorderSide.none,
),
isDense: true
isDense: true,
contentPadding: EdgeInsets.symmetric(vertical: 0, horizontal: 16),
),
) : null,
),

View file

@ -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';

View file

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

View file

@ -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,6 +59,8 @@ class MessageAsset extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: FormattedText(
formattedName,
maxLines: null,
@ -69,27 +68,11 @@ class MessageAsset extends StatelessWidget {
.of(context)
.textTheme
.bodySmall
?.copyWith(color: Colors.white60),
?.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,),
],
),
],

View file

@ -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<MessageAudioPlayer> createState() => _MessageAudioPlayerState();
@ -20,7 +20,6 @@ class MessageAudioPlayer extends StatefulWidget {
class _MessageAudioPlayerState extends State<MessageAudioPlayer> {
final AudioPlayer _audioPlayer = AudioPlayer();
final DateFormat _dateFormat = DateFormat.Hm();
double _sliderValue = 0;
@override
@ -75,9 +74,12 @@ class _MessageAudioPlayerState extends State<MessageAudioPlayer> {
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<MessageAudioPlayer> {
case ProcessingState.buffering:
break;
case ProcessingState.ready:
if (playerState.playing) {
_audioPlayer.pause();
} else {
_audioPlayer.play();
}
break;
case ProcessingState.completed:
_audioPlayer.seek(Duration.zero);
@ -95,6 +101,7 @@ class _MessageAudioPlayerState extends State<MessageAudioPlayer> {
break;
}
},
color: widget.foregroundColor,
icon: SizedBox(
width: 24,
height: 24,
@ -110,9 +117,14 @@ class _MessageAudioPlayerState extends State<MessageAudioPlayer> {
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(
return SliderTheme(
data: SliderThemeData(
inactiveTrackColor: widget.foregroundColor?.withAlpha(100),
),
child: Slider(
thumbColor: widget.foregroundColor,
value: _sliderValue,
min: 0.0,
max: 1.0,
@ -125,6 +137,7 @@ class _MessageAudioPlayerState extends State<MessageAudioPlayer> {
milliseconds: (value * (_audioPlayer.duration?.inMilliseconds ?? 0)).round(),
));
},
),
);
}
);
@ -134,36 +147,24 @@ class _MessageAudioPlayerState extends State<MessageAudioPlayer> {
),
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() ??
"??"}");
}
),
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),
.bodySmall
?.copyWith(color: widget.foregroundColor?.withAlpha(150)),
);
}
),
const Spacer(),
MessageStateIndicator(message: widget.message, foregroundColor: widget.foregroundColor,),
],
)
],

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/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,
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: [
Card(
shape: RoundedRectangleBorder(
Material(
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,),
),
),
],
);
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),
color: backgroundColor,
textStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(color: foregroundColor),
child: Container(
constraints: const BoxConstraints(maxWidth: 300),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
child: MessageAsset(message: message,),
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.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 '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,41 +8,37 @@ 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(
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: 10),
constraints: const BoxConstraints(maxWidth: 300),
padding: const EdgeInsets.only(left: 4),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.only(top: 4),
child: FormattedText(sessionInfo.formattedName, maxLines: null, softWrap: true, style: Theme.of(context).textTheme.titleMedium,),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
@ -51,41 +46,45 @@ class MessageSessionInvite extends StatelessWidget {
GenericAvatar(
imageUri: Aux.neosDbToHttp(Aux.neosDbToHttp(sessionInfo.thumbnail)),
placeholderIcon: Icons.no_photography,
foregroundColor: foregroundColor,
),
const SizedBox(height: 4,),
Text("${sessionInfo.sessionUsers.length}/${sessionInfo.maxUsers}")
Text("${sessionInfo.sessionUsers.length}/${sessionInfo.maxUsers}", style: Theme
.of(context)
.textTheme
.bodyMedium
?.copyWith(color: foregroundColor),)
],
),
),
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(
mainAxisSize: MainAxisSize.max,
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
Text("Hosted by ${sessionInfo.hostUsername}", overflow: TextOverflow.ellipsis, 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),
),
.bodySmall
?.copyWith(color: foregroundColor?.withAlpha(150)),),
MessageStateIndicator(message: message, foregroundColor: foregroundColor,),
],
)
],
),
),
),
);
}
}

View file

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

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

View file

@ -6,10 +6,14 @@
#include "generated_plugin_registrant.h"
#include <dynamic_color/dynamic_color_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
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);

View file

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

View file

@ -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"

View file

@ -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:

View file

@ -6,10 +6,13 @@
#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 <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
DynamicColorPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("DynamicColorPluginCApi"));
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
UrlLauncherWindowsRegisterWithRegistrar(

View file

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