Reapply "починил выход, наконец-то. Сделал недо жесты, добавил эксперементалбные функции замены цвета у боковой панели и списка чатов, добавил настройку отступа сообщений на андроедах а то может у кого то ломаться"
This reverts commit c0834eaa39.
This commit is contained in:
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@@ -2972,499 +2973,530 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = context.watch<ThemeProvider>();
|
final theme = context.watch<ThemeProvider>();
|
||||||
|
|
||||||
return Scaffold(
|
final isDesktop =
|
||||||
extendBodyBehindAppBar: theme.useGlassPanels,
|
kIsWeb ||
|
||||||
resizeToAvoidBottomInset: false,
|
defaultTargetPlatform == TargetPlatform.windows ||
|
||||||
appBar: _buildAppBar(),
|
defaultTargetPlatform == TargetPlatform.linux ||
|
||||||
body: Stack(
|
defaultTargetPlatform == TargetPlatform.macOS;
|
||||||
children: [
|
|
||||||
Positioned.fill(child: _buildChatWallpaper(theme)),
|
final body = Stack(
|
||||||
Column(
|
children: [
|
||||||
children: [
|
Positioned.fill(child: _buildChatWallpaper(theme)),
|
||||||
AnimatedSwitcher(
|
Column(
|
||||||
duration: const Duration(milliseconds: 250),
|
children: [
|
||||||
switchInCurve: Curves.easeInOutCubic,
|
AnimatedSwitcher(
|
||||||
switchOutCurve: Curves.easeInOutCubic,
|
duration: const Duration(milliseconds: 250),
|
||||||
transitionBuilder: (child, animation) {
|
switchInCurve: Curves.easeInOutCubic,
|
||||||
return SlideTransition(
|
switchOutCurve: Curves.easeInOutCubic,
|
||||||
position: Tween<Offset>(
|
transitionBuilder: (child, animation) {
|
||||||
begin: const Offset(0, -0.5),
|
return SlideTransition(
|
||||||
end: Offset.zero,
|
position: Tween<Offset>(
|
||||||
).animate(animation),
|
begin: const Offset(0, -0.5),
|
||||||
child: FadeTransition(opacity: animation, child: child),
|
end: Offset.zero,
|
||||||
);
|
).animate(animation),
|
||||||
},
|
child: FadeTransition(opacity: animation, child: child),
|
||||||
child: _pinnedMessage != null
|
);
|
||||||
? SafeArea(
|
},
|
||||||
key: ValueKey(_pinnedMessage!.id),
|
child: _pinnedMessage != null
|
||||||
child: InkWell(
|
? SafeArea(
|
||||||
|
key: ValueKey(_pinnedMessage!.id),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: _scrollToPinnedMessage,
|
||||||
|
child: PinnedMessageWidget(
|
||||||
|
pinnedMessage: _pinnedMessage!,
|
||||||
|
contacts: _contactDetailsCache,
|
||||||
|
myId: _actualMyId ?? 0,
|
||||||
onTap: _scrollToPinnedMessage,
|
onTap: _scrollToPinnedMessage,
|
||||||
child: PinnedMessageWidget(
|
onClose: () {
|
||||||
pinnedMessage: _pinnedMessage!,
|
setState(() {
|
||||||
contacts: _contactDetailsCache,
|
_pinnedMessage = null;
|
||||||
myId: _actualMyId ?? 0,
|
});
|
||||||
onTap: _scrollToPinnedMessage,
|
},
|
||||||
onClose: () {
|
|
||||||
setState(() {
|
|
||||||
_pinnedMessage = null;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
: const SizedBox.shrink(key: ValueKey('empty')),
|
)
|
||||||
),
|
: const SizedBox.shrink(key: ValueKey('empty')),
|
||||||
Expanded(
|
),
|
||||||
child: Stack(
|
Expanded(
|
||||||
children: [
|
child: Stack(
|
||||||
AnimatedSwitcher(
|
children: [
|
||||||
duration: const Duration(milliseconds: 300),
|
AnimatedSwitcher(
|
||||||
switchInCurve: Curves.easeInOutCubic,
|
duration: const Duration(milliseconds: 300),
|
||||||
switchOutCurve: Curves.easeInOutCubic,
|
switchInCurve: Curves.easeInOutCubic,
|
||||||
transitionBuilder: (child, animation) {
|
switchOutCurve: Curves.easeInOutCubic,
|
||||||
return FadeTransition(
|
transitionBuilder: (child, animation) {
|
||||||
opacity: animation,
|
return FadeTransition(
|
||||||
child: ScaleTransition(
|
opacity: animation,
|
||||||
scale: Tween<double>(begin: 0.8, end: 1.0).animate(
|
child: ScaleTransition(
|
||||||
CurvedAnimation(
|
scale: Tween<double>(begin: 0.8, end: 1.0).animate(
|
||||||
parent: animation,
|
CurvedAnimation(
|
||||||
curve: Curves.easeOutCubic,
|
parent: animation,
|
||||||
),
|
curve: Curves.easeOutCubic,
|
||||||
),
|
),
|
||||||
child: child,
|
|
||||||
),
|
),
|
||||||
);
|
child: child,
|
||||||
},
|
),
|
||||||
child: (!_isIdReady || _isLoadingHistory)
|
);
|
||||||
? const Center(
|
},
|
||||||
key: ValueKey('loading'),
|
child: (!_isIdReady || _isLoadingHistory)
|
||||||
child: CircularProgressIndicator(),
|
? const Center(
|
||||||
)
|
key: ValueKey('loading'),
|
||||||
: _messages.isEmpty && !widget.isChannel
|
child: CircularProgressIndicator(),
|
||||||
? _EmptyChatWidget(
|
)
|
||||||
sticker: _emptyChatSticker,
|
: _messages.isEmpty && !widget.isChannel
|
||||||
onStickerTap: _sendEmptyChatSticker,
|
? _EmptyChatWidget(
|
||||||
)
|
sticker: _emptyChatSticker,
|
||||||
: AnimatedPadding(
|
onStickerTap: _sendEmptyChatSticker,
|
||||||
key: const ValueKey('chat_list'),
|
)
|
||||||
duration: const Duration(milliseconds: 300),
|
: AnimatedPadding(
|
||||||
curve: Curves.easeInOutCubic,
|
key: const ValueKey('chat_list'),
|
||||||
padding: EdgeInsets.only(
|
duration: const Duration(milliseconds: 300),
|
||||||
bottom: MediaQuery.of(
|
curve: Curves.easeInOutCubic,
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
bottom: () {
|
||||||
|
final baseInset = MediaQuery.of(
|
||||||
context,
|
context,
|
||||||
).viewInsets.bottom,
|
).viewInsets.bottom;
|
||||||
|
final isAndroid =
|
||||||
|
defaultTargetPlatform ==
|
||||||
|
TargetPlatform.android;
|
||||||
|
if (!isAndroid) {
|
||||||
|
return baseInset;
|
||||||
|
}
|
||||||
|
final keyboardVisible = baseInset > 0.0;
|
||||||
|
if (keyboardVisible &&
|
||||||
|
theme
|
||||||
|
.ignoreMobileBottomPaddingWhenKeyboard) {
|
||||||
|
return baseInset;
|
||||||
|
}
|
||||||
|
return baseInset +
|
||||||
|
theme.mobileChatBottomPadding;
|
||||||
|
}(),
|
||||||
|
),
|
||||||
|
child: ScrollablePositionedList.builder(
|
||||||
|
itemScrollController: _itemScrollController,
|
||||||
|
itemPositionsListener: _itemPositionsListener,
|
||||||
|
reverse: true,
|
||||||
|
padding: EdgeInsets.fromLTRB(
|
||||||
|
8.0,
|
||||||
|
8.0,
|
||||||
|
8.0,
|
||||||
|
widget.isChannel
|
||||||
|
? 24.0
|
||||||
|
: (isDesktop ? 100.0 : 0.0),
|
||||||
),
|
),
|
||||||
child: ScrollablePositionedList.builder(
|
itemCount: _chatItems.length,
|
||||||
itemScrollController: _itemScrollController,
|
itemBuilder: (context, index) {
|
||||||
itemPositionsListener: _itemPositionsListener,
|
final mappedIndex =
|
||||||
reverse: true,
|
_chatItems.length - 1 - index;
|
||||||
padding: EdgeInsets.fromLTRB(
|
final item = _chatItems[mappedIndex];
|
||||||
8.0,
|
final isLastVisual =
|
||||||
8.0,
|
index == _chatItems.length - 1;
|
||||||
8.0,
|
|
||||||
widget.isChannel ? 16.0 : 100.0,
|
|
||||||
),
|
|
||||||
itemCount: _chatItems.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final mappedIndex =
|
|
||||||
_chatItems.length - 1 - index;
|
|
||||||
final item = _chatItems[mappedIndex];
|
|
||||||
final isLastVisual =
|
|
||||||
index == _chatItems.length - 1;
|
|
||||||
|
|
||||||
// Убрали вызов _loadMore() отсюда - он вызывается из _itemPositionsListener
|
// Убрали вызов _loadMore() отсюда - он вызывается из _itemPositionsListener
|
||||||
// чтобы избежать setState() во время build фазы
|
// чтобы избежать setState() во время build фазы
|
||||||
|
|
||||||
if (item is MessageItem) {
|
if (item is MessageItem) {
|
||||||
final message = item.message;
|
final message = item.message;
|
||||||
final key = _messageKeys.putIfAbsent(
|
final key = _messageKeys.putIfAbsent(
|
||||||
message.id,
|
message.id,
|
||||||
() => GlobalKey(),
|
() => GlobalKey(),
|
||||||
|
);
|
||||||
|
final bool isHighlighted =
|
||||||
|
_isSearching &&
|
||||||
|
_searchResults.isNotEmpty &&
|
||||||
|
_currentResultIndex != -1 &&
|
||||||
|
message.id ==
|
||||||
|
_searchResults[_currentResultIndex]
|
||||||
|
.id;
|
||||||
|
|
||||||
|
final isControlMessage = message.attaches.any(
|
||||||
|
(a) => a['_type'] == 'CONTROL',
|
||||||
|
);
|
||||||
|
if (isControlMessage) {
|
||||||
|
return _ControlMessageChip(
|
||||||
|
message: message,
|
||||||
|
contacts: _contactDetailsCache,
|
||||||
|
myId: _actualMyId ?? widget.myId,
|
||||||
);
|
);
|
||||||
final bool isHighlighted =
|
|
||||||
_isSearching &&
|
|
||||||
_searchResults.isNotEmpty &&
|
|
||||||
_currentResultIndex != -1 &&
|
|
||||||
message.id ==
|
|
||||||
_searchResults[_currentResultIndex]
|
|
||||||
.id;
|
|
||||||
|
|
||||||
final isControlMessage = message.attaches
|
|
||||||
.any((a) => a['_type'] == 'CONTROL');
|
|
||||||
if (isControlMessage) {
|
|
||||||
return _ControlMessageChip(
|
|
||||||
message: message,
|
|
||||||
contacts: _contactDetailsCache,
|
|
||||||
myId: _actualMyId ?? widget.myId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final bool isMe =
|
|
||||||
item.message.senderId == _actualMyId;
|
|
||||||
|
|
||||||
MessageReadStatus? readStatus;
|
|
||||||
if (isMe) {
|
|
||||||
final messageId = item.message.id;
|
|
||||||
if (messageId.startsWith('local_')) {
|
|
||||||
readStatus = MessageReadStatus.sending;
|
|
||||||
} else {
|
|
||||||
readStatus = MessageReadStatus.sent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String? forwardedFrom;
|
|
||||||
String? forwardedFromAvatarUrl;
|
|
||||||
if (message.isForwarded) {
|
|
||||||
final link = message.link;
|
|
||||||
if (link is Map<String, dynamic>) {
|
|
||||||
final chatName =
|
|
||||||
link['chatName'] as String?;
|
|
||||||
final chatIconUrl =
|
|
||||||
link['chatIconUrl'] as String?;
|
|
||||||
|
|
||||||
if (chatName != null) {
|
|
||||||
forwardedFrom = chatName;
|
|
||||||
forwardedFromAvatarUrl = chatIconUrl;
|
|
||||||
} else {
|
|
||||||
final forwardedMessage =
|
|
||||||
link['message']
|
|
||||||
as Map<String, dynamic>?;
|
|
||||||
final originalSenderId =
|
|
||||||
forwardedMessage?['sender']
|
|
||||||
as int?;
|
|
||||||
if (originalSenderId != null) {
|
|
||||||
final originalSenderContact =
|
|
||||||
_contactDetailsCache[originalSenderId];
|
|
||||||
if (originalSenderContact == null) {
|
|
||||||
_loadContactIfNeeded(
|
|
||||||
originalSenderId,
|
|
||||||
);
|
|
||||||
forwardedFrom =
|
|
||||||
'Участник $originalSenderId';
|
|
||||||
forwardedFromAvatarUrl = null;
|
|
||||||
} else {
|
|
||||||
forwardedFrom =
|
|
||||||
originalSenderContact.name;
|
|
||||||
forwardedFromAvatarUrl =
|
|
||||||
originalSenderContact
|
|
||||||
.photoBaseUrl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
String? senderName;
|
|
||||||
if (widget.isGroupChat && !isMe) {
|
|
||||||
bool shouldShowName = true;
|
|
||||||
if (mappedIndex > 0) {
|
|
||||||
final previousItem =
|
|
||||||
_chatItems[mappedIndex - 1];
|
|
||||||
if (previousItem is MessageItem) {
|
|
||||||
final previousMessage =
|
|
||||||
previousItem.message;
|
|
||||||
if (previousMessage.senderId ==
|
|
||||||
message.senderId) {
|
|
||||||
final timeDifferenceInMinutes =
|
|
||||||
(message.time -
|
|
||||||
previousMessage.time) /
|
|
||||||
(1000 * 60);
|
|
||||||
if (timeDifferenceInMinutes < 5) {
|
|
||||||
shouldShowName = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (shouldShowName) {
|
|
||||||
final senderContact =
|
|
||||||
_contactDetailsCache[message
|
|
||||||
.senderId];
|
|
||||||
if (senderContact != null) {
|
|
||||||
senderName = getContactDisplayName(
|
|
||||||
contactId: senderContact.id,
|
|
||||||
originalName: senderContact.name,
|
|
||||||
originalFirstName:
|
|
||||||
senderContact.firstName,
|
|
||||||
originalLastName:
|
|
||||||
senderContact.lastName,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
senderName = 'ID ${message.senderId}';
|
|
||||||
_loadContactIfNeeded(
|
|
||||||
message.senderId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
final hasPhoto = item.message.attaches.any(
|
|
||||||
(a) => a['_type'] == 'PHOTO',
|
|
||||||
);
|
|
||||||
final isNew = !_animatedMessageIds.contains(
|
|
||||||
item.message.id,
|
|
||||||
);
|
|
||||||
final deferImageLoading =
|
|
||||||
hasPhoto &&
|
|
||||||
isNew &&
|
|
||||||
!_anyOptimize &&
|
|
||||||
!context
|
|
||||||
.read<ThemeProvider>()
|
|
||||||
.animatePhotoMessages;
|
|
||||||
|
|
||||||
String? decryptedText;
|
|
||||||
if (_isEncryptionPasswordSetForCurrentChat &&
|
|
||||||
_encryptionConfigForCurrentChat !=
|
|
||||||
null &&
|
|
||||||
_encryptionConfigForCurrentChat!
|
|
||||||
.password
|
|
||||||
.isNotEmpty &&
|
|
||||||
item.message.text.startsWith(
|
|
||||||
ChatEncryptionService.encryptedPrefix,
|
|
||||||
)) {
|
|
||||||
decryptedText =
|
|
||||||
ChatEncryptionService.decryptWithPassword(
|
|
||||||
_encryptionConfigForCurrentChat!
|
|
||||||
.password,
|
|
||||||
item.message.text,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final bubble = ChatMessageBubble(
|
|
||||||
key: key,
|
|
||||||
message: item.message,
|
|
||||||
isMe: isMe,
|
|
||||||
readStatus: readStatus,
|
|
||||||
isReactionSending: _sendingReactions
|
|
||||||
.contains(item.message.id),
|
|
||||||
deferImageLoading: deferImageLoading,
|
|
||||||
myUserId: _actualMyId,
|
|
||||||
chatId: widget.chatId,
|
|
||||||
isEncryptionPasswordSet:
|
|
||||||
_isEncryptionPasswordSetForCurrentChat,
|
|
||||||
decryptedText: decryptedText,
|
|
||||||
onReply: widget.isChannel
|
|
||||||
? null
|
|
||||||
: () => _replyToMessage(item.message),
|
|
||||||
onForward: () =>
|
|
||||||
_forwardMessage(item.message),
|
|
||||||
onEdit: isMe
|
|
||||||
? () => _editMessage(item.message)
|
|
||||||
: null,
|
|
||||||
canEditMessage: isMe
|
|
||||||
? item.message.canEdit(_actualMyId!)
|
|
||||||
: null,
|
|
||||||
onDeleteForMe: isMe
|
|
||||||
? () async {
|
|
||||||
await ApiService.instance
|
|
||||||
.deleteMessage(
|
|
||||||
widget.chatId,
|
|
||||||
item.message.id,
|
|
||||||
forMe: true,
|
|
||||||
);
|
|
||||||
widget.onChatUpdated?.call();
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
onDeleteForAll: isMe
|
|
||||||
? () async {
|
|
||||||
await ApiService.instance
|
|
||||||
.deleteMessage(
|
|
||||||
widget.chatId,
|
|
||||||
item.message.id,
|
|
||||||
forMe: false,
|
|
||||||
);
|
|
||||||
widget.onChatUpdated?.call();
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
onReaction: (emoji) {
|
|
||||||
_updateReactionOptimistically(
|
|
||||||
item.message.id,
|
|
||||||
emoji,
|
|
||||||
);
|
|
||||||
ApiService.instance.sendReaction(
|
|
||||||
widget.chatId,
|
|
||||||
item.message.id,
|
|
||||||
emoji,
|
|
||||||
);
|
|
||||||
widget.onChatUpdated?.call();
|
|
||||||
},
|
|
||||||
onRemoveReaction: () {
|
|
||||||
_removeReactionOptimistically(
|
|
||||||
item.message.id,
|
|
||||||
);
|
|
||||||
ApiService.instance.removeReaction(
|
|
||||||
widget.chatId,
|
|
||||||
item.message.id,
|
|
||||||
);
|
|
||||||
widget.onChatUpdated?.call();
|
|
||||||
},
|
|
||||||
isGroupChat: widget.isGroupChat,
|
|
||||||
isChannel: widget.isChannel,
|
|
||||||
senderName: senderName,
|
|
||||||
forwardedFrom: forwardedFrom,
|
|
||||||
forwardedFromAvatarUrl:
|
|
||||||
forwardedFromAvatarUrl,
|
|
||||||
contactDetailsCache: _contactDetailsCache,
|
|
||||||
onReplyTap: _scrollToMessage,
|
|
||||||
useAutoReplyColor: context
|
|
||||||
.read<ThemeProvider>()
|
|
||||||
.useAutoReplyColor,
|
|
||||||
customReplyColor: context
|
|
||||||
.read<ThemeProvider>()
|
|
||||||
.customReplyColor,
|
|
||||||
isFirstInGroup: item.isFirstInGroup,
|
|
||||||
isLastInGroup: item.isLastInGroup,
|
|
||||||
isGrouped: item.isGrouped,
|
|
||||||
avatarVerticalOffset:
|
|
||||||
-8.0, // Смещение аватарки вверх на 8px
|
|
||||||
onComplain: () =>
|
|
||||||
_showComplaintDialog(item.message.id),
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget finalMessageWidget =
|
|
||||||
bubble as Widget;
|
|
||||||
|
|
||||||
if (isHighlighted) {
|
|
||||||
return TweenAnimationBuilder<double>(
|
|
||||||
duration: const Duration(
|
|
||||||
milliseconds: 600,
|
|
||||||
),
|
|
||||||
tween: Tween<double>(
|
|
||||||
begin: 0.3,
|
|
||||||
end: 0.6,
|
|
||||||
),
|
|
||||||
curve: Curves.easeInOut,
|
|
||||||
builder: (context, value, child) {
|
|
||||||
return Container(
|
|
||||||
margin: const EdgeInsets.symmetric(
|
|
||||||
vertical: 2,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.primaryContainer
|
|
||||||
.withOpacity(value),
|
|
||||||
borderRadius:
|
|
||||||
BorderRadius.circular(16),
|
|
||||||
border: Border.all(
|
|
||||||
color: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.primary,
|
|
||||||
width: 1.5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: finalMessageWidget,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Плавное появление новых сообщений
|
|
||||||
if (isNew && !_anyOptimize) {
|
|
||||||
return TweenAnimationBuilder<double>(
|
|
||||||
duration: const Duration(
|
|
||||||
milliseconds: 400,
|
|
||||||
),
|
|
||||||
tween: Tween<double>(
|
|
||||||
begin: 0.0,
|
|
||||||
end: 1.0,
|
|
||||||
),
|
|
||||||
curve: Curves.easeOutCubic,
|
|
||||||
builder: (context, value, child) {
|
|
||||||
return Opacity(
|
|
||||||
opacity: value,
|
|
||||||
child: Transform.translate(
|
|
||||||
offset: Offset(
|
|
||||||
0,
|
|
||||||
20 * (1 - value),
|
|
||||||
),
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: finalMessageWidget,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return finalMessageWidget;
|
|
||||||
} else if (item is DateSeparatorItem) {
|
|
||||||
return _DateSeparatorChip(date: item.date);
|
|
||||||
}
|
}
|
||||||
if (isLastVisual && _isLoadingMore) {
|
|
||||||
|
final bool isMe =
|
||||||
|
item.message.senderId == _actualMyId;
|
||||||
|
|
||||||
|
MessageReadStatus? readStatus;
|
||||||
|
if (isMe) {
|
||||||
|
final messageId = item.message.id;
|
||||||
|
if (messageId.startsWith('local_')) {
|
||||||
|
readStatus = MessageReadStatus.sending;
|
||||||
|
} else {
|
||||||
|
readStatus = MessageReadStatus.sent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String? forwardedFrom;
|
||||||
|
String? forwardedFromAvatarUrl;
|
||||||
|
if (message.isForwarded) {
|
||||||
|
final link = message.link;
|
||||||
|
if (link is Map<String, dynamic>) {
|
||||||
|
final chatName =
|
||||||
|
link['chatName'] as String?;
|
||||||
|
final chatIconUrl =
|
||||||
|
link['chatIconUrl'] as String?;
|
||||||
|
|
||||||
|
if (chatName != null) {
|
||||||
|
forwardedFrom = chatName;
|
||||||
|
forwardedFromAvatarUrl = chatIconUrl;
|
||||||
|
} else {
|
||||||
|
final forwardedMessage =
|
||||||
|
link['message']
|
||||||
|
as Map<String, dynamic>?;
|
||||||
|
final originalSenderId =
|
||||||
|
forwardedMessage?['sender'] as int?;
|
||||||
|
if (originalSenderId != null) {
|
||||||
|
final originalSenderContact =
|
||||||
|
_contactDetailsCache[originalSenderId];
|
||||||
|
if (originalSenderContact == null) {
|
||||||
|
_loadContactIfNeeded(
|
||||||
|
originalSenderId,
|
||||||
|
);
|
||||||
|
forwardedFrom =
|
||||||
|
'Участник $originalSenderId';
|
||||||
|
forwardedFromAvatarUrl = null;
|
||||||
|
} else {
|
||||||
|
forwardedFrom =
|
||||||
|
originalSenderContact.name;
|
||||||
|
forwardedFromAvatarUrl =
|
||||||
|
originalSenderContact
|
||||||
|
.photoBaseUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String? senderName;
|
||||||
|
if (widget.isGroupChat && !isMe) {
|
||||||
|
bool shouldShowName = true;
|
||||||
|
if (mappedIndex > 0) {
|
||||||
|
final previousItem =
|
||||||
|
_chatItems[mappedIndex - 1];
|
||||||
|
if (previousItem is MessageItem) {
|
||||||
|
final previousMessage =
|
||||||
|
previousItem.message;
|
||||||
|
if (previousMessage.senderId ==
|
||||||
|
message.senderId) {
|
||||||
|
final timeDifferenceInMinutes =
|
||||||
|
(message.time -
|
||||||
|
previousMessage.time) /
|
||||||
|
(1000 * 60);
|
||||||
|
if (timeDifferenceInMinutes < 5) {
|
||||||
|
shouldShowName = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (shouldShowName) {
|
||||||
|
final senderContact =
|
||||||
|
_contactDetailsCache[message
|
||||||
|
.senderId];
|
||||||
|
if (senderContact != null) {
|
||||||
|
senderName = getContactDisplayName(
|
||||||
|
contactId: senderContact.id,
|
||||||
|
originalName: senderContact.name,
|
||||||
|
originalFirstName:
|
||||||
|
senderContact.firstName,
|
||||||
|
originalLastName:
|
||||||
|
senderContact.lastName,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
senderName = 'ID ${message.senderId}';
|
||||||
|
_loadContactIfNeeded(message.senderId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final hasPhoto = item.message.attaches.any(
|
||||||
|
(a) => a['_type'] == 'PHOTO',
|
||||||
|
);
|
||||||
|
final isNew = !_animatedMessageIds.contains(
|
||||||
|
item.message.id,
|
||||||
|
);
|
||||||
|
final deferImageLoading =
|
||||||
|
hasPhoto &&
|
||||||
|
isNew &&
|
||||||
|
!_anyOptimize &&
|
||||||
|
!context
|
||||||
|
.read<ThemeProvider>()
|
||||||
|
.animatePhotoMessages;
|
||||||
|
|
||||||
|
String? decryptedText;
|
||||||
|
if (_isEncryptionPasswordSetForCurrentChat &&
|
||||||
|
_encryptionConfigForCurrentChat != null &&
|
||||||
|
_encryptionConfigForCurrentChat!
|
||||||
|
.password
|
||||||
|
.isNotEmpty &&
|
||||||
|
item.message.text.startsWith(
|
||||||
|
ChatEncryptionService.encryptedPrefix,
|
||||||
|
)) {
|
||||||
|
decryptedText =
|
||||||
|
ChatEncryptionService.decryptWithPassword(
|
||||||
|
_encryptionConfigForCurrentChat!
|
||||||
|
.password,
|
||||||
|
item.message.text,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final bubble = ChatMessageBubble(
|
||||||
|
key: key,
|
||||||
|
message: item.message,
|
||||||
|
isMe: isMe,
|
||||||
|
readStatus: readStatus,
|
||||||
|
isReactionSending: _sendingReactions
|
||||||
|
.contains(item.message.id),
|
||||||
|
deferImageLoading: deferImageLoading,
|
||||||
|
myUserId: _actualMyId,
|
||||||
|
chatId: widget.chatId,
|
||||||
|
isEncryptionPasswordSet:
|
||||||
|
_isEncryptionPasswordSetForCurrentChat,
|
||||||
|
decryptedText: decryptedText,
|
||||||
|
onReply: widget.isChannel
|
||||||
|
? null
|
||||||
|
: () => _replyToMessage(item.message),
|
||||||
|
onForward: () =>
|
||||||
|
_forwardMessage(item.message),
|
||||||
|
onEdit: isMe
|
||||||
|
? () => _editMessage(item.message)
|
||||||
|
: null,
|
||||||
|
canEditMessage: isMe
|
||||||
|
? item.message.canEdit(_actualMyId!)
|
||||||
|
: null,
|
||||||
|
onDeleteForMe: isMe
|
||||||
|
? () async {
|
||||||
|
await ApiService.instance
|
||||||
|
.deleteMessage(
|
||||||
|
widget.chatId,
|
||||||
|
item.message.id,
|
||||||
|
forMe: true,
|
||||||
|
);
|
||||||
|
widget.onChatUpdated?.call();
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
onDeleteForAll: isMe
|
||||||
|
? () async {
|
||||||
|
await ApiService.instance
|
||||||
|
.deleteMessage(
|
||||||
|
widget.chatId,
|
||||||
|
item.message.id,
|
||||||
|
forMe: false,
|
||||||
|
);
|
||||||
|
widget.onChatUpdated?.call();
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
onReaction: (emoji) {
|
||||||
|
_updateReactionOptimistically(
|
||||||
|
item.message.id,
|
||||||
|
emoji,
|
||||||
|
);
|
||||||
|
ApiService.instance.sendReaction(
|
||||||
|
widget.chatId,
|
||||||
|
item.message.id,
|
||||||
|
emoji,
|
||||||
|
);
|
||||||
|
widget.onChatUpdated?.call();
|
||||||
|
},
|
||||||
|
onRemoveReaction: () {
|
||||||
|
_removeReactionOptimistically(
|
||||||
|
item.message.id,
|
||||||
|
);
|
||||||
|
ApiService.instance.removeReaction(
|
||||||
|
widget.chatId,
|
||||||
|
item.message.id,
|
||||||
|
);
|
||||||
|
widget.onChatUpdated?.call();
|
||||||
|
},
|
||||||
|
isGroupChat: widget.isGroupChat,
|
||||||
|
isChannel: widget.isChannel,
|
||||||
|
senderName: senderName,
|
||||||
|
forwardedFrom: forwardedFrom,
|
||||||
|
forwardedFromAvatarUrl:
|
||||||
|
forwardedFromAvatarUrl,
|
||||||
|
contactDetailsCache: _contactDetailsCache,
|
||||||
|
onReplyTap: _scrollToMessage,
|
||||||
|
useAutoReplyColor: context
|
||||||
|
.read<ThemeProvider>()
|
||||||
|
.useAutoReplyColor,
|
||||||
|
customReplyColor: context
|
||||||
|
.read<ThemeProvider>()
|
||||||
|
.customReplyColor,
|
||||||
|
isFirstInGroup: item.isFirstInGroup,
|
||||||
|
isLastInGroup: item.isLastInGroup,
|
||||||
|
isGrouped: item.isGrouped,
|
||||||
|
avatarVerticalOffset:
|
||||||
|
-8.0, // Смещение аватарки вверх на 8px
|
||||||
|
onComplain: () =>
|
||||||
|
_showComplaintDialog(item.message.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget finalMessageWidget = bubble as Widget;
|
||||||
|
|
||||||
|
if (isHighlighted) {
|
||||||
return TweenAnimationBuilder<double>(
|
return TweenAnimationBuilder<double>(
|
||||||
duration: const Duration(
|
duration: const Duration(
|
||||||
milliseconds: 300,
|
milliseconds: 600,
|
||||||
|
),
|
||||||
|
tween: Tween<double>(
|
||||||
|
begin: 0.3,
|
||||||
|
end: 0.6,
|
||||||
|
),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
builder: (context, value, child) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.symmetric(
|
||||||
|
vertical: 2,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.primaryContainer
|
||||||
|
.withOpacity(value),
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
16,
|
||||||
|
),
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.primary,
|
||||||
|
width: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: finalMessageWidget,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Плавное появление новых сообщений
|
||||||
|
if (isNew && !_anyOptimize) {
|
||||||
|
return TweenAnimationBuilder<double>(
|
||||||
|
duration: const Duration(
|
||||||
|
milliseconds: 400,
|
||||||
),
|
),
|
||||||
tween: Tween<double>(
|
tween: Tween<double>(
|
||||||
begin: 0.0,
|
begin: 0.0,
|
||||||
end: 1.0,
|
end: 1.0,
|
||||||
),
|
),
|
||||||
curve: Curves.easeOut,
|
curve: Curves.easeOutCubic,
|
||||||
builder: (context, value, child) {
|
builder: (context, value, child) {
|
||||||
return Opacity(
|
return Opacity(
|
||||||
opacity: value,
|
opacity: value,
|
||||||
child: Transform.scale(
|
child: Transform.translate(
|
||||||
scale: 0.7 + (0.3 * value),
|
offset: Offset(0, 20 * (1 - value)),
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: const Padding(
|
child: finalMessageWidget,
|
||||||
padding: EdgeInsets.symmetric(
|
|
||||||
vertical: 12,
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return const SizedBox.shrink();
|
|
||||||
},
|
return finalMessageWidget;
|
||||||
),
|
} else if (item is DateSeparatorItem) {
|
||||||
|
return _DateSeparatorChip(date: item.date);
|
||||||
|
}
|
||||||
|
if (isLastVisual && _isLoadingMore) {
|
||||||
|
return TweenAnimationBuilder<double>(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
tween: Tween<double>(begin: 0.0, end: 1.0),
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
builder: (context, value, child) {
|
||||||
|
return Opacity(
|
||||||
|
opacity: value,
|
||||||
|
child: Transform.scale(
|
||||||
|
scale: 0.7 + (0.3 * value),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
|
||||||
AnimatedPositioned(
|
|
||||||
duration: const Duration(milliseconds: 100),
|
|
||||||
curve: Curves.easeOutQuad,
|
|
||||||
right: 16,
|
|
||||||
bottom:
|
|
||||||
MediaQuery.of(context).viewInsets.bottom +
|
|
||||||
MediaQuery.of(context).padding.bottom +
|
|
||||||
100,
|
|
||||||
child: AnimatedScale(
|
|
||||||
duration: const Duration(milliseconds: 200),
|
|
||||||
curve: Curves.easeOutBack,
|
|
||||||
scale: _showScrollToBottomNotifier.value ? 1.0 : 0.0,
|
|
||||||
child: AnimatedOpacity(
|
|
||||||
duration: const Duration(milliseconds: 150),
|
|
||||||
opacity: _showScrollToBottomNotifier.value
|
|
||||||
? 1.0
|
|
||||||
: 0.0,
|
|
||||||
child: FloatingActionButton(
|
|
||||||
mini: true,
|
|
||||||
onPressed: _scrollToBottom,
|
|
||||||
elevation: 4,
|
|
||||||
child: const Icon(Icons.arrow_downward_rounded),
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
AnimatedPositioned(
|
||||||
|
duration: const Duration(milliseconds: 100),
|
||||||
|
curve: Curves.easeOutQuad,
|
||||||
|
right: 16,
|
||||||
|
bottom:
|
||||||
|
MediaQuery.of(context).viewInsets.bottom +
|
||||||
|
MediaQuery.of(context).padding.bottom +
|
||||||
|
100,
|
||||||
|
child: AnimatedScale(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
curve: Curves.easeOutBack,
|
||||||
|
scale: _showScrollToBottomNotifier.value ? 1.0 : 0.0,
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
duration: const Duration(milliseconds: 150),
|
||||||
|
opacity: _showScrollToBottomNotifier.value ? 1.0 : 0.0,
|
||||||
|
child: FloatingActionButton(
|
||||||
|
mini: true,
|
||||||
|
onPressed: _scrollToBottom,
|
||||||
|
elevation: 4,
|
||||||
|
child: const Icon(Icons.arrow_downward_rounded),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
AnimatedPositioned(
|
),
|
||||||
duration: const Duration(milliseconds: 100),
|
AnimatedPositioned(
|
||||||
curve: Curves.easeOutQuad,
|
duration: const Duration(milliseconds: 100),
|
||||||
left: 8,
|
curve: Curves.easeOutQuad,
|
||||||
right: 8,
|
left: 8,
|
||||||
bottom:
|
right: 8,
|
||||||
MediaQuery.of(context).viewInsets.bottom +
|
bottom:
|
||||||
MediaQuery.of(context).padding.bottom +
|
MediaQuery.of(context).viewInsets.bottom +
|
||||||
12,
|
MediaQuery.of(context).padding.bottom +
|
||||||
child: _buildTextInput(),
|
12,
|
||||||
),
|
child: _buildTextInput(),
|
||||||
],
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isDesktop) {
|
||||||
|
return Scaffold(
|
||||||
|
extendBodyBehindAppBar: theme.useGlassPanels,
|
||||||
|
resizeToAvoidBottomInset: false,
|
||||||
|
appBar: _buildAppBar(),
|
||||||
|
body: body,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onHorizontalDragEnd: (details) {
|
||||||
|
final velocity = details.primaryVelocity ?? 0;
|
||||||
|
if (velocity > 400) {
|
||||||
|
Navigator.of(context).maybePop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Scaffold(
|
||||||
|
extendBodyBehindAppBar: theme.useGlassPanels,
|
||||||
|
resizeToAvoidBottomInset: false,
|
||||||
|
appBar: _buildAppBar(),
|
||||||
|
body: body,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1997,13 +1997,63 @@ class _ChatsScreenState extends State<ChatsScreen>
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final themeProvider = context.watch<ThemeProvider>();
|
||||||
|
Widget? chatsListBackground;
|
||||||
|
if (themeProvider.useExperimentalChatsListBackground) {
|
||||||
|
switch (themeProvider.experimentalChatsListBackgroundType) {
|
||||||
|
case ChatWallpaperType.solid:
|
||||||
|
chatsListBackground = Container(color: themeProvider.experimentalChatsListBackgroundColor1);
|
||||||
|
break;
|
||||||
|
case ChatWallpaperType.gradient:
|
||||||
|
chatsListBackground = Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
themeProvider.experimentalChatsListBackgroundColor1,
|
||||||
|
themeProvider.experimentalChatsListBackgroundColor2,
|
||||||
|
],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case ChatWallpaperType.image:
|
||||||
|
if (themeProvider.experimentalChatsListBackgroundImagePath != null) {
|
||||||
|
chatsListBackground = Image.file(
|
||||||
|
File(themeProvider.experimentalChatsListBackgroundImagePath!),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ChatWallpaperType.video:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final bodyContentWithBackground = chatsListBackground != null
|
||||||
|
? Stack(
|
||||||
|
children: [
|
||||||
|
Positioned.fill(child: chatsListBackground),
|
||||||
|
bodyContent,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: bodyContent;
|
||||||
|
|
||||||
if (widget.hasScaffold) {
|
if (widget.hasScaffold) {
|
||||||
return Builder(
|
return Builder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return Scaffold(
|
final platform = Theme.of(context).platform;
|
||||||
|
final isMobile =
|
||||||
|
platform == TargetPlatform.android ||
|
||||||
|
platform == TargetPlatform.iOS;
|
||||||
|
|
||||||
|
Widget scaffold = Scaffold(
|
||||||
appBar: _buildAppBar(context),
|
appBar: _buildAppBar(context),
|
||||||
drawer: _buildAppDrawer(context),
|
drawer: _buildAppDrawer(context),
|
||||||
body: Row(children: [Expanded(child: bodyContent)]),
|
body: Row(children: [Expanded(child: bodyContentWithBackground)]),
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_showAddMenu(context);
|
_showAddMenu(context);
|
||||||
@@ -2013,10 +2063,50 @@ class _ChatsScreenState extends State<ChatsScreen>
|
|||||||
child: const Icon(Icons.edit),
|
child: const Icon(Icons.edit),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!isMobile) return scaffold;
|
||||||
|
|
||||||
|
final scaffoldKey = GlobalKey<ScaffoldState>();
|
||||||
|
scaffold = Scaffold(
|
||||||
|
key: scaffoldKey,
|
||||||
|
appBar: _buildAppBar(context),
|
||||||
|
drawer: _buildAppDrawer(context),
|
||||||
|
body: GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onHorizontalDragEnd: (details) {
|
||||||
|
final velocity = details.primaryVelocity ?? 0;
|
||||||
|
// Делаем жест проще: реагируем на более медленный свайп
|
||||||
|
if (velocity < -150) {
|
||||||
|
if (_folderTabController.length > 0 &&
|
||||||
|
_folderTabController.index ==
|
||||||
|
_folderTabController.length - 1) {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (ctx) => const SettingsScreen(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
scaffoldKey.currentState?.openDrawer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Row(children: [Expanded(child: bodyContentWithBackground)]),
|
||||||
|
),
|
||||||
|
floatingActionButton: FloatingActionButton(
|
||||||
|
onPressed: () {
|
||||||
|
_showAddMenu(context);
|
||||||
|
},
|
||||||
|
tooltip: 'Создать',
|
||||||
|
heroTag: 'create_menu',
|
||||||
|
child: const Icon(Icons.edit),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return scaffold;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return bodyContent;
|
return bodyContentWithBackground;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2026,6 +2116,49 @@ class _ChatsScreenState extends State<ChatsScreen>
|
|||||||
final themeProvider = context.watch<ThemeProvider>();
|
final themeProvider = context.watch<ThemeProvider>();
|
||||||
final isDarkMode = themeProvider.themeMode == ThemeMode.dark;
|
final isDarkMode = themeProvider.themeMode == ThemeMode.dark;
|
||||||
|
|
||||||
|
Widget? _buildBackgroundWidget(ChatWallpaperType type, Color color1, Color color2, String? imagePath) {
|
||||||
|
switch (type) {
|
||||||
|
case ChatWallpaperType.solid:
|
||||||
|
return Container(color: color1);
|
||||||
|
case ChatWallpaperType.gradient:
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [color1, color2],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
case ChatWallpaperType.image:
|
||||||
|
if (imagePath != null) {
|
||||||
|
return Image.file(
|
||||||
|
File(imagePath),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
case ChatWallpaperType.video:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final drawerTopBackground = _buildBackgroundWidget(
|
||||||
|
themeProvider.drawerTopBackgroundType,
|
||||||
|
themeProvider.drawerTopBackgroundColor1,
|
||||||
|
themeProvider.drawerTopBackgroundColor2,
|
||||||
|
themeProvider.drawerTopBackgroundImagePath,
|
||||||
|
);
|
||||||
|
|
||||||
|
final drawerBottomBackground = _buildBackgroundWidget(
|
||||||
|
themeProvider.drawerBottomBackgroundType,
|
||||||
|
themeProvider.drawerBottomBackgroundColor1,
|
||||||
|
themeProvider.drawerBottomBackgroundColor2,
|
||||||
|
themeProvider.drawerBottomBackgroundImagePath,
|
||||||
|
);
|
||||||
|
|
||||||
return Drawer(
|
return Drawer(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
@@ -2038,132 +2171,136 @@ class _ChatsScreenState extends State<ChatsScreen>
|
|||||||
final currentAccount = accountManager.currentAccount;
|
final currentAccount = accountManager.currentAccount;
|
||||||
final hasMultipleAccounts = accounts.length > 1;
|
final hasMultipleAccounts = accounts.length > 1;
|
||||||
|
|
||||||
return Column(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
if (drawerTopBackground != null)
|
||||||
width: double.infinity,
|
Positioned.fill(child: drawerTopBackground),
|
||||||
padding: EdgeInsets.only(
|
Column(
|
||||||
top: MediaQuery.of(context).padding.top + 16.0,
|
children: [
|
||||||
left: 16.0,
|
Container(
|
||||||
right: 16.0,
|
width: double.infinity,
|
||||||
bottom: 16.0,
|
padding: EdgeInsets.only(
|
||||||
),
|
top: MediaQuery.of(context).padding.top + 16.0,
|
||||||
decoration: BoxDecoration(color: colors.primaryContainer),
|
left: 16.0,
|
||||||
child: Column(
|
right: 16.0,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
bottom: 16.0,
|
||||||
children: [
|
),
|
||||||
Row(
|
decoration: BoxDecoration(color: drawerTopBackground != null ? Colors.transparent : colors.primaryContainer),
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
CircleAvatar(
|
Row(
|
||||||
radius: 30, // Чуть крупнее
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
backgroundColor: colors.primary,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
backgroundImage:
|
|
||||||
_isProfileLoading ||
|
|
||||||
_myProfile?.photoBaseUrl == null
|
|
||||||
? null
|
|
||||||
: NetworkImage(_myProfile!.photoBaseUrl!),
|
|
||||||
child: _isProfileLoading
|
|
||||||
? const SizedBox(
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: (_myProfile?.photoBaseUrl == null
|
|
||||||
? Text(
|
|
||||||
_myProfile
|
|
||||||
?.displayName
|
|
||||||
.isNotEmpty ==
|
|
||||||
true
|
|
||||||
? _myProfile!.displayName[0]
|
|
||||||
.toUpperCase()
|
|
||||||
: '?',
|
|
||||||
style: TextStyle(
|
|
||||||
color: colors.onPrimary,
|
|
||||||
fontSize: 28, // Крупнее
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: null),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
isDarkMode
|
|
||||||
? Icons.brightness_7
|
|
||||||
: Icons.brightness_4, // Солнце / Луна
|
|
||||||
color: colors.onPrimaryContainer,
|
|
||||||
size: 26,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
themeProvider.toggleTheme();
|
|
||||||
},
|
|
||||||
tooltip: isDarkMode
|
|
||||||
? 'Светлая тема'
|
|
||||||
: 'Темная тема',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
|
|
||||||
Text(
|
|
||||||
_myProfile?.displayName ?? 'Загрузка...',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: colors.onPrimaryContainer,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
_myProfile?.formattedPhone ?? '',
|
|
||||||
style: TextStyle(
|
|
||||||
color: colors.onPrimaryContainer.withOpacity(
|
|
||||||
0.8,
|
|
||||||
),
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
InkWell(
|
|
||||||
onTap: () {
|
|
||||||
setState(() {
|
|
||||||
_isAccountsExpanded = !_isAccountsExpanded;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 8.0),
|
|
||||||
child: Icon(
|
|
||||||
_isAccountsExpanded
|
|
||||||
? Icons.expand_less
|
|
||||||
: Icons.expand_more,
|
|
||||||
color: colors.onPrimaryContainer,
|
|
||||||
size: 24,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
ClipRect(
|
|
||||||
child: AnimatedSize(
|
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
curve: Curves.easeInOutCubic,
|
|
||||||
child: _isAccountsExpanded
|
|
||||||
? Column(
|
|
||||||
children: [
|
children: [
|
||||||
if (hasMultipleAccounts)
|
CircleAvatar(
|
||||||
...accounts.map((account) {
|
radius: 30, // Чуть крупнее
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
backgroundImage:
|
||||||
|
_isProfileLoading ||
|
||||||
|
_myProfile?.photoBaseUrl == null
|
||||||
|
? null
|
||||||
|
: NetworkImage(_myProfile!.photoBaseUrl!),
|
||||||
|
child: _isProfileLoading
|
||||||
|
? const SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: (_myProfile?.photoBaseUrl == null
|
||||||
|
? Text(
|
||||||
|
_myProfile
|
||||||
|
?.displayName
|
||||||
|
.isNotEmpty ==
|
||||||
|
true
|
||||||
|
? _myProfile!.displayName[0]
|
||||||
|
.toUpperCase()
|
||||||
|
: '?',
|
||||||
|
style: TextStyle(
|
||||||
|
color: colors.onPrimary,
|
||||||
|
fontSize: 28, // Крупнее
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
isDarkMode
|
||||||
|
? Icons.brightness_7
|
||||||
|
: Icons.brightness_4, // Солнце / Луна
|
||||||
|
color: colors.onPrimaryContainer,
|
||||||
|
size: 26,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
themeProvider.toggleTheme();
|
||||||
|
},
|
||||||
|
tooltip: isDarkMode
|
||||||
|
? 'Светлая тема'
|
||||||
|
: 'Темная тема',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
Text(
|
||||||
|
_myProfile?.displayName ?? 'Загрузка...',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: colors.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
_myProfile?.formattedPhone ?? '',
|
||||||
|
style: TextStyle(
|
||||||
|
color: colors.onPrimaryContainer.withOpacity(
|
||||||
|
0.8,
|
||||||
|
),
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
InkWell(
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
_isAccountsExpanded = !_isAccountsExpanded;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 8.0),
|
||||||
|
child: Icon(
|
||||||
|
_isAccountsExpanded
|
||||||
|
? Icons.expand_less
|
||||||
|
: Icons.expand_more,
|
||||||
|
color: colors.onPrimaryContainer,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
ClipRect(
|
||||||
|
child: AnimatedSize(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
curve: Curves.easeInOutCubic,
|
||||||
|
child: _isAccountsExpanded
|
||||||
|
? Column(
|
||||||
|
children: [
|
||||||
|
if (hasMultipleAccounts)
|
||||||
|
...accounts.map((account) {
|
||||||
final isCurrent =
|
final isCurrent =
|
||||||
account.id == currentAccount?.id;
|
account.id == currentAccount?.id;
|
||||||
return ListTile(
|
return ListTile(
|
||||||
@@ -2289,16 +2426,22 @@ class _ChatsScreenState extends State<ChatsScreen>
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
: const SizedBox.shrink(),
|
: const SizedBox.shrink(),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
_buildAccountsSection(context, colors),
|
if (drawerBottomBackground != null)
|
||||||
|
Positioned.fill(child: drawerBottomBackground),
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
_buildAccountsSection(context, colors),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.person_outline),
|
leading: const Icon(Icons.person_outline),
|
||||||
title: const Text('Мой профиль'),
|
title: const Text('Мой профиль'),
|
||||||
@@ -2441,6 +2584,8 @@ class _ChatsScreenState extends State<ChatsScreen>
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8), // Небольшой отступ снизу
|
const SizedBox(height: 8), // Небольшой отступ снизу
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -535,6 +535,37 @@ class _CustomizationScreenState extends State<CustomizationScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
_ExpandableSection(
|
||||||
|
title: "Отступ внизу чата",
|
||||||
|
initiallyExpanded: false,
|
||||||
|
children: [
|
||||||
|
_SliderTile(
|
||||||
|
icon: Icons.vertical_align_bottom,
|
||||||
|
label: "Доп. отступ снизу (мобилы)",
|
||||||
|
value: theme.mobileChatBottomPadding,
|
||||||
|
min: 60,
|
||||||
|
max: 240,
|
||||||
|
divisions: 18,
|
||||||
|
onChanged: (value) =>
|
||||||
|
theme.setMobileChatBottomPadding(value),
|
||||||
|
displayValue:
|
||||||
|
"${theme.mobileChatBottomPadding.toStringAsFixed(0)} px",
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_CustomSettingTile(
|
||||||
|
icon: Icons.keyboard,
|
||||||
|
title: "Убирать отступ при открытой клавиатуре",
|
||||||
|
subtitle:
|
||||||
|
"Только Android. Когда вводите текст — отступ снизу не добавляется",
|
||||||
|
child: Switch(
|
||||||
|
value: theme.ignoreMobileBottomPaddingWhenKeyboard,
|
||||||
|
onChanged: (value) => theme
|
||||||
|
.setIgnoreMobileBottomPaddingWhenKeyboard(value),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
// Развернуть настройки
|
// Развернуть настройки
|
||||||
_ExpandableSection(
|
_ExpandableSection(
|
||||||
@@ -582,6 +613,296 @@ class _CustomizationScreenState extends State<CustomizationScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_ModernSection(
|
||||||
|
title: "Экспериментальные настройки фона",
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.errorContainer.withOpacity(0.3),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(
|
||||||
|
color: colors.error.withOpacity(0.5),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.science, color: colors.error, size: 20),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
"Экспериментальные функции. Могут работать нестабильно.",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: colors.onErrorContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_ExpandableSection(
|
||||||
|
title: "Фон списка чатов",
|
||||||
|
initiallyExpanded: false,
|
||||||
|
children: [
|
||||||
|
_CustomSettingTile(
|
||||||
|
icon: Icons.science,
|
||||||
|
title: "Использовать экспериментальный фон",
|
||||||
|
subtitle: "Фон для экрана со списком чатов",
|
||||||
|
child: Switch(
|
||||||
|
value: theme.useExperimentalChatsListBackground,
|
||||||
|
onChanged: (value) =>
|
||||||
|
theme.setUseExperimentalChatsListBackground(value),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (theme.useExperimentalChatsListBackground) ...[
|
||||||
|
const Divider(height: 24),
|
||||||
|
_CustomSettingTile(
|
||||||
|
icon: Icons.image,
|
||||||
|
title: "Тип фона",
|
||||||
|
child: DropdownButton<ChatWallpaperType>(
|
||||||
|
value: theme.experimentalChatsListBackgroundType,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
theme.setExperimentalChatsListBackgroundType(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
ChatWallpaperType.solid,
|
||||||
|
ChatWallpaperType.gradient,
|
||||||
|
ChatWallpaperType.image,
|
||||||
|
].map((type) {
|
||||||
|
return DropdownMenuItem(
|
||||||
|
value: type,
|
||||||
|
child: Text(type.displayName),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (theme.experimentalChatsListBackgroundType ==
|
||||||
|
ChatWallpaperType.solid ||
|
||||||
|
theme.experimentalChatsListBackgroundType ==
|
||||||
|
ChatWallpaperType.gradient) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_ColorPickerTile(
|
||||||
|
title: "Цвет 1",
|
||||||
|
subtitle: "Основной цвет",
|
||||||
|
color: theme.experimentalChatsListBackgroundColor1,
|
||||||
|
onColorChanged: (color) =>
|
||||||
|
theme.setExperimentalChatsListBackgroundColor1(color),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (theme.experimentalChatsListBackgroundType ==
|
||||||
|
ChatWallpaperType.gradient) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_ColorPickerTile(
|
||||||
|
title: "Цвет 2",
|
||||||
|
subtitle: "Дополнительный цвет для градиента",
|
||||||
|
color: theme.experimentalChatsListBackgroundColor2,
|
||||||
|
onColorChanged: (color) =>
|
||||||
|
theme.setExperimentalChatsListBackgroundColor2(color),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (theme.experimentalChatsListBackgroundType ==
|
||||||
|
ChatWallpaperType.image) ...[
|
||||||
|
const Divider(height: 24),
|
||||||
|
ListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
leading: const Icon(Icons.photo_library_outlined),
|
||||||
|
title: const Text("Выбрать изображение"),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () async {
|
||||||
|
final picker = ImagePicker();
|
||||||
|
final image = await picker.pickImage(
|
||||||
|
source: ImageSource.gallery,
|
||||||
|
);
|
||||||
|
if (image != null) {
|
||||||
|
theme.setExperimentalChatsListBackgroundImagePath(
|
||||||
|
image.path,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (theme.experimentalChatsListBackgroundImagePath
|
||||||
|
?.isNotEmpty ==
|
||||||
|
true) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
ListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
leading: const Icon(Icons.delete_outline),
|
||||||
|
title: const Text("Удалить изображение"),
|
||||||
|
onTap: () {
|
||||||
|
theme.setExperimentalChatsListBackgroundImagePath(null);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_ExpandableSection(
|
||||||
|
title: "Фон боковой панели - верхняя часть (профиль)",
|
||||||
|
initiallyExpanded: false,
|
||||||
|
children: [
|
||||||
|
_CustomSettingTile(
|
||||||
|
icon: Icons.image,
|
||||||
|
title: "Тип фона",
|
||||||
|
child: DropdownButton<ChatWallpaperType>(
|
||||||
|
value: theme.drawerTopBackgroundType,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
theme.setDrawerTopBackgroundType(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
ChatWallpaperType.solid,
|
||||||
|
ChatWallpaperType.gradient,
|
||||||
|
ChatWallpaperType.image,
|
||||||
|
].map((type) {
|
||||||
|
return DropdownMenuItem(
|
||||||
|
value: type,
|
||||||
|
child: Text(type.displayName),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (theme.drawerTopBackgroundType == ChatWallpaperType.solid ||
|
||||||
|
theme.drawerTopBackgroundType == ChatWallpaperType.gradient) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_ColorPickerTile(
|
||||||
|
title: "Цвет 1",
|
||||||
|
subtitle: "Основной цвет",
|
||||||
|
color: theme.drawerTopBackgroundColor1,
|
||||||
|
onColorChanged: (color) =>
|
||||||
|
theme.setDrawerTopBackgroundColor1(color),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (theme.drawerTopBackgroundType == ChatWallpaperType.gradient) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_ColorPickerTile(
|
||||||
|
title: "Цвет 2",
|
||||||
|
subtitle: "Дополнительный цвет для градиента",
|
||||||
|
color: theme.drawerTopBackgroundColor2,
|
||||||
|
onColorChanged: (color) =>
|
||||||
|
theme.setDrawerTopBackgroundColor2(color),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (theme.drawerTopBackgroundType == ChatWallpaperType.image) ...[
|
||||||
|
const Divider(height: 24),
|
||||||
|
ListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
leading: const Icon(Icons.photo_library_outlined),
|
||||||
|
title: const Text("Выбрать изображение"),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () async {
|
||||||
|
final picker = ImagePicker();
|
||||||
|
final image = await picker.pickImage(
|
||||||
|
source: ImageSource.gallery,
|
||||||
|
);
|
||||||
|
if (image != null) {
|
||||||
|
theme.setDrawerTopBackgroundImagePath(image.path);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (theme.drawerTopBackgroundImagePath?.isNotEmpty == true) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
ListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
leading: const Icon(Icons.delete_outline),
|
||||||
|
title: const Text("Удалить изображение"),
|
||||||
|
onTap: () {
|
||||||
|
theme.setDrawerTopBackgroundImagePath(null);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_ExpandableSection(
|
||||||
|
title: "Фон боковой панели - нижняя часть (меню)",
|
||||||
|
initiallyExpanded: false,
|
||||||
|
children: [
|
||||||
|
_CustomSettingTile(
|
||||||
|
icon: Icons.image,
|
||||||
|
title: "Тип фона",
|
||||||
|
child: DropdownButton<ChatWallpaperType>(
|
||||||
|
value: theme.drawerBottomBackgroundType,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
theme.setDrawerBottomBackgroundType(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
ChatWallpaperType.solid,
|
||||||
|
ChatWallpaperType.gradient,
|
||||||
|
ChatWallpaperType.image,
|
||||||
|
].map((type) {
|
||||||
|
return DropdownMenuItem(
|
||||||
|
value: type,
|
||||||
|
child: Text(type.displayName),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (theme.drawerBottomBackgroundType == ChatWallpaperType.solid ||
|
||||||
|
theme.drawerBottomBackgroundType == ChatWallpaperType.gradient) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_ColorPickerTile(
|
||||||
|
title: "Цвет 1",
|
||||||
|
subtitle: "Основной цвет",
|
||||||
|
color: theme.drawerBottomBackgroundColor1,
|
||||||
|
onColorChanged: (color) =>
|
||||||
|
theme.setDrawerBottomBackgroundColor1(color),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (theme.drawerBottomBackgroundType == ChatWallpaperType.gradient) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_ColorPickerTile(
|
||||||
|
title: "Цвет 2",
|
||||||
|
subtitle: "Дополнительный цвет для градиента",
|
||||||
|
color: theme.drawerBottomBackgroundColor2,
|
||||||
|
onColorChanged: (color) =>
|
||||||
|
theme.setDrawerBottomBackgroundColor2(color),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (theme.drawerBottomBackgroundType == ChatWallpaperType.image) ...[
|
||||||
|
const Divider(height: 24),
|
||||||
|
ListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
leading: const Icon(Icons.photo_library_outlined),
|
||||||
|
title: const Text("Выбрать изображение"),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () async {
|
||||||
|
final picker = ImagePicker();
|
||||||
|
final image = await picker.pickImage(
|
||||||
|
source: ImageSource.gallery,
|
||||||
|
);
|
||||||
|
if (image != null) {
|
||||||
|
theme.setDrawerBottomBackgroundImagePath(image.path);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (theme.drawerBottomBackgroundImagePath?.isNotEmpty == true) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
ListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
leading: const Icon(Icons.delete_outline),
|
||||||
|
title: const Text("Удалить изображение"),
|
||||||
|
onTap: () {
|
||||||
|
theme.setDrawerBottomBackgroundImagePath(null);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -104,6 +104,25 @@ class CustomThemePreset {
|
|||||||
bool useDesktopLayout;
|
bool useDesktopLayout;
|
||||||
bool useAutoReplyColor;
|
bool useAutoReplyColor;
|
||||||
Color? customReplyColor;
|
Color? customReplyColor;
|
||||||
|
double mobileChatBottomPadding;
|
||||||
|
bool ignoreMobileBottomPaddingWhenKeyboard;
|
||||||
|
|
||||||
|
// Экспериментальные настройки фона
|
||||||
|
bool useExperimentalChatsListBackground;
|
||||||
|
ChatWallpaperType experimentalChatsListBackgroundType;
|
||||||
|
Color experimentalChatsListBackgroundColor1;
|
||||||
|
Color experimentalChatsListBackgroundColor2;
|
||||||
|
String? experimentalChatsListBackgroundImagePath;
|
||||||
|
|
||||||
|
ChatWallpaperType drawerTopBackgroundType;
|
||||||
|
Color drawerTopBackgroundColor1;
|
||||||
|
Color drawerTopBackgroundColor2;
|
||||||
|
String? drawerTopBackgroundImagePath;
|
||||||
|
|
||||||
|
ChatWallpaperType drawerBottomBackgroundType;
|
||||||
|
Color drawerBottomBackgroundColor1;
|
||||||
|
Color drawerBottomBackgroundColor2;
|
||||||
|
String? drawerBottomBackgroundImagePath;
|
||||||
|
|
||||||
CustomThemePreset({
|
CustomThemePreset({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -155,6 +174,21 @@ class CustomThemePreset {
|
|||||||
this.useDesktopLayout = true,
|
this.useDesktopLayout = true,
|
||||||
this.useAutoReplyColor = true,
|
this.useAutoReplyColor = true,
|
||||||
this.customReplyColor,
|
this.customReplyColor,
|
||||||
|
this.mobileChatBottomPadding = 140.0,
|
||||||
|
this.ignoreMobileBottomPaddingWhenKeyboard = true,
|
||||||
|
this.useExperimentalChatsListBackground = false,
|
||||||
|
this.experimentalChatsListBackgroundType = ChatWallpaperType.solid,
|
||||||
|
this.experimentalChatsListBackgroundColor1 = const Color(0xFF101010),
|
||||||
|
this.experimentalChatsListBackgroundColor2 = const Color(0xFF202020),
|
||||||
|
this.experimentalChatsListBackgroundImagePath,
|
||||||
|
this.drawerTopBackgroundType = ChatWallpaperType.solid,
|
||||||
|
this.drawerTopBackgroundColor1 = const Color(0xFF1E1E1E),
|
||||||
|
this.drawerTopBackgroundColor2 = const Color(0xFF2E2E2E),
|
||||||
|
this.drawerTopBackgroundImagePath,
|
||||||
|
this.drawerBottomBackgroundType = ChatWallpaperType.solid,
|
||||||
|
this.drawerBottomBackgroundColor1 = const Color(0xFF1E1E1E),
|
||||||
|
this.drawerBottomBackgroundColor2 = const Color(0xFF2E2E2E),
|
||||||
|
this.drawerBottomBackgroundImagePath,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory CustomThemePreset.createDefault() {
|
factory CustomThemePreset.createDefault() {
|
||||||
@@ -211,6 +245,21 @@ class CustomThemePreset {
|
|||||||
bool? useDesktopLayout,
|
bool? useDesktopLayout,
|
||||||
bool? useAutoReplyColor,
|
bool? useAutoReplyColor,
|
||||||
Color? customReplyColor,
|
Color? customReplyColor,
|
||||||
|
double? mobileChatBottomPadding,
|
||||||
|
bool? ignoreMobileBottomPaddingWhenKeyboard,
|
||||||
|
bool? useExperimentalChatsListBackground,
|
||||||
|
ChatWallpaperType? experimentalChatsListBackgroundType,
|
||||||
|
Color? experimentalChatsListBackgroundColor1,
|
||||||
|
Color? experimentalChatsListBackgroundColor2,
|
||||||
|
String? experimentalChatsListBackgroundImagePath,
|
||||||
|
ChatWallpaperType? drawerTopBackgroundType,
|
||||||
|
Color? drawerTopBackgroundColor1,
|
||||||
|
Color? drawerTopBackgroundColor2,
|
||||||
|
String? drawerTopBackgroundImagePath,
|
||||||
|
ChatWallpaperType? drawerBottomBackgroundType,
|
||||||
|
Color? drawerBottomBackgroundColor1,
|
||||||
|
Color? drawerBottomBackgroundColor2,
|
||||||
|
String? drawerBottomBackgroundImagePath,
|
||||||
}) {
|
}) {
|
||||||
return CustomThemePreset(
|
return CustomThemePreset(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
@@ -271,6 +320,35 @@ class CustomThemePreset {
|
|||||||
useDesktopLayout: useDesktopLayout ?? this.useDesktopLayout,
|
useDesktopLayout: useDesktopLayout ?? this.useDesktopLayout,
|
||||||
useAutoReplyColor: useAutoReplyColor ?? this.useAutoReplyColor,
|
useAutoReplyColor: useAutoReplyColor ?? this.useAutoReplyColor,
|
||||||
customReplyColor: customReplyColor ?? this.customReplyColor,
|
customReplyColor: customReplyColor ?? this.customReplyColor,
|
||||||
|
mobileChatBottomPadding:
|
||||||
|
mobileChatBottomPadding ?? this.mobileChatBottomPadding,
|
||||||
|
ignoreMobileBottomPaddingWhenKeyboard:
|
||||||
|
ignoreMobileBottomPaddingWhenKeyboard ??
|
||||||
|
this.ignoreMobileBottomPaddingWhenKeyboard,
|
||||||
|
useExperimentalChatsListBackground: useExperimentalChatsListBackground ??
|
||||||
|
this.useExperimentalChatsListBackground,
|
||||||
|
experimentalChatsListBackgroundType: experimentalChatsListBackgroundType ??
|
||||||
|
this.experimentalChatsListBackgroundType,
|
||||||
|
experimentalChatsListBackgroundColor1: experimentalChatsListBackgroundColor1 ??
|
||||||
|
this.experimentalChatsListBackgroundColor1,
|
||||||
|
experimentalChatsListBackgroundColor2: experimentalChatsListBackgroundColor2 ??
|
||||||
|
this.experimentalChatsListBackgroundColor2,
|
||||||
|
experimentalChatsListBackgroundImagePath: experimentalChatsListBackgroundImagePath ??
|
||||||
|
this.experimentalChatsListBackgroundImagePath,
|
||||||
|
drawerTopBackgroundType: drawerTopBackgroundType ?? this.drawerTopBackgroundType,
|
||||||
|
drawerTopBackgroundColor1:
|
||||||
|
drawerTopBackgroundColor1 ?? this.drawerTopBackgroundColor1,
|
||||||
|
drawerTopBackgroundColor2:
|
||||||
|
drawerTopBackgroundColor2 ?? this.drawerTopBackgroundColor2,
|
||||||
|
drawerTopBackgroundImagePath:
|
||||||
|
drawerTopBackgroundImagePath ?? this.drawerTopBackgroundImagePath,
|
||||||
|
drawerBottomBackgroundType: drawerBottomBackgroundType ?? this.drawerBottomBackgroundType,
|
||||||
|
drawerBottomBackgroundColor1:
|
||||||
|
drawerBottomBackgroundColor1 ?? this.drawerBottomBackgroundColor1,
|
||||||
|
drawerBottomBackgroundColor2:
|
||||||
|
drawerBottomBackgroundColor2 ?? this.drawerBottomBackgroundColor2,
|
||||||
|
drawerBottomBackgroundImagePath:
|
||||||
|
drawerBottomBackgroundImagePath ?? this.drawerBottomBackgroundImagePath,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,6 +403,22 @@ class CustomThemePreset {
|
|||||||
'useDesktopLayout': useDesktopLayout,
|
'useDesktopLayout': useDesktopLayout,
|
||||||
'useAutoReplyColor': useAutoReplyColor,
|
'useAutoReplyColor': useAutoReplyColor,
|
||||||
'customReplyColor': customReplyColor?.value,
|
'customReplyColor': customReplyColor?.value,
|
||||||
|
'mobileChatBottomPadding': mobileChatBottomPadding,
|
||||||
|
'ignoreMobileBottomPaddingWhenKeyboard':
|
||||||
|
ignoreMobileBottomPaddingWhenKeyboard,
|
||||||
|
'useExperimentalChatsListBackground': useExperimentalChatsListBackground,
|
||||||
|
'experimentalChatsListBackgroundType': experimentalChatsListBackgroundType.index,
|
||||||
|
'experimentalChatsListBackgroundColor1': experimentalChatsListBackgroundColor1.value,
|
||||||
|
'experimentalChatsListBackgroundColor2': experimentalChatsListBackgroundColor2.value,
|
||||||
|
'experimentalChatsListBackgroundImagePath': experimentalChatsListBackgroundImagePath,
|
||||||
|
'drawerTopBackgroundType': drawerTopBackgroundType.index,
|
||||||
|
'drawerTopBackgroundColor1': drawerTopBackgroundColor1.value,
|
||||||
|
'drawerTopBackgroundColor2': drawerTopBackgroundColor2.value,
|
||||||
|
'drawerTopBackgroundImagePath': drawerTopBackgroundImagePath,
|
||||||
|
'drawerBottomBackgroundType': drawerBottomBackgroundType.index,
|
||||||
|
'drawerBottomBackgroundColor1': drawerBottomBackgroundColor1.value,
|
||||||
|
'drawerBottomBackgroundColor2': drawerBottomBackgroundColor2.value,
|
||||||
|
'drawerBottomBackgroundImagePath': drawerBottomBackgroundImagePath,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,6 +516,44 @@ class CustomThemePreset {
|
|||||||
customReplyColor: json['customReplyColor'] != null
|
customReplyColor: json['customReplyColor'] != null
|
||||||
? Color(json['customReplyColor'] as int)
|
? Color(json['customReplyColor'] as int)
|
||||||
: null,
|
: null,
|
||||||
|
mobileChatBottomPadding:
|
||||||
|
(json['mobileChatBottomPadding'] as double? ?? 140.0).clamp(
|
||||||
|
60.0,
|
||||||
|
240.0,
|
||||||
|
),
|
||||||
|
ignoreMobileBottomPaddingWhenKeyboard:
|
||||||
|
json['ignoreMobileBottomPaddingWhenKeyboard'] as bool? ?? true,
|
||||||
|
useExperimentalChatsListBackground: json['useExperimentalChatsListBackground'] as bool? ?? false,
|
||||||
|
experimentalChatsListBackgroundType: () {
|
||||||
|
final index = json['experimentalChatsListBackgroundType'] as int?;
|
||||||
|
if (index == null || index < 0 || index >= ChatWallpaperType.values.length) {
|
||||||
|
return ChatWallpaperType.solid;
|
||||||
|
}
|
||||||
|
return ChatWallpaperType.values[index];
|
||||||
|
}(),
|
||||||
|
experimentalChatsListBackgroundColor1: Color(json['experimentalChatsListBackgroundColor1'] as int? ?? const Color(0xFF101010).value),
|
||||||
|
experimentalChatsListBackgroundColor2: Color(json['experimentalChatsListBackgroundColor2'] as int? ?? const Color(0xFF202020).value),
|
||||||
|
experimentalChatsListBackgroundImagePath: json['experimentalChatsListBackgroundImagePath'] as String?,
|
||||||
|
drawerTopBackgroundType: () {
|
||||||
|
final index = json['drawerTopBackgroundType'] as int?;
|
||||||
|
if (index == null || index < 0 || index >= ChatWallpaperType.values.length) {
|
||||||
|
return ChatWallpaperType.solid;
|
||||||
|
}
|
||||||
|
return ChatWallpaperType.values[index];
|
||||||
|
}(),
|
||||||
|
drawerTopBackgroundColor1: Color(json['drawerTopBackgroundColor1'] as int? ?? const Color(0xFF1E1E1E).value),
|
||||||
|
drawerTopBackgroundColor2: Color(json['drawerTopBackgroundColor2'] as int? ?? const Color(0xFF2E2E2E).value),
|
||||||
|
drawerTopBackgroundImagePath: json['drawerTopBackgroundImagePath'] as String?,
|
||||||
|
drawerBottomBackgroundType: () {
|
||||||
|
final index = json['drawerBottomBackgroundType'] as int?;
|
||||||
|
if (index == null || index < 0 || index >= ChatWallpaperType.values.length) {
|
||||||
|
return ChatWallpaperType.solid;
|
||||||
|
}
|
||||||
|
return ChatWallpaperType.values[index];
|
||||||
|
}(),
|
||||||
|
drawerBottomBackgroundColor1: Color(json['drawerBottomBackgroundColor1'] as int? ?? const Color(0xFF1E1E1E).value),
|
||||||
|
drawerBottomBackgroundColor2: Color(json['drawerBottomBackgroundColor2'] as int? ?? const Color(0xFF2E2E2E).value),
|
||||||
|
drawerBottomBackgroundImagePath: json['drawerBottomBackgroundImagePath'] as String?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -529,6 +661,26 @@ class ThemeProvider with ChangeNotifier {
|
|||||||
bool get debugShowMessageCount => _debugShowMessageCount;
|
bool get debugShowMessageCount => _debugShowMessageCount;
|
||||||
bool get debugReadOnEnter => _debugReadOnEnter;
|
bool get debugReadOnEnter => _debugReadOnEnter;
|
||||||
bool get debugReadOnAction => _debugReadOnAction;
|
bool get debugReadOnAction => _debugReadOnAction;
|
||||||
|
double get mobileChatBottomPadding => _activeTheme.mobileChatBottomPadding;
|
||||||
|
bool get ignoreMobileBottomPaddingWhenKeyboard =>
|
||||||
|
_activeTheme.ignoreMobileBottomPaddingWhenKeyboard;
|
||||||
|
|
||||||
|
// Экспериментальные настройки фона
|
||||||
|
bool get useExperimentalChatsListBackground => _activeTheme.useExperimentalChatsListBackground;
|
||||||
|
ChatWallpaperType get experimentalChatsListBackgroundType => _activeTheme.experimentalChatsListBackgroundType;
|
||||||
|
Color get experimentalChatsListBackgroundColor1 => _activeTheme.experimentalChatsListBackgroundColor1;
|
||||||
|
Color get experimentalChatsListBackgroundColor2 => _activeTheme.experimentalChatsListBackgroundColor2;
|
||||||
|
String? get experimentalChatsListBackgroundImagePath => _activeTheme.experimentalChatsListBackgroundImagePath;
|
||||||
|
|
||||||
|
ChatWallpaperType get drawerTopBackgroundType => _activeTheme.drawerTopBackgroundType;
|
||||||
|
Color get drawerTopBackgroundColor1 => _activeTheme.drawerTopBackgroundColor1;
|
||||||
|
Color get drawerTopBackgroundColor2 => _activeTheme.drawerTopBackgroundColor2;
|
||||||
|
String? get drawerTopBackgroundImagePath => _activeTheme.drawerTopBackgroundImagePath;
|
||||||
|
|
||||||
|
ChatWallpaperType get drawerBottomBackgroundType => _activeTheme.drawerBottomBackgroundType;
|
||||||
|
Color get drawerBottomBackgroundColor1 => _activeTheme.drawerBottomBackgroundColor1;
|
||||||
|
Color get drawerBottomBackgroundColor2 => _activeTheme.drawerBottomBackgroundColor2;
|
||||||
|
String? get drawerBottomBackgroundImagePath => _activeTheme.drawerBottomBackgroundImagePath;
|
||||||
|
|
||||||
TransitionOption get chatTransition => _activeTheme.ultraOptimizeChats
|
TransitionOption get chatTransition => _activeTheme.ultraOptimizeChats
|
||||||
? TransitionOption.systemDefault
|
? TransitionOption.systemDefault
|
||||||
@@ -1212,6 +1364,101 @@ class ThemeProvider with ChangeNotifier {
|
|||||||
await _saveActiveTheme();
|
await _saveActiveTheme();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setMobileChatBottomPadding(double value) async {
|
||||||
|
final clamped = value.clamp(60.0, 240.0);
|
||||||
|
_activeTheme = _activeTheme.copyWith(
|
||||||
|
mobileChatBottomPadding: clamped,
|
||||||
|
);
|
||||||
|
notifyListeners();
|
||||||
|
await _saveActiveTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setIgnoreMobileBottomPaddingWhenKeyboard(bool value) async {
|
||||||
|
_activeTheme = _activeTheme.copyWith(
|
||||||
|
ignoreMobileBottomPaddingWhenKeyboard: value,
|
||||||
|
);
|
||||||
|
notifyListeners();
|
||||||
|
await _saveActiveTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setUseExperimentalChatsListBackground(bool value) async {
|
||||||
|
_activeTheme = _activeTheme.copyWith(useExperimentalChatsListBackground: value);
|
||||||
|
notifyListeners();
|
||||||
|
await _saveActiveTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setExperimentalChatsListBackgroundType(ChatWallpaperType value) async {
|
||||||
|
_activeTheme = _activeTheme.copyWith(experimentalChatsListBackgroundType: value);
|
||||||
|
notifyListeners();
|
||||||
|
await _saveActiveTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setExperimentalChatsListBackgroundColor1(Color value) async {
|
||||||
|
_activeTheme = _activeTheme.copyWith(experimentalChatsListBackgroundColor1: value);
|
||||||
|
notifyListeners();
|
||||||
|
await _saveActiveTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setExperimentalChatsListBackgroundColor2(Color value) async {
|
||||||
|
_activeTheme = _activeTheme.copyWith(experimentalChatsListBackgroundColor2: value);
|
||||||
|
notifyListeners();
|
||||||
|
await _saveActiveTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setExperimentalChatsListBackgroundImagePath(String? path) async {
|
||||||
|
_activeTheme = _activeTheme.copyWith(experimentalChatsListBackgroundImagePath: path);
|
||||||
|
notifyListeners();
|
||||||
|
await _saveActiveTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setDrawerTopBackgroundType(ChatWallpaperType value) async {
|
||||||
|
_activeTheme = _activeTheme.copyWith(drawerTopBackgroundType: value);
|
||||||
|
notifyListeners();
|
||||||
|
await _saveActiveTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setDrawerTopBackgroundColor1(Color value) async {
|
||||||
|
_activeTheme = _activeTheme.copyWith(drawerTopBackgroundColor1: value);
|
||||||
|
notifyListeners();
|
||||||
|
await _saveActiveTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setDrawerTopBackgroundColor2(Color value) async {
|
||||||
|
_activeTheme = _activeTheme.copyWith(drawerTopBackgroundColor2: value);
|
||||||
|
notifyListeners();
|
||||||
|
await _saveActiveTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setDrawerTopBackgroundImagePath(String? path) async {
|
||||||
|
_activeTheme = _activeTheme.copyWith(drawerTopBackgroundImagePath: path);
|
||||||
|
notifyListeners();
|
||||||
|
await _saveActiveTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setDrawerBottomBackgroundType(ChatWallpaperType value) async {
|
||||||
|
_activeTheme = _activeTheme.copyWith(drawerBottomBackgroundType: value);
|
||||||
|
notifyListeners();
|
||||||
|
await _saveActiveTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setDrawerBottomBackgroundColor1(Color value) async {
|
||||||
|
_activeTheme = _activeTheme.copyWith(drawerBottomBackgroundColor1: value);
|
||||||
|
notifyListeners();
|
||||||
|
await _saveActiveTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setDrawerBottomBackgroundColor2(Color value) async {
|
||||||
|
_activeTheme = _activeTheme.copyWith(drawerBottomBackgroundColor2: value);
|
||||||
|
notifyListeners();
|
||||||
|
await _saveActiveTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setDrawerBottomBackgroundImagePath(String? path) async {
|
||||||
|
_activeTheme = _activeTheme.copyWith(drawerBottomBackgroundImagePath: path);
|
||||||
|
notifyListeners();
|
||||||
|
await _saveActiveTheme();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> setCustomReplyColor(Color? color) async {
|
Future<void> setCustomReplyColor(Color? color) async {
|
||||||
_activeTheme = _activeTheme.copyWith(customReplyColor: color);
|
_activeTheme = _activeTheme.copyWith(customReplyColor: color);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|||||||
Reference in New Issue
Block a user