просто босс, просто начальник нахуй оптимизации
This commit is contained in:
@@ -81,7 +81,7 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
|
|
||||||
double _navPageAnimStart = 0;
|
double _navPageAnimStart = 0;
|
||||||
double _navPageAnimEnd = 0;
|
double _navPageAnimEnd = 0;
|
||||||
double _navDragDx = 0;
|
final ValueNotifier<double> _navDragDx = ValueNotifier(0);
|
||||||
double _navDragBaseLeft = 0;
|
double _navDragBaseLeft = 0;
|
||||||
double _revealAnimBegin = 0.0;
|
double _revealAnimBegin = 0.0;
|
||||||
double _closeAnimBegin = 0.0;
|
double _closeAnimBegin = 0.0;
|
||||||
@@ -144,7 +144,6 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
_isSelectionMode,
|
_isSelectionMode,
|
||||||
_shouldCollapseSearch,
|
_shouldCollapseSearch,
|
||||||
_selectedChats.length,
|
_selectedChats.length,
|
||||||
_pullRatio,
|
|
||||||
_storiesDockedOpen,
|
_storiesDockedOpen,
|
||||||
_storiesAnimClosing,
|
_storiesAnimClosing,
|
||||||
_storiesOverscrollRevealArmed,
|
_storiesOverscrollRevealArmed,
|
||||||
@@ -619,7 +618,19 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int? _pageChatsBaseKey;
|
||||||
|
final Map<int, List<CachedChat>> _pageChatsCache = {};
|
||||||
|
|
||||||
List<CachedChat> _chatsForPageIndex(int pageIndex) {
|
List<CachedChat> _chatsForPageIndex(int pageIndex) {
|
||||||
|
final baseKey =
|
||||||
|
Object.hash(identityHashCode(_chats), identityHashCode(_folders));
|
||||||
|
if (_pageChatsBaseKey != baseKey) {
|
||||||
|
_pageChatsBaseKey = baseKey;
|
||||||
|
_pageChatsCache.clear();
|
||||||
|
}
|
||||||
|
final cached = _pageChatsCache[pageIndex];
|
||||||
|
if (cached != null) return cached;
|
||||||
|
|
||||||
List<CachedChat> base;
|
List<CachedChat> base;
|
||||||
if (_folders.isEmpty) {
|
if (_folders.isEmpty) {
|
||||||
base = _chats;
|
base = _chats;
|
||||||
@@ -634,7 +645,9 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
final pinned = base.where((c) => (c.favIndex ?? 0) > 0).toList()
|
final pinned = base.where((c) => (c.favIndex ?? 0) > 0).toList()
|
||||||
..sort((a, b) => a.favIndex!.compareTo(b.favIndex!));
|
..sort((a, b) => a.favIndex!.compareTo(b.favIndex!));
|
||||||
final regular = base.where((c) => (c.favIndex ?? 0) <= 0).toList();
|
final regular = base.where((c) => (c.favIndex ?? 0) <= 0).toList();
|
||||||
return [...pinned, ...regular];
|
final result = [...pinned, ...regular];
|
||||||
|
_pageChatsCache[pageIndex] = result;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _syncFolderChatScrollControllers() {
|
void _syncFolderChatScrollControllers() {
|
||||||
@@ -947,6 +960,7 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
}
|
}
|
||||||
_contactRebuildTimer?.cancel();
|
_contactRebuildTimer?.cancel();
|
||||||
_storiesUi.dispose();
|
_storiesUi.dispose();
|
||||||
|
_navDragDx.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -955,7 +969,7 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
required double Function(int index) bubbleLeftForIndex,
|
required double Function(int index) bubbleLeftForIndex,
|
||||||
}) {
|
}) {
|
||||||
if (_navDragging) {
|
if (_navDragging) {
|
||||||
final left = (_navDragBaseLeft + _navDragDx).clamp(
|
final left = (_navDragBaseLeft + _navDragDx.value).clamp(
|
||||||
bubbleLeftForIndex(0),
|
bubbleLeftForIndex(0),
|
||||||
bubbleLeftForIndex(3),
|
bubbleLeftForIndex(3),
|
||||||
);
|
);
|
||||||
@@ -1343,6 +1357,7 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
|
|
||||||
if (hasSeparator && index == pinnedCount) {
|
if (hasSeparator && index == pinnedCount) {
|
||||||
return Padding(
|
return Padding(
|
||||||
|
key: const ValueKey('pinned_divider'),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
child: Divider(
|
child: Divider(
|
||||||
height: 1,
|
height: 1,
|
||||||
@@ -1357,10 +1372,13 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
final isPinned = (chat.favIndex ?? 0) > 0;
|
final isPinned = (chat.favIndex ?? 0) > 0;
|
||||||
|
|
||||||
if (chat.type.isNotEmpty && chat.type == "DIALOG" && chat.id != 0) {
|
if (chat.type.isNotEmpty && chat.type == "DIALOG" && chat.id != 0) {
|
||||||
final secondId = chat.participants.entries
|
int secondId = _profile?.id ?? 0;
|
||||||
.where((entry) => entry.key != _profile?.id)
|
for (final entry in chat.participants.entries) {
|
||||||
.first
|
if (entry.key != _profile?.id) {
|
||||||
.key;
|
secondId = entry.key;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
final name = ContactCache.get(secondId);
|
final name = ContactCache.get(secondId);
|
||||||
final avatar = ContactCache.getAvatar(secondId);
|
final avatar = ContactCache.getAvatar(secondId);
|
||||||
// ContactCache.isOfficial covers contacts loaded via opcode 32;
|
// ContactCache.isOfficial covers contacts loaded via opcode 32;
|
||||||
@@ -1510,12 +1528,6 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
final minBubbleLeft = bubbleLeftForIndex(0);
|
final minBubbleLeft = bubbleLeftForIndex(0);
|
||||||
final maxBubbleLeft = bubbleLeftForIndex(3);
|
final maxBubbleLeft = bubbleLeftForIndex(3);
|
||||||
|
|
||||||
final bubbleLeft = _navDragging
|
|
||||||
? (_navDragBaseLeft + _navDragDx).clamp(minBubbleLeft, maxBubbleLeft)
|
|
||||||
: leftOffset;
|
|
||||||
|
|
||||||
final navRowT = ((bubbleLeft - 4) / inactiveWidth).clamp(0.0, 3.0);
|
|
||||||
|
|
||||||
double navInterpolatedWidth(int tabIndex, double rowT) {
|
double navInterpolatedWidth(int tabIndex, double rowT) {
|
||||||
final rt = rowT.clamp(0.0, 3.0);
|
final rt = rowT.clamp(0.0, 3.0);
|
||||||
final i0 = rt.floor().clamp(0, 3);
|
final i0 = rt.floor().clamp(0, 3);
|
||||||
@@ -1568,39 +1580,46 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
if (_isSelectionMode) return;
|
if (_isSelectionMode) return;
|
||||||
_navPageAnimController.stop();
|
_navPageAnimController.stop();
|
||||||
_navPageAnimController.value = 1.0;
|
_navPageAnimController.value = 1.0;
|
||||||
|
_navDragDx.value = 0;
|
||||||
setState(() {
|
setState(() {
|
||||||
_navDragging = true;
|
_navDragging = true;
|
||||||
_navDragDx = 0;
|
|
||||||
_navDragBaseLeft = bubbleLeftForIndex(_currentNavIndex);
|
_navDragBaseLeft = bubbleLeftForIndex(_currentNavIndex);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onHorizontalDragUpdate: (details) {
|
onHorizontalDragUpdate: (details) {
|
||||||
if (!_navDragging) return;
|
if (!_navDragging) return;
|
||||||
setState(() {
|
_navDragDx.value += details.delta.dx;
|
||||||
_navDragDx += details.delta.dx;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
onHorizontalDragEnd: (_) {
|
onHorizontalDragEnd: (_) {
|
||||||
if (!_navDragging) return;
|
if (!_navDragging) return;
|
||||||
final left = (_navDragBaseLeft + _navDragDx).clamp(
|
final left = (_navDragBaseLeft + _navDragDx.value).clamp(
|
||||||
minBubbleLeft,
|
minBubbleLeft,
|
||||||
maxBubbleLeft,
|
maxBubbleLeft,
|
||||||
);
|
);
|
||||||
final next = indexForBubbleLeft(left);
|
final next = indexForBubbleLeft(left);
|
||||||
|
_navDragDx.value = 0;
|
||||||
setState(() {
|
setState(() {
|
||||||
_currentNavIndex = next;
|
_currentNavIndex = next;
|
||||||
_navDragging = false;
|
_navDragging = false;
|
||||||
_navDragDx = 0;
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onHorizontalDragCancel: () {
|
onHorizontalDragCancel: () {
|
||||||
if (!_navDragging) return;
|
if (!_navDragging) return;
|
||||||
|
_navDragDx.value = 0;
|
||||||
setState(() {
|
setState(() {
|
||||||
_navDragging = false;
|
_navDragging = false;
|
||||||
_navDragDx = 0;
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
child: Stack(
|
child: ValueListenableBuilder<double>(
|
||||||
|
valueListenable: _navDragDx,
|
||||||
|
builder: (context, navDragDx, _) {
|
||||||
|
final bubbleLeft = _navDragging
|
||||||
|
? (_navDragBaseLeft + navDragDx)
|
||||||
|
.clamp(minBubbleLeft, maxBubbleLeft)
|
||||||
|
: leftOffset;
|
||||||
|
final navRowT =
|
||||||
|
((bubbleLeft - 4) / inactiveWidth).clamp(0.0, 3.0);
|
||||||
|
return Stack(
|
||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
children: [
|
children: [
|
||||||
AnimatedPositioned(
|
AnimatedPositioned(
|
||||||
@@ -1672,6 +1691,8 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1717,7 +1738,8 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
width: pageW * 4,
|
width: pageW * 4,
|
||||||
height: pageH,
|
height: pageH,
|
||||||
child: AnimatedBuilder(
|
child: AnimatedBuilder(
|
||||||
animation: _navPageAnimController,
|
animation: Listenable.merge(
|
||||||
|
[_navPageAnimController, _navDragDx]),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
@@ -2066,6 +2088,7 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
final isSelected = _selectedChats.contains(id);
|
final isSelected = _selectedChats.contains(id);
|
||||||
|
|
||||||
return InkWell(
|
return InkWell(
|
||||||
|
key: ValueKey('chat_$id'),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (_isSelectionMode) {
|
if (_isSelectionMode) {
|
||||||
_toggleSelection(id);
|
_toggleSelection(id);
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
int _otherStatus = 0;
|
int _otherStatus = 0;
|
||||||
int? _otherSeenTime;
|
int? _otherSeenTime;
|
||||||
final ValueNotifier<String> _headerStatusNotifier = ValueNotifier('');
|
final ValueNotifier<String> _headerStatusNotifier = ValueNotifier('');
|
||||||
|
final ValueNotifier<int> _otherReadTime = ValueNotifier(0);
|
||||||
int _tempIdCounter = 0;
|
int _tempIdCounter = 0;
|
||||||
late final AnimationController _attachAnim;
|
late final AnimationController _attachAnim;
|
||||||
|
|
||||||
@@ -123,6 +124,8 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
Timer? _shimmerStartTimer;
|
Timer? _shimmerStartTimer;
|
||||||
bool _historyKickedOff = false;
|
bool _historyKickedOff = false;
|
||||||
List<CachedMessage> _messages = [];
|
List<CachedMessage> _messages = [];
|
||||||
|
List<Object>? _combinedItemsCache;
|
||||||
|
int? _combinedItemsKey;
|
||||||
int _myId = 0;
|
int _myId = 0;
|
||||||
CachedChat? chat;
|
CachedChat? chat;
|
||||||
|
|
||||||
@@ -183,6 +186,7 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
chat = value.first;
|
chat = value.first;
|
||||||
});
|
});
|
||||||
_recomputeHeaderStatus();
|
_recomputeHeaderStatus();
|
||||||
|
_syncOtherReadTime();
|
||||||
}
|
}
|
||||||
}).catchError((_) {});
|
}).catchError((_) {});
|
||||||
|
|
||||||
@@ -395,6 +399,7 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
}
|
}
|
||||||
_typingTimers.clear();
|
_typingTimers.clear();
|
||||||
_headerStatusNotifier.dispose();
|
_headerStatusNotifier.dispose();
|
||||||
|
_otherReadTime.dispose();
|
||||||
_uploadStatus.dispose();
|
_uploadStatus.dispose();
|
||||||
_attachAnim.dispose();
|
_attachAnim.dispose();
|
||||||
_messageController.dispose();
|
_messageController.dispose();
|
||||||
@@ -419,17 +424,28 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String? _effectiveStatus(CachedMessage msg) {
|
int _computeOtherReadTime() {
|
||||||
if (msg.senderId != _myId) return null;
|
|
||||||
if (msg.status == 'sending' || msg.status == 'error') return msg.status;
|
|
||||||
final c = chat;
|
final c = chat;
|
||||||
if (c == null) return 'sent';
|
if (c == null) return 0;
|
||||||
int otherReadTime = 0;
|
int otherReadTime = 0;
|
||||||
for (final entry in c.participants.entries) {
|
for (final entry in c.participants.entries) {
|
||||||
if (entry.key != _myId && entry.value > otherReadTime) {
|
if (entry.key != _myId && entry.value > otherReadTime) {
|
||||||
otherReadTime = entry.value;
|
otherReadTime = entry.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return otherReadTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _syncOtherReadTime() {
|
||||||
|
final t = _computeOtherReadTime();
|
||||||
|
if (_otherReadTime.value != t) _otherReadTime.value = t;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _effectiveStatus(CachedMessage msg) {
|
||||||
|
if (msg.senderId != _myId) return null;
|
||||||
|
if (msg.status == 'sending' || msg.status == 'error') return msg.status;
|
||||||
|
if (chat == null) return 'sent';
|
||||||
|
final otherReadTime = _otherReadTime.value;
|
||||||
if (otherReadTime > 0 && otherReadTime >= msg.time) return 'read';
|
if (otherReadTime > 0 && otherReadTime >= msg.time) return 'read';
|
||||||
return 'sent';
|
return 'sent';
|
||||||
}
|
}
|
||||||
@@ -555,9 +571,8 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
final c = chat;
|
final c = chat;
|
||||||
if (c == null) return;
|
if (c == null) return;
|
||||||
if (c.participants[userId] == mark) return;
|
if (c.participants[userId] == mark) return;
|
||||||
setState(() {
|
c.participants[userId] = mark;
|
||||||
c.participants[userId] = mark;
|
_syncOtherReadTime();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _sendMessage() async {
|
Future<void> _sendMessage() async {
|
||||||
@@ -614,6 +629,7 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
ChatsModule.refreshChats(api, [widget.chatId]).then((list) {
|
ChatsModule.refreshChats(api, [widget.chatId]).then((list) {
|
||||||
if (!mounted || list.isEmpty) return;
|
if (!mounted || list.isEmpty) return;
|
||||||
setState(() => chat = list.first);
|
setState(() => chat = list.first);
|
||||||
|
_syncOtherReadTime();
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -708,6 +724,10 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<Object> _buildCombinedItems() {
|
List<Object> _buildCombinedItems() {
|
||||||
|
final key = Object.hashAll(_messages.map(identityHashCode));
|
||||||
|
final cached = _combinedItemsCache;
|
||||||
|
if (cached != null && _combinedItemsKey == key) return cached;
|
||||||
|
|
||||||
final List<Object> items = [];
|
final List<Object> items = [];
|
||||||
final Set<int> usedDates = {};
|
final Set<int> usedDates = {};
|
||||||
|
|
||||||
@@ -740,6 +760,8 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
_separatorKeys.removeWhere((k, _) => !usedDates.contains(k));
|
_separatorKeys.removeWhere((k, _) => !usedDates.contains(k));
|
||||||
|
_combinedItemsCache = items;
|
||||||
|
_combinedItemsKey = key;
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1022,7 +1044,9 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
return Stack(
|
return Stack(
|
||||||
key: _listKey,
|
key: _listKey,
|
||||||
children: [
|
children: [
|
||||||
ValueListenableBuilder<double>(
|
ValueListenableBuilder<int>(
|
||||||
|
valueListenable: _otherReadTime,
|
||||||
|
builder: (context, _, _) => ValueListenableBuilder<double>(
|
||||||
valueListenable: AppCacheExtent.current,
|
valueListenable: AppCacheExtent.current,
|
||||||
builder: (context, cacheExtent, _) => ListView.builder(
|
builder: (context, cacheExtent, _) => ListView.builder(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
@@ -1082,6 +1106,7 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 8,
|
top: 8,
|
||||||
left: 0,
|
left: 0,
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class _CloudStorageScreenState extends State<CloudStorageScreen>
|
|||||||
int? _accountId;
|
int? _accountId;
|
||||||
List<CloudFile> _files = [];
|
List<CloudFile> _files = [];
|
||||||
bool _isUploading = false;
|
bool _isUploading = false;
|
||||||
double _uploadProgress = 0;
|
final ValueNotifier<double> _uploadProgress = ValueNotifier(0);
|
||||||
bool _animateNewCard = false;
|
bool _animateNewCard = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -68,16 +68,19 @@ class _CloudStorageScreenState extends State<CloudStorageScreen>
|
|||||||
}
|
}
|
||||||
mgr.onProgress = (progress, _) {
|
mgr.onProgress = (progress, _) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() { _isUploading = true; _uploadProgress = progress; });
|
if (!_isUploading) setState(() => _isUploading = true);
|
||||||
|
_uploadProgress.value = progress;
|
||||||
};
|
};
|
||||||
mgr.onDone = (file) {
|
mgr.onDone = (file) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() { _isUploading = false; _uploadProgress = 0; });
|
_uploadProgress.value = 0;
|
||||||
|
setState(() => _isUploading = false);
|
||||||
_prependFile(file);
|
_prependFile(file);
|
||||||
};
|
};
|
||||||
mgr.onError = (msg) {
|
mgr.onError = (msg) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() { _isUploading = false; _uploadProgress = 0; });
|
_uploadProgress.value = 0;
|
||||||
|
setState(() => _isUploading = false);
|
||||||
showCustomNotification(context, 'Ошибка: $msg');
|
showCustomNotification(context, 'Ошибка: $msg');
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -91,6 +94,7 @@ class _CloudStorageScreenState extends State<CloudStorageScreen>
|
|||||||
_mode.dispose();
|
_mode.dispose();
|
||||||
_pageController.dispose();
|
_pageController.dispose();
|
||||||
_currentFilePage.dispose();
|
_currentFilePage.dispose();
|
||||||
|
_uploadProgress.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,7 +222,8 @@ class _CloudStorageScreenState extends State<CloudStorageScreen>
|
|||||||
final picked = result.files.first;
|
final picked = result.files.first;
|
||||||
if (picked.path == null) return;
|
if (picked.path == null) return;
|
||||||
|
|
||||||
setState(() { _isUploading = true; _uploadProgress = 0; });
|
_uploadProgress.value = 0;
|
||||||
|
setState(() => _isUploading = true);
|
||||||
|
|
||||||
await UploadManager.instance.start(
|
await UploadManager.instance.start(
|
||||||
chatId: chatId,
|
chatId: chatId,
|
||||||
@@ -427,17 +432,27 @@ class _CloudStorageScreenState extends State<CloudStorageScreen>
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
],
|
],
|
||||||
if (_isUploading) ...[
|
if (_isUploading) ...[
|
||||||
LinearProgressIndicator(
|
ValueListenableBuilder<double>(
|
||||||
value: _uploadProgress,
|
valueListenable: _uploadProgress,
|
||||||
borderRadius: BorderRadius.circular(4),
|
builder: (context, progress, _) => Column(
|
||||||
minHeight: 5,
|
mainAxisSize: MainAxisSize.min,
|
||||||
color: cs.primary,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
backgroundColor: cs.surfaceContainerHighest,
|
children: [
|
||||||
),
|
LinearProgressIndicator(
|
||||||
const SizedBox(height: 8),
|
value: progress,
|
||||||
Text(
|
borderRadius: BorderRadius.circular(4),
|
||||||
'Загрузка ${(_uploadProgress * 100).toStringAsFixed(0)}%',
|
minHeight: 5,
|
||||||
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
|
color: cs.primary,
|
||||||
|
backgroundColor: cs.surfaceContainerHighest,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Загрузка ${(progress * 100).toStringAsFixed(0)}%',
|
||||||
|
style:
|
||||||
|
TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
] else if (_files.isEmpty) ...[
|
] else if (_files.isEmpty) ...[
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -307,33 +307,40 @@ class TwoFactorSetupScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _TwoFactorSetupScreenState extends State<TwoFactorSetupScreen> {
|
class _TwoFactorSetupScreenState extends State<TwoFactorSetupScreen> {
|
||||||
final _passwordController = TextEditingController();
|
final _passwordController = TextEditingController();
|
||||||
|
final _confirmController = TextEditingController();
|
||||||
final _hintController = TextEditingController();
|
final _hintController = TextEditingController();
|
||||||
final _emailController = TextEditingController();
|
final _emailController = TextEditingController();
|
||||||
final _codeController = TextEditingController();
|
final _codeController = TextEditingController();
|
||||||
|
|
||||||
int _step = 0;
|
int _step = 0;
|
||||||
bool _isLoading = false;
|
final ValueNotifier<bool> _isLoading = ValueNotifier(false);
|
||||||
String? _trackId;
|
String? _trackId;
|
||||||
String? _errorMessage;
|
String? _errorMessage;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_passwordController.dispose();
|
_passwordController.dispose();
|
||||||
|
_confirmController.dispose();
|
||||||
_hintController.dispose();
|
_hintController.dispose();
|
||||||
_emailController.dispose();
|
_emailController.dispose();
|
||||||
_codeController.dispose();
|
_codeController.dispose();
|
||||||
|
_isLoading.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _nextStep() async {
|
Future<void> _nextStep() async {
|
||||||
setState(() {
|
_isLoading.value = true;
|
||||||
_isLoading = true;
|
setState(() => _errorMessage = null);
|
||||||
_errorMessage = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
switch (_step) {
|
switch (_step) {
|
||||||
case 0:
|
case 0:
|
||||||
|
if (_passwordController.text.length < 6) {
|
||||||
|
setState(
|
||||||
|
() => _errorMessage = 'Пароль должен быть минимум 6 символов',
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
final trackId = await accountModule.create2faTrack();
|
final trackId = await accountModule.create2faTrack();
|
||||||
setState(() {
|
setState(() {
|
||||||
_trackId = trackId;
|
_trackId = trackId;
|
||||||
@@ -341,10 +348,8 @@ class _TwoFactorSetupScreenState extends State<TwoFactorSetupScreen> {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
if (_passwordController.text.length < 6) {
|
if (_confirmController.text != _passwordController.text) {
|
||||||
setState(
|
setState(() => _errorMessage = 'Пароли не совпадают');
|
||||||
() => _errorMessage = 'Пароль должен быть минимум 6 символов',
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
await accountModule.set2faPassword(
|
await accountModule.set2faPassword(
|
||||||
@@ -392,7 +397,7 @@ class _TwoFactorSetupScreenState extends State<TwoFactorSetupScreen> {
|
|||||||
setState(() => _errorMessage = e.toString());
|
setState(() => _errorMessage = e.toString());
|
||||||
} finally {
|
} finally {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() => _isLoading = false);
|
_isLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -458,26 +463,29 @@ class _TwoFactorSetupScreenState extends State<TwoFactorSetupScreen> {
|
|||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: FilledButton(
|
child: ValueListenableBuilder<bool>(
|
||||||
onPressed: _isLoading ? null : _nextStep,
|
valueListenable: _isLoading,
|
||||||
style: FilledButton.styleFrom(
|
builder: (context, loading, _) => FilledButton(
|
||||||
backgroundColor: cs.primary,
|
onPressed: loading ? null : _nextStep,
|
||||||
foregroundColor: cs.onPrimary,
|
style: FilledButton.styleFrom(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
backgroundColor: cs.primary,
|
||||||
shape: RoundedRectangleBorder(
|
foregroundColor: cs.onPrimary,
|
||||||
borderRadius: BorderRadius.circular(12),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
child: loading
|
||||||
|
? SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: cs.onPrimary,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(_step == 4 ? 'Установить пароль' : 'Продолжить'),
|
||||||
),
|
),
|
||||||
child: _isLoading
|
|
||||||
? SizedBox(
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2,
|
|
||||||
color: cs.onPrimary,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Text(_step == 4 ? 'Установить пароль' : 'Продолжить'),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -591,7 +599,7 @@ class _TwoFactorSetupScreenState extends State<TwoFactorSetupScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_PasswordField(
|
_PasswordField(
|
||||||
controller: _passwordController,
|
controller: _confirmController,
|
||||||
hintText: 'Повторите пароль',
|
hintText: 'Повторите пароль',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -714,7 +722,7 @@ class TwoFactorManageScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _TwoFactorManageScreenState extends State<TwoFactorManageScreen> {
|
class _TwoFactorManageScreenState extends State<TwoFactorManageScreen> {
|
||||||
final _passwordController = TextEditingController();
|
final _passwordController = TextEditingController();
|
||||||
bool _isLoading = false;
|
final ValueNotifier<bool> _isLoading = ValueNotifier(false);
|
||||||
bool _isAuthenticated = false;
|
bool _isAuthenticated = false;
|
||||||
String? _trackId;
|
String? _trackId;
|
||||||
TwoFactorDetails? _details;
|
TwoFactorDetails? _details;
|
||||||
@@ -723,14 +731,13 @@ class _TwoFactorManageScreenState extends State<TwoFactorManageScreen> {
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_passwordController.dispose();
|
_passwordController.dispose();
|
||||||
|
_isLoading.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _authenticate() async {
|
Future<void> _authenticate() async {
|
||||||
setState(() {
|
_isLoading.value = true;
|
||||||
_isLoading = true;
|
setState(() => _errorMessage = null);
|
||||||
_errorMessage = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
_trackId = await accountModule.enter2faPanel();
|
_trackId = await accountModule.enter2faPanel();
|
||||||
@@ -743,7 +750,7 @@ class _TwoFactorManageScreenState extends State<TwoFactorManageScreen> {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
setState(() => _errorMessage = 'Неверный пароль');
|
setState(() => _errorMessage = 'Неверный пароль');
|
||||||
} finally {
|
} finally {
|
||||||
if (mounted) setState(() => _isLoading = false);
|
if (mounted) _isLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -807,26 +814,29 @@ class _TwoFactorManageScreenState extends State<TwoFactorManageScreen> {
|
|||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: FilledButton(
|
child: ValueListenableBuilder<bool>(
|
||||||
onPressed: _isLoading ? null : _authenticate,
|
valueListenable: _isLoading,
|
||||||
style: FilledButton.styleFrom(
|
builder: (context, loading, _) => FilledButton(
|
||||||
backgroundColor: cs.primary,
|
onPressed: loading ? null : _authenticate,
|
||||||
foregroundColor: cs.onPrimary,
|
style: FilledButton.styleFrom(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
backgroundColor: cs.primary,
|
||||||
shape: RoundedRectangleBorder(
|
foregroundColor: cs.onPrimary,
|
||||||
borderRadius: BorderRadius.circular(12),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
child: loading
|
||||||
|
? SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: cs.onPrimary,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Text('Продолжить'),
|
||||||
),
|
),
|
||||||
child: _isLoading
|
|
||||||
? SizedBox(
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2,
|
|
||||||
color: cs.onPrimary,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const Text('Продолжить'),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -917,13 +927,14 @@ class _TwoFactorPasswordChangeScreenState
|
|||||||
extends State<TwoFactorPasswordChangeScreen> {
|
extends State<TwoFactorPasswordChangeScreen> {
|
||||||
final _passwordController = TextEditingController();
|
final _passwordController = TextEditingController();
|
||||||
final _hintController = TextEditingController();
|
final _hintController = TextEditingController();
|
||||||
bool _isLoading = false;
|
final ValueNotifier<bool> _isLoading = ValueNotifier(false);
|
||||||
String? _errorMessage;
|
String? _errorMessage;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_passwordController.dispose();
|
_passwordController.dispose();
|
||||||
_hintController.dispose();
|
_hintController.dispose();
|
||||||
|
_isLoading.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -933,10 +944,8 @@ class _TwoFactorPasswordChangeScreenState
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
_isLoading.value = true;
|
||||||
_isLoading = true;
|
setState(() => _errorMessage = null);
|
||||||
_errorMessage = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final trackId = await accountModule.enter2faPanel();
|
final trackId = await accountModule.enter2faPanel();
|
||||||
@@ -956,7 +965,7 @@ class _TwoFactorPasswordChangeScreenState
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
setState(() => _errorMessage = e.toString());
|
setState(() => _errorMessage = e.toString());
|
||||||
} finally {
|
} finally {
|
||||||
if (mounted) setState(() => _isLoading = false);
|
if (mounted) _isLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1037,26 +1046,29 @@ class _TwoFactorPasswordChangeScreenState
|
|||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: FilledButton(
|
child: ValueListenableBuilder<bool>(
|
||||||
onPressed: _isLoading ? null : _changePassword,
|
valueListenable: _isLoading,
|
||||||
style: FilledButton.styleFrom(
|
builder: (context, loading, _) => FilledButton(
|
||||||
backgroundColor: cs.primary,
|
onPressed: loading ? null : _changePassword,
|
||||||
foregroundColor: cs.onPrimary,
|
style: FilledButton.styleFrom(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
backgroundColor: cs.primary,
|
||||||
shape: RoundedRectangleBorder(
|
foregroundColor: cs.onPrimary,
|
||||||
borderRadius: BorderRadius.circular(12),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
child: loading
|
||||||
|
? SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: cs.onPrimary,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Text('Сохранить'),
|
||||||
),
|
),
|
||||||
child: _isLoading
|
|
||||||
? SizedBox(
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2,
|
|
||||||
color: cs.onPrimary,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const Text('Сохранить'),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -1080,7 +1092,7 @@ class _TwoFactorEmailChangeScreenState
|
|||||||
final _emailController = TextEditingController();
|
final _emailController = TextEditingController();
|
||||||
final _codeController = TextEditingController();
|
final _codeController = TextEditingController();
|
||||||
int _step = 0;
|
int _step = 0;
|
||||||
bool _isLoading = false;
|
final ValueNotifier<bool> _isLoading = ValueNotifier(false);
|
||||||
String? _trackId;
|
String? _trackId;
|
||||||
String? _errorMessage;
|
String? _errorMessage;
|
||||||
|
|
||||||
@@ -1089,14 +1101,13 @@ class _TwoFactorEmailChangeScreenState
|
|||||||
_passwordController.dispose();
|
_passwordController.dispose();
|
||||||
_emailController.dispose();
|
_emailController.dispose();
|
||||||
_codeController.dispose();
|
_codeController.dispose();
|
||||||
|
_isLoading.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _nextStep() async {
|
Future<void> _nextStep() async {
|
||||||
setState(() {
|
_isLoading.value = true;
|
||||||
_isLoading = true;
|
setState(() => _errorMessage = null);
|
||||||
_errorMessage = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
switch (_step) {
|
switch (_step) {
|
||||||
@@ -1140,7 +1151,7 @@ class _TwoFactorEmailChangeScreenState
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
setState(() => _errorMessage = e.toString());
|
setState(() => _errorMessage = e.toString());
|
||||||
} finally {
|
} finally {
|
||||||
if (mounted) setState(() => _isLoading = false);
|
if (mounted) _isLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1256,26 +1267,29 @@ class _TwoFactorEmailChangeScreenState
|
|||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: FilledButton(
|
child: ValueListenableBuilder<bool>(
|
||||||
onPressed: _isLoading ? null : _nextStep,
|
valueListenable: _isLoading,
|
||||||
style: FilledButton.styleFrom(
|
builder: (context, loading, _) => FilledButton(
|
||||||
backgroundColor: cs.primary,
|
onPressed: loading ? null : _nextStep,
|
||||||
foregroundColor: cs.onPrimary,
|
style: FilledButton.styleFrom(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
backgroundColor: cs.primary,
|
||||||
shape: RoundedRectangleBorder(
|
foregroundColor: cs.onPrimary,
|
||||||
borderRadius: BorderRadius.circular(12),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
child: loading
|
||||||
|
? SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: cs.onPrimary,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(_step == 2 ? 'Сохранить' : 'Продолжить'),
|
||||||
),
|
),
|
||||||
child: _isLoading
|
|
||||||
? SizedBox(
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2,
|
|
||||||
color: cs.onPrimary,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Text(_step == 2 ? 'Сохранить' : 'Продолжить'),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -1294,20 +1308,19 @@ class TwoFactorRemoveScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _TwoFactorRemoveScreenState extends State<TwoFactorRemoveScreen> {
|
class _TwoFactorRemoveScreenState extends State<TwoFactorRemoveScreen> {
|
||||||
final _passwordController = TextEditingController();
|
final _passwordController = TextEditingController();
|
||||||
bool _isLoading = false;
|
final ValueNotifier<bool> _isLoading = ValueNotifier(false);
|
||||||
String? _errorMessage;
|
String? _errorMessage;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_passwordController.dispose();
|
_passwordController.dispose();
|
||||||
|
_isLoading.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _remove2fa() async {
|
Future<void> _remove2fa() async {
|
||||||
setState(() {
|
_isLoading.value = true;
|
||||||
_isLoading = true;
|
setState(() => _errorMessage = null);
|
||||||
_errorMessage = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final trackId = await accountModule.enter2faPanel();
|
final trackId = await accountModule.enter2faPanel();
|
||||||
@@ -1323,7 +1336,7 @@ class _TwoFactorRemoveScreenState extends State<TwoFactorRemoveScreen> {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
setState(() => _errorMessage = e.toString());
|
setState(() => _errorMessage = e.toString());
|
||||||
} finally {
|
} finally {
|
||||||
if (mounted) setState(() => _isLoading = false);
|
if (mounted) _isLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1402,26 +1415,29 @@ class _TwoFactorRemoveScreenState extends State<TwoFactorRemoveScreen> {
|
|||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: FilledButton(
|
child: ValueListenableBuilder<bool>(
|
||||||
onPressed: _isLoading ? null : _remove2fa,
|
valueListenable: _isLoading,
|
||||||
style: FilledButton.styleFrom(
|
builder: (context, loading, _) => FilledButton(
|
||||||
backgroundColor: cs.error,
|
onPressed: loading ? null : _remove2fa,
|
||||||
foregroundColor: cs.onError,
|
style: FilledButton.styleFrom(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
backgroundColor: cs.error,
|
||||||
shape: RoundedRectangleBorder(
|
foregroundColor: cs.onError,
|
||||||
borderRadius: BorderRadius.circular(12),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
child: loading
|
||||||
|
? SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: cs.onError,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Text('Удалить пароль'),
|
||||||
),
|
),
|
||||||
child: _isLoading
|
|
||||||
? SizedBox(
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2,
|
|
||||||
color: cs.onError,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const Text('Удалить пароль'),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ class MessageBubble extends StatelessWidget {
|
|||||||
static const Radius _smallRadius = Radius.circular(4);
|
static const Radius _smallRadius = Radius.circular(4);
|
||||||
static const Radius _photoRadius = Radius.circular(photoBorderRadius);
|
static const Radius _photoRadius = Radius.circular(photoBorderRadius);
|
||||||
|
|
||||||
|
static final Color _reactionChipBg = Colors.black.withValues(alpha: 0.18);
|
||||||
|
static const BorderRadius _reactionChipRadius =
|
||||||
|
BorderRadius.all(Radius.circular(10));
|
||||||
|
|
||||||
static Color bubbleTextColor(BuildContext context) =>
|
static Color bubbleTextColor(BuildContext context) =>
|
||||||
Theme.of(context).brightness == Brightness.dark
|
Theme.of(context).brightness == Brightness.dark
|
||||||
? Colors.white
|
? Colors.white
|
||||||
@@ -271,14 +275,30 @@ class MessageBubble extends StatelessWidget {
|
|||||||
final showAvatarSlot = !isMe;
|
final showAvatarSlot = !isMe;
|
||||||
final showAvatar = showAvatarSlot &&
|
final showAvatar = showAvatarSlot &&
|
||||||
chatType == "CHAT" &&
|
chatType == "CHAT" &&
|
||||||
nextMessage?.senderId != message.senderId &&
|
nextMessage?.senderId != message.senderId;
|
||||||
prevMessage?.senderId == message.senderId;
|
|
||||||
|
|
||||||
final bubbleListenable = Listenable.merge([
|
final maxBubbleWidth = MediaQuery.sizeOf(context).width * 0.75;
|
||||||
AppBubbleShape.current,
|
final bubbleColor =
|
||||||
AppBubbleBehavior.current,
|
isMe ? cs.primaryContainer : cs.surfaceContainerHighest;
|
||||||
?reactionsListenable,
|
|
||||||
]);
|
_BubbleCtx makeCtx() => _BubbleCtx(
|
||||||
|
context: context,
|
||||||
|
cs: cs,
|
||||||
|
text: textColor,
|
||||||
|
shape: shape,
|
||||||
|
contentType: contentType,
|
||||||
|
hasPhotoWithCaption: hasPhotoCap,
|
||||||
|
hasMultiplePhotosNoCaption: hasMultiPhotos,
|
||||||
|
reactionInfo: _resolveReactionInfo(),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Widget bubbleContent =
|
||||||
|
reactionsListenable != null && contentType == MessageType.text
|
||||||
|
? ValueListenableBuilder<Map<String, dynamic>?>(
|
||||||
|
valueListenable: reactionsListenable!,
|
||||||
|
builder: (context, _, _) => _buildContent(makeCtx()),
|
||||||
|
)
|
||||||
|
: _buildContent(makeCtx());
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: Haptics.tap,
|
onTap: Haptics.tap,
|
||||||
@@ -310,63 +330,35 @@ class MessageBubble extends StatelessWidget {
|
|||||||
isMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
|
isMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
ListenableBuilder(
|
ListenableBuilder(
|
||||||
listenable: bubbleListenable,
|
listenable: Listenable.merge([
|
||||||
builder: (context, _) {
|
AppBubbleShape.current,
|
||||||
final reactionInfo = _resolveReactionInfo();
|
AppBubbleBehavior.current,
|
||||||
final ctx = _BubbleCtx(
|
]),
|
||||||
context: context,
|
builder: (context, child) => Container(
|
||||||
cs: cs,
|
constraints: BoxConstraints(maxWidth: maxBubbleWidth),
|
||||||
text: textColor,
|
decoration: BoxDecoration(
|
||||||
shape: shape,
|
color: bubbleColor,
|
||||||
contentType: contentType,
|
borderRadius: _borderRadiusFor(
|
||||||
hasPhotoWithCaption: hasPhotoCap,
|
AppBubbleShape.current.value,
|
||||||
hasMultiplePhotosNoCaption: hasMultiPhotos,
|
AppBubbleBehavior.current.value,
|
||||||
reactionInfo: reactionInfo,
|
shape,
|
||||||
);
|
hasPhotoCap,
|
||||||
return AnimatedSize(
|
hasMultiPhotos,
|
||||||
duration: const Duration(milliseconds: 150),
|
|
||||||
curve: Curves.easeOutCubic,
|
|
||||||
alignment: isMe
|
|
||||||
? Alignment.bottomRight
|
|
||||||
: Alignment.bottomLeft,
|
|
||||||
child: Container(
|
|
||||||
constraints: BoxConstraints(
|
|
||||||
maxWidth:
|
|
||||||
MediaQuery.sizeOf(context).width * 0.75,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: isMe
|
|
||||||
? cs.primaryContainer
|
|
||||||
: cs.surfaceContainerHighest,
|
|
||||||
borderRadius: _borderRadiusFor(
|
|
||||||
AppBubbleShape.current.value,
|
|
||||||
AppBubbleBehavior.current.value,
|
|
||||||
shape,
|
|
||||||
hasPhotoCap,
|
|
||||||
hasMultiPhotos,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
padding: padding,
|
|
||||||
child: _buildContent(ctx),
|
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
padding: padding,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
child: bubbleContent,
|
||||||
),
|
),
|
||||||
if (contentType != MessageType.text)
|
if (contentType != MessageType.text)
|
||||||
AnimatedSize(
|
reactionsListenable != null
|
||||||
duration: const Duration(milliseconds: 150),
|
? ValueListenableBuilder<Map<String, dynamic>?>(
|
||||||
curve: Curves.easeOutCubic,
|
valueListenable: reactionsListenable!,
|
||||||
alignment: isMe
|
builder: (context, info, _) =>
|
||||||
? Alignment.centerRight
|
_buildReactionsBarFor(cs, info),
|
||||||
: Alignment.centerLeft,
|
)
|
||||||
child: reactionsListenable != null
|
: _buildReactionsBar(cs),
|
||||||
? ValueListenableBuilder<Map<String, dynamic>?>(
|
|
||||||
valueListenable: reactionsListenable!,
|
|
||||||
builder: (context, info, _) =>
|
|
||||||
_buildReactionsBarFor(cs, info),
|
|
||||||
)
|
|
||||||
: _buildReactionsBar(cs),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -432,8 +424,8 @@ class MessageBubble extends StatelessWidget {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isYours
|
color: isYours
|
||||||
? cs.primary.withValues(alpha: 0.22)
|
? cs.primary.withValues(alpha: 0.22)
|
||||||
: Colors.black.withValues(alpha: 0.18),
|
: _reactionChipBg,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: _reactionChipRadius,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
|||||||
+28
-13
@@ -66,18 +66,33 @@ void main() async {
|
|||||||
}
|
}
|
||||||
attachInfoCacheApi(api);
|
attachInfoCacheApi(api);
|
||||||
ChatsModule.attachGlobalPushHandlers(api);
|
ChatsModule.attachGlobalPushHandlers(api);
|
||||||
|
|
||||||
|
final packageInfoFuture = PackageInfo.fromPlatform();
|
||||||
|
final localeFuture = _loadInitialLocale();
|
||||||
|
final hapticsFuture = Haptics.load();
|
||||||
|
final prefsFuture = SharedPreferences.getInstance();
|
||||||
|
final accentFuture = AppAccent.load();
|
||||||
|
final bubbleShapeFuture = AppBubbleShape.load();
|
||||||
|
final bubbleBehaviorFuture = AppBubbleBehavior.load();
|
||||||
|
final cacheExtentFuture = AppCacheExtent.load();
|
||||||
|
final themeModeFuture = AppThemeModeConfig.load();
|
||||||
|
final amoledFuture = AppAmoled.load();
|
||||||
|
final themeScheduleFuture = AppThemeSchedule.load();
|
||||||
|
final messageActionsFuture = AppMessageActionsStyle.load();
|
||||||
|
final swipeBackFuture = AppSwipeBackDesktop.load();
|
||||||
|
|
||||||
await api.connect();
|
await api.connect();
|
||||||
|
|
||||||
final packageInfo = await PackageInfo.fromPlatform();
|
final packageInfo = await packageInfoFuture;
|
||||||
if (packageInfo.packageName == 'ru.oneme.app') {
|
if (packageInfo.packageName == 'ru.oneme.app') {
|
||||||
await PushService.instance.init(api: api, account: accountModule);
|
await PushService.instance.init(api: api, account: accountModule);
|
||||||
}
|
}
|
||||||
|
|
||||||
final initialLocale = await _loadInitialLocale();
|
final initialLocale = await localeFuture;
|
||||||
|
|
||||||
await Haptics.load();
|
await hapticsFuture;
|
||||||
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await prefsFuture;
|
||||||
await FileHistoryCache.load(prefs);
|
await FileHistoryCache.load(prefs);
|
||||||
final initialFpsOverlay = prefs.getBool('dev_fps_overlay') ?? false;
|
final initialFpsOverlay = prefs.getBool('dev_fps_overlay') ?? false;
|
||||||
final initialVpnBypass = prefs.getBool(VpnBypassService.prefKey) ?? false;
|
final initialVpnBypass = prefs.getBool(VpnBypassService.prefKey) ?? false;
|
||||||
@@ -87,15 +102,15 @@ void main() async {
|
|||||||
final initialFontScale = AppFonts.clampScale(
|
final initialFontScale = AppFonts.clampScale(
|
||||||
prefs.getDouble(AppFonts.scalePrefKey) ?? AppFonts.defaultScale,
|
prefs.getDouble(AppFonts.scalePrefKey) ?? AppFonts.defaultScale,
|
||||||
);
|
);
|
||||||
final initialAccentSeed = await AppAccent.load();
|
final initialAccentSeed = await accentFuture;
|
||||||
AppBubbleShape.current.value = await AppBubbleShape.load();
|
AppBubbleShape.current.value = await bubbleShapeFuture;
|
||||||
AppBubbleBehavior.current.value = await AppBubbleBehavior.load();
|
AppBubbleBehavior.current.value = await bubbleBehaviorFuture;
|
||||||
AppCacheExtent.current.value = await AppCacheExtent.load();
|
AppCacheExtent.current.value = await cacheExtentFuture;
|
||||||
AppThemeModeConfig.current.value = await AppThemeModeConfig.load();
|
AppThemeModeConfig.current.value = await themeModeFuture;
|
||||||
AppAmoled.current.value = await AppAmoled.load();
|
AppAmoled.current.value = await amoledFuture;
|
||||||
AppThemeSchedule.current.value = await AppThemeSchedule.load();
|
AppThemeSchedule.current.value = await themeScheduleFuture;
|
||||||
AppMessageActionsStyle.current.value = await AppMessageActionsStyle.load();
|
AppMessageActionsStyle.current.value = await messageActionsFuture;
|
||||||
AppSwipeBackDesktop.current.value = await AppSwipeBackDesktop.load();
|
AppSwipeBackDesktop.current.value = await swipeBackFuture;
|
||||||
runApp(
|
runApp(
|
||||||
KometApp(
|
KometApp(
|
||||||
initialLocale: initialLocale,
|
initialLocale: initialLocale,
|
||||||
|
|||||||
Reference in New Issue
Block a user