Add support for material you and improve message design coherency
This commit is contained in:
parent
042bb5efbc
commit
2b7b4e2dba
17 changed files with 303 additions and 463 deletions
|
@ -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> {
|
|||
}
|
||||
)
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
|
|
@ -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,),
|
||||
],
|
||||
)
|
||||
],
|
||||
|
|
|
@ -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,),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
41
lib/widgets/messages/message_text.dart
Normal file
41
lib/widgets/messages/message_text.dart
Normal 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,),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
dynamic_color
|
||||
flutter_secure_storage_linux
|
||||
url_launcher_linux
|
||||
)
|
||||
|
|
10
pubspec.lock
10
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"
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
dynamic_color
|
||||
flutter_secure_storage_windows
|
||||
url_launcher_windows
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue