pull from upstream
This commit is contained in:
parent
1dae348415
commit
b4fa4a7208
|
|
@ -2,6 +2,10 @@
|
|||
"@@locale": "en",
|
||||
"@@last_modified": "2025-06-05 12:38:37.885451",
|
||||
"noSendPermission": "You can't send messages here",
|
||||
"noMessagesYet": "No messages yet",
|
||||
"longPressToRecordVoiceMessage": "Long press to record voice message.",
|
||||
"pause": "Pause",
|
||||
"resume": "Resume",
|
||||
"@noSendPermission": {},
|
||||
"alwaysUse24HourFormat": "false",
|
||||
"@alwaysUse24HourFormat": {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@
|
|||
"@@last_modified": "2021-08-14 12:41:09.903021",
|
||||
"noSendPermission": "Вы не можете отправлять сообщения",
|
||||
"@noSendPermission": {},
|
||||
"noMessagesYet": "Нет сообщений",
|
||||
"longPressToRecordVoiceMessage": "Зажмите, чтобы записать голосовое сообщение.",
|
||||
"pause": "Пауза",
|
||||
"resume": "Продолжить",
|
||||
"alwaysUse24HourFormat": "нет",
|
||||
"@alwaysUse24HourFormat": {
|
||||
"description": "Set to true to always display time of day in 24 hour format."
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ abstract class FluffyThemes {
|
|||
BuildContext context,
|
||||
Brightness brightness, [
|
||||
Color? seed,
|
||||
bool? pureBlack,
|
||||
bool? pureBlack,
|
||||
]) {
|
||||
final extraDarkColors = (brightness == Brightness.dark && pureBlack == true)
|
||||
? {
|
||||
|
|
@ -118,6 +118,8 @@ abstract class FluffyThemes {
|
|||
isColumnMode ? colorScheme.surfaceContainer.withAlpha(128) : null,
|
||||
surfaceTintColor: isColumnMode ? colorScheme.surface : null,
|
||||
backgroundColor: isColumnMode ? colorScheme.surface : null,
|
||||
actionsPadding:
|
||||
isColumnMode ? const EdgeInsets.symmetric(horizontal: 16.0) : null,
|
||||
systemOverlayStyle: SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: brightness.reversed,
|
||||
|
|
@ -140,6 +142,7 @@ abstract class FluffyThemes {
|
|||
),
|
||||
snackBarTheme: isColumnMode
|
||||
? const SnackBarThemeData(
|
||||
showCloseIcon: true,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
width: FluffyThemes.columnWidth * 1.5,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -199,6 +199,30 @@ abstract class L10n {
|
|||
/// **'You can\'t send messages here'**
|
||||
String get noSendPermission;
|
||||
|
||||
/// No description provided for @noMessagesYet.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No messages yet'**
|
||||
String get noMessagesYet;
|
||||
|
||||
/// No description provided for @longPressToRecordVoiceMessage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Long press to record voice message.'**
|
||||
String get longPressToRecordVoiceMessage;
|
||||
|
||||
/// No description provided for @pause.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Pause'**
|
||||
String get pause;
|
||||
|
||||
/// No description provided for @resume.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Resume'**
|
||||
String get resume;
|
||||
|
||||
/// Set to true to always display time of day in 24 hour format.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nAr extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'false';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nBe extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'false';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nBn extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'false';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nBo extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'false';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nCa extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'true';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nCs extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'Vypnuto';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nDe extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'true';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nEl extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'false';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nEn extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'false';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nEo extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'false';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nEs extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'falso';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nEt extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'false';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nEu extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'false';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nFa extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'false';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nFi extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'false';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nFil extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'false';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nFr extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'true';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nGa extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'bréagach';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nGl extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'falso';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nHe extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'false';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nHi extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'false';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nHr extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'true';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nHu extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'true';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nIa extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'false';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nId extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'tidak';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nIe extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'false';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nIt extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'disattivato';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nJa extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'false';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nKa extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'false';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nKo extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'false';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nLt extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'false';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nLv extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'nē';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nNb extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'false';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nNl extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'true';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nPl extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'false';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nPt extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'false';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nRo extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'false';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nRu extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'Вы не можете отправлять сообщения';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'Нет сообщений';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Зажмите, чтобы записать голосовое сообщение.';
|
||||
|
||||
@override
|
||||
String get pause => 'Пауза';
|
||||
|
||||
@override
|
||||
String get resume => 'Продолжить';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'нет';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nSk extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'false';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nSl extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'false';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nSr extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'false';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nSv extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'false';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nTa extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'தவறு';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nTe extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'తప్పుడు';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nTh extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'false';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nTr extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'false';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nUk extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'ні';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nVi extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'Không';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class L10nZh extends L10n {
|
|||
@override
|
||||
String get noSendPermission => 'You can\'t send messages here';
|
||||
|
||||
@override
|
||||
String get noMessagesYet => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get longPressToRecordVoiceMessage =>
|
||||
'Long press to record voice message.';
|
||||
|
||||
@override
|
||||
String get pause => 'Pause';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@override
|
||||
String get alwaysUse24HourFormat => 'false';
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import 'dart:io';
|
|||
|
||||
import 'package:extera_next/pages/chat/recovered_event_dialog.dart';
|
||||
import 'package:extera_next/pages/chat/translated_event_dialog.dart';
|
||||
import 'package:extera_next/utils/file_description.dart';
|
||||
import 'package:extera_next/utils/matrix_sdk_extensions/synapse_admin_extension.dart';
|
||||
import 'package:extera_next/utils/translator.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
|
@ -18,7 +17,6 @@ import 'package:extera_next/generated/l10n/l10n.dart';
|
|||
import 'package:go_router/go_router.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:record/record.dart';
|
||||
import 'package:scroll_to_index/scroll_to_index.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:universal_html/html.dart' as html;
|
||||
|
|
@ -28,7 +26,6 @@ import 'package:extera_next/config/setting_keys.dart';
|
|||
import 'package:extera_next/config/themes.dart';
|
||||
import 'package:extera_next/pages/chat/chat_view.dart';
|
||||
import 'package:extera_next/pages/chat/event_info_dialog.dart';
|
||||
import 'package:extera_next/pages/chat/recording_dialog.dart';
|
||||
import 'package:extera_next/pages/chat_details/chat_details.dart';
|
||||
import 'package:extera_next/utils/error_reporter.dart';
|
||||
import 'package:extera_next/utils/file_selector.dart';
|
||||
|
|
@ -619,45 +616,39 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
);
|
||||
}
|
||||
|
||||
void voiceMessageAction() async {
|
||||
Future<void> onVoiceMessageSend(
|
||||
String path,
|
||||
int duration,
|
||||
List<int> waveform,
|
||||
String? fileName,
|
||||
) async {
|
||||
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
||||
if (PlatformInfos.isAndroid) {
|
||||
final info = await DeviceInfoPlugin().androidInfo;
|
||||
if (info.version.sdkInt < 19) {
|
||||
showOkAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).unsupportedAndroidVersion,
|
||||
message: L10n.of(context).unsupportedAndroidVersionLong,
|
||||
okLabel: L10n.of(context).close,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
final audioFile = XFile(path);
|
||||
|
||||
if (await AudioRecorder().hasPermission() == false) return;
|
||||
final result = await showDialog<RecordingResult>(
|
||||
final bytesResult = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (c) => const RecordingDialog(),
|
||||
future: audioFile.readAsBytes,
|
||||
);
|
||||
if (result == null) return;
|
||||
final audioFile = XFile(result.path);
|
||||
final bytes = bytesResult.result;
|
||||
if (bytes == null) return;
|
||||
|
||||
final file = MatrixAudioFile(
|
||||
bytes: await audioFile.readAsBytes(),
|
||||
name: result.fileName ?? audioFile.path,
|
||||
bytes: bytes,
|
||||
name: fileName ?? audioFile.path,
|
||||
);
|
||||
|
||||
await room.sendFileEvent(
|
||||
file,
|
||||
inReplyTo: replyEvent,
|
||||
extraContent: {
|
||||
'info': {
|
||||
...file.info,
|
||||
'duration': result.duration,
|
||||
'duration': duration,
|
||||
},
|
||||
'org.matrix.msc3245.voice': {},
|
||||
'org.matrix.msc1767.audio': {
|
||||
'duration': result.duration,
|
||||
'waveform': result.waveform,
|
||||
'duration': duration,
|
||||
'waveform': waveform,
|
||||
},
|
||||
},
|
||||
).catchError((e) {
|
||||
|
|
@ -737,8 +728,11 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
return;
|
||||
}
|
||||
final event = selectedEvents.single;
|
||||
await mx.client.reportEvent(roomId, event.eventId,
|
||||
reason: "Extera (Next) Redacted Event Recover");
|
||||
await mx.client.reportEvent(
|
||||
roomId,
|
||||
event.eventId,
|
||||
reason: "Extera (Next) Redacted Event Recover",
|
||||
);
|
||||
|
||||
final reports = await mx.client.getEventReports();
|
||||
final report = reports.firstWhere(
|
||||
|
|
@ -753,11 +747,14 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
}
|
||||
|
||||
Navigator.of(context).push(new MaterialPageRoute(
|
||||
builder: (BuildContext ctx) {
|
||||
return RecoveredEventDialog(
|
||||
event: recoveredEvent!, timeline: timeline!);
|
||||
},
|
||||
fullscreenDialog: true));
|
||||
builder: (BuildContext ctx) {
|
||||
return RecoveredEventDialog(
|
||||
event: recoveredEvent,
|
||||
timeline: timeline!,
|
||||
);
|
||||
},
|
||||
fullscreenDialog: true,
|
||||
));
|
||||
}
|
||||
|
||||
void translateEventAction() async {
|
||||
|
|
@ -769,12 +766,6 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
}
|
||||
final event = selectedEvents.single;
|
||||
var text = event.isRichMessage ? event.formattedText : event.text;
|
||||
if (text == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(L10n.of(context).errorTranslatingMessage)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
var content = {...event.content};
|
||||
try {
|
||||
text = await Translator.translate(
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:extera_next/generated/l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:extera_next/config/app_config.dart';
|
||||
import 'package:extera_next/utils/other_party_can_receive.dart';
|
||||
import 'package:extera_next/generated/l10n/l10n.dart';
|
||||
import 'package:extera_next/pages/chat/recording_input_row.dart';
|
||||
import 'package:extera_next/pages/chat/recording_view_model.dart';
|
||||
import 'package:extera_next/utils/platform_infos.dart';
|
||||
import 'package:extera_next/widgets/avatar.dart';
|
||||
import 'package:extera_next/widgets/matrix.dart';
|
||||
|
|
@ -21,325 +22,331 @@ class ChatInputRow extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
if (controller.showEmojiPicker &&
|
||||
controller.emojiPickerType == EmojiPickerType.reaction) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
const height = 48.0;
|
||||
|
||||
if (!controller.room.canSendDefaultMessages) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Text(
|
||||
L10n.of(context).noSendPermission,
|
||||
style: theme.textTheme.bodySmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
final selectedTextButtonStyle = TextButton.styleFrom(
|
||||
foregroundColor: theme.colorScheme.onTertiaryContainer,
|
||||
);
|
||||
|
||||
// if (!controller.room.otherPartyCanReceiveMessages) {
|
||||
// return Center(
|
||||
// child: Padding(
|
||||
// padding: const EdgeInsets.all(12.0),
|
||||
// child: Text(
|
||||
// L10n.of(context).otherPartyNotLoggedIn,
|
||||
// style: theme.textTheme.bodySmall,
|
||||
// textAlign: TextAlign.center,
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
|
||||
|
||||
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: controller.selectMode
|
||||
? <Widget>[
|
||||
if (controller.selectedEvents
|
||||
.every((event) => event.status == EventStatus.error))
|
||||
SizedBox(
|
||||
height: height,
|
||||
child: TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: theme.colorScheme.error,
|
||||
return RecordingViewModel(
|
||||
builder: (context, recordingViewModel) {
|
||||
if (recordingViewModel.isRecording) {
|
||||
return RecordingInputRow(
|
||||
state: recordingViewModel,
|
||||
onSend: controller.onVoiceMessageSend,
|
||||
);
|
||||
}
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: controller.selectMode
|
||||
? <Widget>[
|
||||
if (controller.selectedEvents
|
||||
.every((event) => event.status == EventStatus.error))
|
||||
SizedBox(
|
||||
height: height,
|
||||
child: TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: theme.colorScheme.error,
|
||||
),
|
||||
onPressed: controller.deleteErrorEventsAction,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
const Icon(Icons.delete_forever_outlined),
|
||||
Text(L10n.of(context).delete),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
SizedBox(
|
||||
height: height,
|
||||
child: TextButton(
|
||||
style: selectedTextButtonStyle,
|
||||
onPressed: controller.forwardEventsAction,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
const Icon(Icons.keyboard_arrow_left_outlined),
|
||||
Text(L10n.of(context).forward),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
onPressed: controller.deleteErrorEventsAction,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
const Icon(Icons.delete),
|
||||
Text(L10n.of(context).delete),
|
||||
controller.selectedEvents.length == 1
|
||||
? controller.selectedEvents.first
|
||||
.getDisplayEvent(controller.timeline!)
|
||||
.status
|
||||
.isSent
|
||||
? SizedBox(
|
||||
height: height,
|
||||
child: TextButton(
|
||||
style: selectedTextButtonStyle,
|
||||
onPressed: controller.replyAction,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Text(L10n.of(context).reply),
|
||||
const Icon(Icons.keyboard_arrow_right),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: SizedBox(
|
||||
height: height,
|
||||
child: TextButton(
|
||||
style: selectedTextButtonStyle,
|
||||
onPressed: controller.sendAgainAction,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Text(L10n.of(context).tryToSendAgain),
|
||||
const SizedBox(width: 4),
|
||||
const Icon(Icons.send_outlined, size: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
]
|
||||
: <Widget>[
|
||||
const SizedBox(width: 4),
|
||||
AnimatedContainer(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
width:
|
||||
controller.sendController.text.isNotEmpty ? 0 : height,
|
||||
height: height,
|
||||
alignment: Alignment.center,
|
||||
decoration: const BoxDecoration(),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: PopupMenuButton<String>(
|
||||
useRootNavigator: true,
|
||||
icon: const Icon(Icons.add_circle_outline),
|
||||
iconColor: theme.colorScheme.onPrimaryContainer,
|
||||
onSelected: controller.onAddPopupMenuButtonSelected,
|
||||
itemBuilder: (BuildContext context) =>
|
||||
<PopupMenuEntry<String>>[
|
||||
if (PlatformInfos.isMobile)
|
||||
PopupMenuItem<String>(
|
||||
value: 'location',
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
theme.colorScheme.onPrimaryContainer,
|
||||
foregroundColor:
|
||||
theme.colorScheme.primaryContainer,
|
||||
child: const Icon(Icons.gps_fixed_outlined),
|
||||
),
|
||||
title: Text(L10n.of(context).shareLocation),
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
),
|
||||
),
|
||||
// PopupMenuItem<String>(
|
||||
// value: 'image',
|
||||
// child: ListTile(
|
||||
// leading: CircleAvatar(
|
||||
// backgroundColor:
|
||||
// theme.colorScheme.onPrimaryContainer,
|
||||
// foregroundColor:
|
||||
// theme.colorScheme.primaryContainer,
|
||||
// child: const Icon(Icons.photo_outlined),
|
||||
// ),
|
||||
// title: Text(L10n.of(context).sendImage),
|
||||
// contentPadding: const EdgeInsets.all(0),
|
||||
// ),
|
||||
// ),
|
||||
// PopupMenuItem<String>(
|
||||
// value: 'video',
|
||||
// child: ListTile(
|
||||
// leading: CircleAvatar(
|
||||
// backgroundColor:
|
||||
// theme.colorScheme.onPrimaryContainer,
|
||||
// foregroundColor:
|
||||
// theme.colorScheme.primaryContainer,
|
||||
// child:
|
||||
// const Icon(Icons.video_camera_back_outlined),
|
||||
// ),
|
||||
// title: Text(L10n.of(context).sendVideo),
|
||||
// contentPadding: const EdgeInsets.all(0),
|
||||
// ),
|
||||
// ),
|
||||
PopupMenuItem<String>(
|
||||
value: 'file',
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
theme.colorScheme.onPrimaryContainer,
|
||||
foregroundColor:
|
||||
theme.colorScheme.primaryContainer,
|
||||
child: const Icon(Icons.attachment_outlined),
|
||||
),
|
||||
title: Text(L10n.of(context).sendFile),
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
SizedBox(
|
||||
height: height,
|
||||
child: TextButton(
|
||||
onPressed: controller.forwardEventsAction,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
const Icon(Icons.keyboard_arrow_left_outlined),
|
||||
Text(L10n.of(context).forward),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
controller.selectedEvents.length == 1
|
||||
? controller.selectedEvents.first
|
||||
.getDisplayEvent(controller.timeline!)
|
||||
.status
|
||||
.isSent
|
||||
? SizedBox(
|
||||
height: height,
|
||||
child: TextButton(
|
||||
onPressed: controller.replyAction,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Text(L10n.of(context).reply),
|
||||
const Icon(Icons.keyboard_arrow_right),
|
||||
],
|
||||
if (PlatformInfos.isMobile)
|
||||
AnimatedContainer(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
width: controller.sendController.text.isNotEmpty
|
||||
? 0
|
||||
: height,
|
||||
height: height,
|
||||
alignment: Alignment.center,
|
||||
decoration: const BoxDecoration(),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: PopupMenuButton(
|
||||
useRootNavigator: true,
|
||||
icon: const Icon(Icons.camera_alt_outlined),
|
||||
onSelected: controller.onAddPopupMenuButtonSelected,
|
||||
iconColor: theme.colorScheme.onPrimaryContainer,
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem<String>(
|
||||
value: 'camera-video',
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
theme.colorScheme.onPrimaryContainer,
|
||||
foregroundColor:
|
||||
theme.colorScheme.primaryContainer,
|
||||
child: const Icon(Icons.videocam_outlined),
|
||||
),
|
||||
title: Text(L10n.of(context).recordAVideo),
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
),
|
||||
),
|
||||
)
|
||||
: SizedBox(
|
||||
height: height,
|
||||
child: TextButton(
|
||||
onPressed: controller.sendAgainAction,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Text(L10n.of(context).tryToSendAgain),
|
||||
const SizedBox(width: 4),
|
||||
const Icon(Icons.send_outlined, size: 16),
|
||||
],
|
||||
PopupMenuItem<String>(
|
||||
value: 'camera',
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
theme.colorScheme.onPrimaryContainer,
|
||||
foregroundColor:
|
||||
theme.colorScheme.primaryContainer,
|
||||
child: const Icon(Icons.camera_alt_outlined),
|
||||
),
|
||||
title: Text(L10n.of(context).takeAPhoto),
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
]
|
||||
: <Widget>[
|
||||
const SizedBox(width: 4),
|
||||
AnimatedContainer(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
width: controller.sendController.text.isNotEmpty ? 0 : height,
|
||||
height: height,
|
||||
alignment: Alignment.center,
|
||||
decoration: const BoxDecoration(),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: PopupMenuButton<String>(
|
||||
icon: const Icon(Icons.add_circle_outline),
|
||||
iconColor: theme.colorScheme.onPrimaryContainer,
|
||||
onSelected: controller.onAddPopupMenuButtonSelected,
|
||||
itemBuilder: (BuildContext context) =>
|
||||
<PopupMenuEntry<String>>[
|
||||
if (PlatformInfos.isMobile)
|
||||
PopupMenuItem<String>(
|
||||
value: 'location',
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
theme.colorScheme.onPrimaryContainer,
|
||||
foregroundColor: theme.colorScheme.primaryContainer,
|
||||
child: const Icon(Icons.gps_fixed_outlined),
|
||||
),
|
||||
title: Text(L10n.of(context).shareLocation),
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
),
|
||||
),
|
||||
PopupMenuItem<String>(
|
||||
value: 'image',
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: theme.colorScheme.onPrimaryContainer,
|
||||
foregroundColor: theme.colorScheme.primaryContainer,
|
||||
child: const Icon(Icons.photo_outlined),
|
||||
),
|
||||
title: Text(L10n.of(context).sendImage),
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem<String>(
|
||||
value: 'video',
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: theme.colorScheme.onPrimaryContainer,
|
||||
foregroundColor: theme.colorScheme.primaryContainer,
|
||||
child: const Icon(Icons.video_camera_back_outlined),
|
||||
),
|
||||
title: Text(L10n.of(context).sendVideo),
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
),
|
||||
),
|
||||
PopupMenuItem<String>(
|
||||
value: 'file',
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: theme.colorScheme.onPrimaryContainer,
|
||||
foregroundColor: theme.colorScheme.primaryContainer,
|
||||
child: const Icon(Icons.attachment_outlined),
|
||||
),
|
||||
title: Text(L10n.of(context).sendFile),
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (PlatformInfos.isMobile)
|
||||
AnimatedContainer(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
width: controller.sendController.text.isNotEmpty ? 0 : height,
|
||||
height: height,
|
||||
alignment: Alignment.center,
|
||||
decoration: const BoxDecoration(),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: PopupMenuButton(
|
||||
icon: const Icon(Icons.camera_alt_outlined),
|
||||
onSelected: controller.onAddPopupMenuButtonSelected,
|
||||
iconColor: theme.colorScheme.onPrimaryContainer,
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem<String>(
|
||||
value: 'camera-video',
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
theme.colorScheme.onPrimaryContainer,
|
||||
foregroundColor: theme.colorScheme.primaryContainer,
|
||||
child: const Icon(Icons.videocam_outlined),
|
||||
),
|
||||
title: Text(L10n.of(context).recordAVideo),
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
Container(
|
||||
height: height,
|
||||
width: height,
|
||||
alignment: Alignment.center,
|
||||
child: IconButton(
|
||||
tooltip: L10n.of(context).emojis,
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
icon: PageTransitionSwitcher(
|
||||
transitionBuilder: (
|
||||
Widget child,
|
||||
Animation<double> primaryAnimation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
return SharedAxisTransition(
|
||||
animation: primaryAnimation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
transitionType: SharedAxisTransitionType.scaled,
|
||||
fillColor: Colors.transparent,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: Icon(
|
||||
controller.showEmojiPicker
|
||||
? Icons.keyboard
|
||||
: Icons.add_reaction_outlined,
|
||||
key: ValueKey(controller.showEmojiPicker),
|
||||
),
|
||||
),
|
||||
PopupMenuItem<String>(
|
||||
value: 'camera',
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
theme.colorScheme.onPrimaryContainer,
|
||||
foregroundColor: theme.colorScheme.primaryContainer,
|
||||
child: const Icon(Icons.camera_alt_outlined),
|
||||
),
|
||||
title: Text(L10n.of(context).takeAPhoto),
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: height,
|
||||
width: height,
|
||||
alignment: Alignment.center,
|
||||
child: IconButton(
|
||||
tooltip: L10n.of(context).emojis,
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
icon: PageTransitionSwitcher(
|
||||
transitionBuilder: (
|
||||
Widget child,
|
||||
Animation<double> primaryAnimation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
return SharedAxisTransition(
|
||||
animation: primaryAnimation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
transitionType: SharedAxisTransitionType.scaled,
|
||||
fillColor: Colors.transparent,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: Icon(
|
||||
controller.showEmojiPicker
|
||||
? Icons.keyboard
|
||||
: Icons.add_reaction_outlined,
|
||||
key: ValueKey(controller.showEmojiPicker),
|
||||
onPressed: controller.emojiPickerAction,
|
||||
),
|
||||
),
|
||||
onPressed: controller.emojiPickerAction,
|
||||
),
|
||||
),
|
||||
if (Matrix.of(context).isMultiAccount &&
|
||||
Matrix.of(context).hasComplexBundles &&
|
||||
Matrix.of(context).currentBundle!.length > 1)
|
||||
Container(
|
||||
width: height,
|
||||
height: height,
|
||||
alignment: Alignment.center,
|
||||
child: _ChatAccountPicker(controller),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 0.0),
|
||||
child: InputBar(
|
||||
room: controller.room,
|
||||
minLines: 1,
|
||||
maxLines: 8,
|
||||
autofocus: !PlatformInfos.isMobile,
|
||||
keyboardType: TextInputType.multiline,
|
||||
textInputAction:
|
||||
AppConfig.sendOnEnter == true && PlatformInfos.isMobile
|
||||
if (Matrix.of(context).isMultiAccount &&
|
||||
Matrix.of(context).hasComplexBundles &&
|
||||
Matrix.of(context).currentBundle!.length > 1)
|
||||
Container(
|
||||
height: height,
|
||||
width: height,
|
||||
alignment: Alignment.center,
|
||||
child: _ChatAccountPicker(controller),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 0.0),
|
||||
child: InputBar(
|
||||
room: controller.room,
|
||||
minLines: 1,
|
||||
maxLines: 8,
|
||||
autofocus: !PlatformInfos.isMobile,
|
||||
keyboardType: TextInputType.multiline,
|
||||
textInputAction: AppConfig.sendOnEnter == true &&
|
||||
PlatformInfos.isMobile
|
||||
? TextInputAction.send
|
||||
: null,
|
||||
onSubmitted: controller.onInputBarSubmitted,
|
||||
onSubmitImage: controller.sendImageFromClipBoard,
|
||||
focusNode: controller.inputFocus,
|
||||
controller: controller.sendController,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.only(
|
||||
left: 6.0,
|
||||
right: 6.0,
|
||||
bottom: 6.0,
|
||||
top: 3.0,
|
||||
onSubmitted: controller.onInputBarSubmitted,
|
||||
onSubmitImage: controller.sendImageFromClipBoard,
|
||||
focusNode: controller.inputFocus,
|
||||
controller: controller.sendController,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.only(
|
||||
left: 6.0,
|
||||
right: 6.0,
|
||||
bottom: 6.0,
|
||||
top: 3.0,
|
||||
),
|
||||
counter: const SizedBox.shrink(),
|
||||
hintText: L10n.of(context).writeAMessage,
|
||||
hintMaxLines: 1,
|
||||
border: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
filled: false,
|
||||
),
|
||||
onChanged: controller.onInputBarChanged,
|
||||
),
|
||||
hintText: L10n.of(context).writeAMessage,
|
||||
hintMaxLines: 1,
|
||||
border: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
filled: false,
|
||||
),
|
||||
onChanged: controller.onInputBarChanged,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: height,
|
||||
width: height,
|
||||
alignment: Alignment.center,
|
||||
child: PlatformInfos.platformCanRecord &&
|
||||
controller.sendController.text.isEmpty
|
||||
? FloatingActionButton.small(
|
||||
tooltip: L10n.of(context).voiceMessage,
|
||||
onPressed: controller.voiceMessageAction,
|
||||
elevation: 0,
|
||||
heroTag: null,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(height),
|
||||
),
|
||||
backgroundColor: theme.bubbleColor,
|
||||
foregroundColor: theme.onBubbleColor,
|
||||
child: const Icon(Icons.mic_none_outlined),
|
||||
)
|
||||
: FloatingActionButton.small(
|
||||
tooltip: L10n.of(context).send,
|
||||
onPressed: controller.send,
|
||||
elevation: 0,
|
||||
heroTag: null,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(height),
|
||||
),
|
||||
backgroundColor: theme.bubbleColor,
|
||||
foregroundColor: theme.onBubbleColor,
|
||||
child: const Icon(Icons.send_outlined),
|
||||
),
|
||||
),
|
||||
],
|
||||
Container(
|
||||
height: height,
|
||||
width: height,
|
||||
alignment: Alignment.center,
|
||||
child: PlatformInfos.platformCanRecord &&
|
||||
controller.sendController.text.isEmpty
|
||||
? IconButton(
|
||||
tooltip: L10n.of(context).voiceMessage,
|
||||
onPressed: () =>
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
L10n.of(context)
|
||||
.longPressToRecordVoiceMessage,
|
||||
),
|
||||
),
|
||||
),
|
||||
onLongPress: () => recordingViewModel
|
||||
.startRecording(controller.room),
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: theme.bubbleColor,
|
||||
foregroundColor: theme.onBubbleColor,
|
||||
),
|
||||
icon: const Icon(Icons.mic_none_outlined),
|
||||
)
|
||||
: IconButton(
|
||||
tooltip: L10n.of(context).send,
|
||||
onPressed: controller.send,
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: theme.bubbleColor,
|
||||
foregroundColor: theme.onBubbleColor,
|
||||
),
|
||||
icon: const Icon(Icons.send_outlined),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -368,6 +375,7 @@ class _ChatAccountPicker extends StatelessWidget {
|
|||
child: FutureBuilder<Profile>(
|
||||
future: controller.sendingClient.fetchOwnProfile(),
|
||||
builder: (context, snapshot) => PopupMenuButton<String>(
|
||||
useRootNavigator: true,
|
||||
onSelected: (mxid) => _popupMenuButtonSelected(mxid, context),
|
||||
itemBuilder: (BuildContext context) => clients
|
||||
.map(
|
||||
|
|
@ -399,4 +407,4 @@ class _ChatAccountPicker extends StatelessWidget {
|
|||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,6 @@ import 'package:extera_next/pages/chat/chat_app_bar_title.dart';
|
|||
import 'package:extera_next/pages/chat/chat_event_list.dart';
|
||||
import 'package:extera_next/pages/chat/encryption_button.dart';
|
||||
import 'package:extera_next/pages/chat/pinned_events.dart';
|
||||
import 'package:extera_next/pages/chat/reactions_picker.dart';
|
||||
import 'package:extera_next/pages/chat/reply_display.dart';
|
||||
import 'package:extera_next/utils/account_config.dart';
|
||||
import 'package:extera_next/utils/localized_exception_extension.dart';
|
||||
|
|
|
|||
|
|
@ -1,256 +0,0 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:extera_next/generated/l10n/l10n.dart';
|
||||
import 'package:path/path.dart' as path_lib;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:record/record.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
|
||||
import 'package:extera_next/config/app_config.dart';
|
||||
import 'package:extera_next/config/setting_keys.dart';
|
||||
import 'package:extera_next/utils/platform_infos.dart';
|
||||
import 'package:extera_next/widgets/matrix.dart';
|
||||
import 'events/audio_player.dart';
|
||||
|
||||
class RecordingDialog extends StatefulWidget {
|
||||
const RecordingDialog({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
RecordingDialogState createState() => RecordingDialogState();
|
||||
}
|
||||
|
||||
class RecordingDialogState extends State<RecordingDialog> {
|
||||
Timer? _recorderSubscription;
|
||||
Duration _duration = Duration.zero;
|
||||
|
||||
bool error = false;
|
||||
|
||||
final _audioRecorder = AudioRecorder();
|
||||
final List<double> amplitudeTimeline = [];
|
||||
|
||||
String? fileName;
|
||||
|
||||
Future<void> startRecording() async {
|
||||
final store = Matrix.of(context).store;
|
||||
try {
|
||||
final codec = kIsWeb
|
||||
// Web seems to create webm instead of ogg when using opus encoder
|
||||
// which does not play on iOS right now. So we use wav for now:
|
||||
? AudioEncoder.wav
|
||||
// Everywhere else we use opus if supported by the platform:
|
||||
: await _audioRecorder.isEncoderSupported(AudioEncoder.opus)
|
||||
? AudioEncoder.opus
|
||||
: AudioEncoder.aacLc;
|
||||
fileName =
|
||||
'recording${DateTime.now().microsecondsSinceEpoch}.${codec.fileExtension}';
|
||||
String? path;
|
||||
if (!kIsWeb) {
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
path = path_lib.join(tempDir.path, fileName);
|
||||
}
|
||||
|
||||
final result = await _audioRecorder.hasPermission();
|
||||
if (result != true) {
|
||||
setState(() => error = true);
|
||||
return;
|
||||
}
|
||||
await WakelockPlus.enable();
|
||||
|
||||
await _audioRecorder.start(
|
||||
RecordConfig(
|
||||
bitRate: AppSettings.audioRecordingBitRate.getItem(store),
|
||||
sampleRate: AppSettings.audioRecordingSamplingRate.getItem(store),
|
||||
numChannels: AppSettings.audioRecordingNumChannels.getItem(store),
|
||||
autoGain: AppSettings.audioRecordingAutoGain.getItem(store),
|
||||
echoCancel: AppSettings.audioRecordingEchoCancel.getItem(store),
|
||||
noiseSuppress: AppSettings.audioRecordingNoiseSuppress.getItem(store),
|
||||
encoder: codec,
|
||||
),
|
||||
path: path ?? '',
|
||||
);
|
||||
setState(() => _duration = Duration.zero);
|
||||
_recorderSubscription?.cancel();
|
||||
_recorderSubscription =
|
||||
Timer.periodic(const Duration(milliseconds: 100), (_) async {
|
||||
final amplitude = await _audioRecorder.getAmplitude();
|
||||
var value = 100 + amplitude.current * 2;
|
||||
value = value < 1 ? 1 : value;
|
||||
amplitudeTimeline.add(value);
|
||||
setState(() {
|
||||
_duration += const Duration(milliseconds: 100);
|
||||
});
|
||||
});
|
||||
} catch (_) {
|
||||
setState(() => error = true);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
startRecording();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WakelockPlus.disable();
|
||||
_recorderSubscription?.cancel();
|
||||
_audioRecorder.stop();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _stopAndSend() async {
|
||||
_recorderSubscription?.cancel();
|
||||
final path = await _audioRecorder.stop();
|
||||
|
||||
if (path == null) throw ('Recording failed!');
|
||||
const waveCount = AudioPlayerWidget.wavesCount;
|
||||
final step = amplitudeTimeline.length < waveCount
|
||||
? 1
|
||||
: (amplitudeTimeline.length / waveCount).round();
|
||||
final waveform = <int>[];
|
||||
for (var i = 0; i < amplitudeTimeline.length; i += step) {
|
||||
waveform.add((amplitudeTimeline[i] / 100 * 1024).round());
|
||||
}
|
||||
Navigator.of(context, rootNavigator: false).pop<RecordingResult>(
|
||||
RecordingResult(
|
||||
path: path,
|
||||
duration: _duration.inMilliseconds,
|
||||
waveform: waveform,
|
||||
fileName: fileName,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
const maxDecibalWidth = 64.0;
|
||||
final time =
|
||||
'${_duration.inMinutes.toString().padLeft(2, '0')}:${(_duration.inSeconds % 60).toString().padLeft(2, '0')}';
|
||||
final content = error
|
||||
? Text(L10n.of(context).oopsSomethingWentWrong)
|
||||
: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 16,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: amplitudeTimeline.reversed
|
||||
.take(26)
|
||||
.toList()
|
||||
.reversed
|
||||
.map(
|
||||
(amplitude) => Container(
|
||||
margin: const EdgeInsets.only(left: 2),
|
||||
width: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primary,
|
||||
borderRadius:
|
||||
BorderRadius.circular(AppConfig.borderRadius),
|
||||
),
|
||||
height: maxDecibalWidth * (amplitude / 100),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(
|
||||
width: 48,
|
||||
child: Text(time),
|
||||
),
|
||||
],
|
||||
);
|
||||
if (PlatformInfos.isCupertinoStyle) {
|
||||
return CupertinoAlertDialog(
|
||||
content: content,
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
onPressed: () => Navigator.of(context, rootNavigator: false).pop(),
|
||||
child: Text(
|
||||
L10n.of(context).cancel,
|
||||
style: TextStyle(
|
||||
color: theme.textTheme.bodyMedium?.color?.withAlpha(150),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (error != true)
|
||||
CupertinoDialogAction(
|
||||
onPressed: _stopAndSend,
|
||||
child: Text(L10n.of(context).send),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return AlertDialog(
|
||||
content: content,
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context, rootNavigator: false).pop(),
|
||||
child: Text(
|
||||
L10n.of(context).cancel,
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (error != true)
|
||||
TextButton(
|
||||
onPressed: _stopAndSend,
|
||||
child: Text(L10n.of(context).send),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RecordingResult {
|
||||
final String path;
|
||||
final int duration;
|
||||
final List<int> waveform;
|
||||
final String? fileName;
|
||||
|
||||
const RecordingResult({
|
||||
required this.path,
|
||||
required this.duration,
|
||||
required this.waveform,
|
||||
required this.fileName,
|
||||
});
|
||||
}
|
||||
|
||||
extension on AudioEncoder {
|
||||
String get fileExtension {
|
||||
switch (this) {
|
||||
case AudioEncoder.aacLc:
|
||||
case AudioEncoder.aacEld:
|
||||
case AudioEncoder.aacHe:
|
||||
return 'm4a';
|
||||
case AudioEncoder.opus:
|
||||
return 'ogg';
|
||||
case AudioEncoder.wav:
|
||||
return 'wav';
|
||||
case AudioEncoder.amrNb:
|
||||
case AudioEncoder.amrWb:
|
||||
case AudioEncoder.flac:
|
||||
case AudioEncoder.pcm16bits:
|
||||
throw UnsupportedError('Not yet used');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:extera_next/config/themes.dart';
|
||||
|
||||
import 'package:extera_next/generated/l10n/l10n.dart';
|
||||
|
||||
import 'package:extera_next/pages/chat/recording_view_model.dart';
|
||||
|
||||
class RecordingInputRow extends StatelessWidget {
|
||||
final RecordingViewModelState state;
|
||||
|
||||
final Future<void> Function(String, int, List<int>, String?) onSend;
|
||||
|
||||
const RecordingInputRow({
|
||||
required this.state,
|
||||
required this.onSend,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
const maxDecibalWidth = 36.0;
|
||||
|
||||
final time =
|
||||
'${state.duration.inMinutes.toString().padLeft(2, '0')}:${(state.duration.inSeconds % 60).toString().padLeft(2, '0')}';
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
IconButton(
|
||||
tooltip: L10n.of(context).cancel,
|
||||
icon: const Icon(Icons.delete_outlined),
|
||||
color: theme.colorScheme.error,
|
||||
onPressed: state.cancel,
|
||||
),
|
||||
if (state.isPaused)
|
||||
IconButton(
|
||||
tooltip: L10n.of(context).resume,
|
||||
icon: const Icon(Icons.play_circle_outline_outlined),
|
||||
onPressed: state.resume,
|
||||
)
|
||||
else
|
||||
IconButton(
|
||||
tooltip: L10n.of(context).pause,
|
||||
icon: const Icon(Icons.pause_circle_outline_outlined),
|
||||
onPressed: state.pause,
|
||||
),
|
||||
Text(time),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
const width = 4;
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: state.amplitudeTimeline.reversed
|
||||
.take((constraints.maxWidth / (width + 2)).floor())
|
||||
.toList()
|
||||
.reversed
|
||||
.map(
|
||||
(amplitude) => Container(
|
||||
margin: const EdgeInsets.only(left: 2),
|
||||
width: width.toDouble(),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primary,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
height: maxDecibalWidth * (amplitude / 100),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
style: IconButton.styleFrom(
|
||||
disabledBackgroundColor: theme.bubbleColor.withAlpha(128),
|
||||
backgroundColor: theme.bubbleColor,
|
||||
foregroundColor: theme.onBubbleColor,
|
||||
),
|
||||
tooltip: L10n.of(context).sendAudio,
|
||||
icon: state.isSending
|
||||
? const SizedBox.square(
|
||||
dimension: 24,
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
)
|
||||
: const Icon(Icons.send_outlined),
|
||||
onPressed: state.isSending ? null : () => state.stopAndSend(onSend),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,230 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:path/path.dart' as path_lib;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:record/record.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
|
||||
import 'package:extera_next/config/setting_keys.dart';
|
||||
import 'package:extera_next/generated/l10n/l10n.dart';
|
||||
import 'package:extera_next/utils/platform_infos.dart';
|
||||
import 'package:extera_next/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
|
||||
import 'package:extera_next/widgets/matrix.dart';
|
||||
import 'events/audio_player.dart';
|
||||
|
||||
class RecordingViewModel extends StatefulWidget {
|
||||
final Widget Function(BuildContext, RecordingViewModelState) builder;
|
||||
|
||||
const RecordingViewModel({
|
||||
required this.builder,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
RecordingViewModelState createState() => RecordingViewModelState();
|
||||
}
|
||||
|
||||
class RecordingViewModelState extends State<RecordingViewModel> {
|
||||
Timer? _recorderSubscription;
|
||||
Duration duration = Duration.zero;
|
||||
|
||||
bool error = false;
|
||||
bool isSending = false;
|
||||
|
||||
bool get isRecording => _audioRecorder != null;
|
||||
|
||||
AudioRecorder? _audioRecorder;
|
||||
final List<double> amplitudeTimeline = [];
|
||||
|
||||
String? fileName;
|
||||
|
||||
bool isPaused = false;
|
||||
|
||||
Future<void> startRecording(Room room) async {
|
||||
room.client.getConfig(); // Preload server file configuration.
|
||||
if (PlatformInfos.isAndroid) {
|
||||
final info = await DeviceInfoPlugin().androidInfo;
|
||||
if (info.version.sdkInt < 19) {
|
||||
showOkAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).unsupportedAndroidVersion,
|
||||
message: L10n.of(context).unsupportedAndroidVersionLong,
|
||||
okLabel: L10n.of(context).close,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: add permission request
|
||||
if (await AudioRecorder().hasPermission() == false) return;
|
||||
|
||||
final store = Matrix.of(context).store;
|
||||
|
||||
final audioRecorder = _audioRecorder ??= AudioRecorder();
|
||||
setState(() {});
|
||||
|
||||
try {
|
||||
final codec = kIsWeb
|
||||
// Web seems to create webm instead of ogg when using opus encoder
|
||||
// which does not play on iOS right now. So we use wav for now:
|
||||
? AudioEncoder.wav
|
||||
// Everywhere else we use opus if supported by the platform:
|
||||
: await audioRecorder.isEncoderSupported(AudioEncoder.opus)
|
||||
? AudioEncoder.opus
|
||||
: AudioEncoder.aacLc;
|
||||
fileName =
|
||||
'recording${DateTime.now().microsecondsSinceEpoch}.${codec.fileExtension}';
|
||||
String? path;
|
||||
if (!kIsWeb) {
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
path = path_lib.join(tempDir.path, fileName);
|
||||
}
|
||||
|
||||
final result = await audioRecorder.hasPermission();
|
||||
if (result != true) {
|
||||
setState(() => error = true);
|
||||
return;
|
||||
}
|
||||
await WakelockPlus.enable();
|
||||
|
||||
await audioRecorder.start(
|
||||
RecordConfig(
|
||||
bitRate: AppSettings.audioRecordingBitRate.getItem(store),
|
||||
sampleRate: AppSettings.audioRecordingSamplingRate.getItem(store),
|
||||
numChannels: AppSettings.audioRecordingNumChannels.getItem(store),
|
||||
autoGain: AppSettings.audioRecordingAutoGain.getItem(store),
|
||||
echoCancel: AppSettings.audioRecordingEchoCancel.getItem(store),
|
||||
noiseSuppress: AppSettings.audioRecordingNoiseSuppress.getItem(store),
|
||||
encoder: codec,
|
||||
),
|
||||
path: path ?? '',
|
||||
);
|
||||
setState(() => duration = Duration.zero);
|
||||
_subscribe();
|
||||
} catch (_) {
|
||||
setState(() => error = true);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_reset();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _subscribe() {
|
||||
_recorderSubscription?.cancel();
|
||||
_recorderSubscription =
|
||||
Timer.periodic(const Duration(milliseconds: 100), (_) async {
|
||||
final amplitude = await _audioRecorder!.getAmplitude();
|
||||
var value = 100 + amplitude.current * 2;
|
||||
value = value < 1 ? 1 : value;
|
||||
amplitudeTimeline.add(value);
|
||||
setState(() {
|
||||
duration += const Duration(milliseconds: 100);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _reset() {
|
||||
WakelockPlus.disable();
|
||||
_recorderSubscription?.cancel();
|
||||
_audioRecorder?.stop();
|
||||
_audioRecorder = null;
|
||||
isSending = false;
|
||||
error = false;
|
||||
fileName = null;
|
||||
duration = Duration.zero;
|
||||
amplitudeTimeline.clear();
|
||||
isPaused = false;
|
||||
}
|
||||
|
||||
void cancel() {
|
||||
setState(() {
|
||||
_reset();
|
||||
});
|
||||
}
|
||||
|
||||
void pause() {
|
||||
_audioRecorder?.pause();
|
||||
_recorderSubscription?.cancel();
|
||||
setState(() {
|
||||
isPaused = true;
|
||||
});
|
||||
}
|
||||
|
||||
void resume() {
|
||||
_audioRecorder?.resume();
|
||||
_subscribe();
|
||||
setState(() {
|
||||
isPaused = false;
|
||||
});
|
||||
}
|
||||
|
||||
void stopAndSend(
|
||||
Future<void> Function(
|
||||
String path,
|
||||
int duration,
|
||||
List<int> waveform,
|
||||
String? fileName,
|
||||
) onSend,
|
||||
) async {
|
||||
_recorderSubscription?.cancel();
|
||||
final path = await _audioRecorder?.stop();
|
||||
|
||||
if (path == null) throw ('Recording failed!');
|
||||
const waveCount = AudioPlayerWidget.wavesCount;
|
||||
final step = amplitudeTimeline.length < waveCount
|
||||
? 1
|
||||
: (amplitudeTimeline.length / waveCount).round();
|
||||
final waveform = <int>[];
|
||||
for (var i = 0; i < amplitudeTimeline.length; i += step) {
|
||||
waveform.add((amplitudeTimeline[i] / 100 * 1024).round());
|
||||
}
|
||||
|
||||
setState(() {
|
||||
isSending = true;
|
||||
});
|
||||
try {
|
||||
await onSend(path, duration.inMilliseconds, waveform, fileName);
|
||||
} catch (e, s) {
|
||||
Logs().e('Unable to send voice message', e, s);
|
||||
setState(() {
|
||||
isSending = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
cancel();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.builder(context, this);
|
||||
}
|
||||
|
||||
extension on AudioEncoder {
|
||||
String get fileExtension {
|
||||
switch (this) {
|
||||
case AudioEncoder.aacLc:
|
||||
case AudioEncoder.aacEld:
|
||||
case AudioEncoder.aacHe:
|
||||
return 'm4a';
|
||||
case AudioEncoder.opus:
|
||||
return 'ogg';
|
||||
case AudioEncoder.wav:
|
||||
return 'wav';
|
||||
case AudioEncoder.amrNb:
|
||||
case AudioEncoder.amrWb:
|
||||
case AudioEncoder.flac:
|
||||
case AudioEncoder.pcm16bits:
|
||||
throw UnsupportedError('Not yet used');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import 'package:extera_next/config/themes.dart';
|
||||
import 'package:extera_next/pages/chat/events/message.dart';
|
||||
import 'package:extera_next/widgets/adaptive_dialogs/adaptive_dialog_action.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import 'package:extera_next/generated/l10n/l10n.dart';
|
|||
import 'package:matrix/matrix.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
|
||||
import 'package:extera_next/config/app_config.dart';
|
||||
import 'package:extera_next/utils/localized_exception_extension.dart';
|
||||
import 'package:extera_next/utils/matrix_sdk_extensions/matrix_file_extension.dart';
|
||||
import 'package:extera_next/utils/other_party_can_receive.dart';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import 'package:extera_next/config/themes.dart';
|
||||
import 'package:extera_next/pages/chat/events/message.dart';
|
||||
import 'package:extera_next/widgets/adaptive_dialogs/adaptive_dialog_action.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -207,8 +207,9 @@ class ChatDetailsView extends StatelessWidget {
|
|||
),
|
||||
],
|
||||
),
|
||||
Divider(color: theme.dividerColor),
|
||||
if (!room.canChangeStateEvent(EventTypes.RoomTopic))
|
||||
if (room.canChangeStateEvent(EventTypes.RoomTopic) ||
|
||||
room.topic.isNotEmpty) ...[
|
||||
Divider(color: theme.dividerColor),
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context).chatDescription,
|
||||
|
|
@ -217,53 +218,46 @@ class ChatDetailsView extends StatelessWidget {
|
|||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
trailing:
|
||||
room.canChangeStateEvent(EventTypes.RoomTopic)
|
||||
? IconButton(
|
||||
onPressed: controller.setTopicAction,
|
||||
tooltip:
|
||||
L10n.of(context).setChatDescription,
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: TextButton.icon(
|
||||
onPressed: controller.setTopicAction,
|
||||
label: Text(L10n.of(context).setChatDescription),
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
style: TextButton.styleFrom(
|
||||
iconColor:
|
||||
theme.colorScheme.onSecondaryContainer,
|
||||
backgroundColor:
|
||||
theme.colorScheme.secondaryContainer,
|
||||
foregroundColor:
|
||||
theme.colorScheme.onSecondaryContainer,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
),
|
||||
child: SelectableLinkify(
|
||||
text: room.topic.isEmpty
|
||||
? L10n.of(context).noChatDescriptionYet
|
||||
: room.topic,
|
||||
textScaleFactor:
|
||||
MediaQuery.textScalerOf(context).scale(1),
|
||||
options: const LinkifyOptions(humanize: false),
|
||||
linkStyle: const TextStyle(
|
||||
color: Colors.blueAccent,
|
||||
decorationColor: Colors.blueAccent,
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontStyle: room.topic.isEmpty
|
||||
? FontStyle.italic
|
||||
: FontStyle.normal,
|
||||
color: theme.textTheme.bodyMedium!.color,
|
||||
decorationColor:
|
||||
theme.textTheme.bodyMedium!.color,
|
||||
),
|
||||
onOpen: (url) =>
|
||||
UrlLauncher(context, url.url).launchUrl(),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
),
|
||||
child: SelectableLinkify(
|
||||
text: room.topic.isEmpty
|
||||
? L10n.of(context).noChatDescriptionYet
|
||||
: room.topic,
|
||||
textScaleFactor:
|
||||
MediaQuery.textScalerOf(context).scale(1),
|
||||
options: const LinkifyOptions(humanize: false),
|
||||
linkStyle: const TextStyle(
|
||||
color: Colors.blueAccent,
|
||||
decorationColor: Colors.blueAccent,
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontStyle: room.topic.isEmpty
|
||||
? FontStyle.italic
|
||||
: FontStyle.normal,
|
||||
color: theme.textTheme.bodyMedium!.color,
|
||||
decorationColor:
|
||||
theme.textTheme.bodyMedium!.color,
|
||||
),
|
||||
onOpen: (url) =>
|
||||
UrlLauncher(context, url.url).launchUrl(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
Divider(color: theme.dividerColor),
|
||||
ListTile(
|
||||
leading: CircleAvatar(
|
||||
|
|
@ -366,4 +360,4 @@ class ChatDetailsView extends StatelessWidget {
|
|||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:extera_next/config/themes.dart';
|
||||
|
||||
class AvatarPageHeader extends StatelessWidget {
|
||||
final Widget avatar;
|
||||
|
||||
final void Function()? onAvatarEdit;
|
||||
|
||||
final Widget? textButtonLeft, textButtonRight;
|
||||
|
||||
final List<Widget> iconButtons;
|
||||
|
||||
const AvatarPageHeader({
|
||||
super.key,
|
||||
required this.avatar,
|
||||
this.onAvatarEdit,
|
||||
this.iconButtons = const [],
|
||||
this.textButtonLeft,
|
||||
this.textButtonRight,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final onAvatarEdit = this.onAvatarEdit;
|
||||
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: FluffyThemes.columnWidth),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
avatar,
|
||||
if (onAvatarEdit != null)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: FloatingActionButton.small(
|
||||
elevation: 2,
|
||||
onPressed: onAvatarEdit,
|
||||
heroTag: null,
|
||||
child: const Icon(Icons.camera_alt_outlined),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
TextButtonTheme(
|
||||
data: TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
disabledForegroundColor: theme.colorScheme.onSurface,
|
||||
foregroundColor: theme.colorScheme.onSurface,
|
||||
textStyle: const TextStyle(fontWeight: FontWeight.normal),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: constraints.maxWidth / 2,
|
||||
),
|
||||
child: textButtonLeft,
|
||||
),
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: constraints.maxWidth / 2,
|
||||
),
|
||||
child: textButtonRight,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButtonTheme(
|
||||
data: IconButtonThemeData(
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: theme.colorScheme.surfaceContainer,
|
||||
iconSize: 24,
|
||||
padding: const EdgeInsets.all(16),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: iconButtons,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 0.0),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,7 @@ import 'package:extera_next/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog
|
|||
import 'package:extera_next/widgets/future_loading_dialog.dart';
|
||||
import 'matrix.dart';
|
||||
|
||||
enum ChatPopupMenuActions { details, mute, unmute, leave, search }
|
||||
enum ChatPopupMenuActions { details, mute, unmute, emote, leave, search }
|
||||
|
||||
class ChatSettingsPopupMenu extends StatefulWidget {
|
||||
final Room room;
|
||||
|
|
@ -31,6 +31,17 @@ class ChatSettingsPopupMenuState extends State<ChatSettingsPopupMenu> {
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
void goToEmoteSettings() async {
|
||||
final room = widget.room;
|
||||
if ((room.states['im.ponies.room_emotes'] ?? <String, Event>{})
|
||||
.keys
|
||||
.any((String s) => s.isNotEmpty)) {
|
||||
context.push('/rooms/${room.id}/details/multiple_emotes');
|
||||
} else {
|
||||
context.push('/rooms/${room.id}/details/emotes');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
notificationChangeSub ??= Matrix.of(context)
|
||||
|
|
@ -73,6 +84,9 @@ class ChatSettingsPopupMenuState extends State<ChatSettingsPopupMenu> {
|
|||
}
|
||||
}
|
||||
break;
|
||||
case ChatPopupMenuActions.emote:
|
||||
goToEmoteSettings();
|
||||
break;
|
||||
case ChatPopupMenuActions.mute:
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
|
|
|
|||
Loading…
Reference in New Issue