Добавлено управление анимацией раскрытия историй в экране списка чатов
This commit is contained in:
@@ -32,6 +32,13 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
final Set<String> _selectedChats = {};
|
final Set<String> _selectedChats = {};
|
||||||
final ScrollController _scrollController = ScrollController();
|
final ScrollController _scrollController = ScrollController();
|
||||||
double _pullRatio = 0.0;
|
double _pullRatio = 0.0;
|
||||||
|
static const double _kStoriesPullTriggerPx = 16.0;
|
||||||
|
late AnimationController _storiesRevealController;
|
||||||
|
double _revealAnimBegin = 0.0;
|
||||||
|
double _closeAnimBegin = 0.0;
|
||||||
|
bool _storiesAnimClosing = false;
|
||||||
|
bool _storiesDockedOpen = false;
|
||||||
|
double _storiesCloseIntentAccum = 0.0;
|
||||||
ProfileData? _profile;
|
ProfileData? _profile;
|
||||||
List<CachedChat> _chats = [];
|
List<CachedChat> _chats = [];
|
||||||
SessionState _sessionState = SessionState.disconnected;
|
SessionState _sessionState = SessionState.disconnected;
|
||||||
@@ -80,6 +87,14 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
duration: const Duration(milliseconds: 1500),
|
duration: const Duration(milliseconds: 1500),
|
||||||
)..repeat();
|
)..repeat();
|
||||||
|
|
||||||
|
_storiesRevealController =
|
||||||
|
AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 400),
|
||||||
|
)
|
||||||
|
..addListener(_onStoriesRevealTick)
|
||||||
|
..addStatusListener(_onStoriesRevealStatus);
|
||||||
|
|
||||||
_scrollController.addListener(_onScroll);
|
_scrollController.addListener(_onScroll);
|
||||||
|
|
||||||
_sessionState = api.state;
|
_sessionState = api.state;
|
||||||
@@ -181,6 +196,110 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onStoriesRevealTick() {
|
||||||
|
if (!mounted) return;
|
||||||
|
final t = Curves.easeOutCubic.transform(_storiesRevealController.value);
|
||||||
|
setState(() {
|
||||||
|
if (_storiesAnimClosing) {
|
||||||
|
_pullRatio = _closeAnimBegin * (1.0 - t);
|
||||||
|
} else {
|
||||||
|
_pullRatio = _revealAnimBegin + (1.0 - _revealAnimBegin) * t;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onStoriesRevealStatus(AnimationStatus status) {
|
||||||
|
if (!mounted) return;
|
||||||
|
if (status == AnimationStatus.completed) {
|
||||||
|
setState(() {
|
||||||
|
if (_storiesAnimClosing) {
|
||||||
|
_pullRatio = 0.0;
|
||||||
|
_storiesDockedOpen = false;
|
||||||
|
_storiesAnimClosing = false;
|
||||||
|
} else {
|
||||||
|
_pullRatio = 1.0;
|
||||||
|
_storiesDockedOpen = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startStoriesAutoReveal(double suggestedFrom) {
|
||||||
|
if (_storiesRevealController.isAnimating && !_storiesAnimClosing) return;
|
||||||
|
if (_storiesDockedOpen) return;
|
||||||
|
_storiesRevealController.stop();
|
||||||
|
_storiesAnimClosing = false;
|
||||||
|
final from = max(_pullRatio, suggestedFrom.clamp(0.0, 1.0));
|
||||||
|
if (from >= 1.0) {
|
||||||
|
setState(() {
|
||||||
|
_pullRatio = 1.0;
|
||||||
|
_storiesDockedOpen = true;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_revealAnimBegin = from;
|
||||||
|
_storiesRevealController.duration = Duration(
|
||||||
|
milliseconds: (260 + 240 * (1.0 - from)).round(),
|
||||||
|
);
|
||||||
|
_storiesRevealController.reset();
|
||||||
|
_storiesRevealController.forward(from: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startStoriesAutoClose() {
|
||||||
|
if (_pullRatio <= 0 &&
|
||||||
|
!_storiesDockedOpen &&
|
||||||
|
!_storiesRevealController.isAnimating) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_storiesAnimClosing && _storiesRevealController.isAnimating) return;
|
||||||
|
_storiesCloseIntentAccum = 0.0;
|
||||||
|
_storiesRevealController.stop();
|
||||||
|
_storiesAnimClosing = true;
|
||||||
|
final from = _pullRatio.clamp(0.0, 1.0);
|
||||||
|
if (from <= 0) {
|
||||||
|
setState(() {
|
||||||
|
_pullRatio = 0.0;
|
||||||
|
_storiesDockedOpen = false;
|
||||||
|
_storiesAnimClosing = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_closeAnimBegin = from;
|
||||||
|
_storiesRevealController.duration = Duration(
|
||||||
|
milliseconds: (260 + 240 * from).round(),
|
||||||
|
);
|
||||||
|
_storiesRevealController.reset();
|
||||||
|
_storiesRevealController.forward(from: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _onStoriesScrollNotification(ScrollNotification n) {
|
||||||
|
if (_currentNavIndex != 0) return false;
|
||||||
|
if (n is! ScrollUpdateNotification) return false;
|
||||||
|
if (!_scrollController.hasClients) return false;
|
||||||
|
if (!_storiesDockedOpen || _storiesRevealController.isAnimating) {
|
||||||
|
_storiesCloseIntentAccum = 0.0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final m = n.metrics;
|
||||||
|
if (m.axis != Axis.vertical) return false;
|
||||||
|
if (m.pixels > m.minScrollExtent + 1.0) {
|
||||||
|
_storiesCloseIntentAccum = 0.0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final d = n.scrollDelta;
|
||||||
|
if (d == null) return false;
|
||||||
|
if (d > 0) {
|
||||||
|
_storiesCloseIntentAccum = 0.0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_storiesCloseIntentAccum += -d;
|
||||||
|
if (_storiesCloseIntentAccum >= _kStoriesPullTriggerPx) {
|
||||||
|
_storiesCloseIntentAccum = 0.0;
|
||||||
|
_startStoriesAutoClose();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
void _onScroll() {
|
void _onScroll() {
|
||||||
if (_scrollController.hasClients) {
|
if (_scrollController.hasClients) {
|
||||||
final double offset = _scrollController.offset;
|
final double offset = _scrollController.offset;
|
||||||
@@ -191,16 +310,28 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (offset < 0) {
|
if (offset < 0) {
|
||||||
final newRatio = (offset.abs() / 80.0).clamp(0.0, 1.0);
|
final dragRatio = (offset.abs() / 80.0).clamp(0.0, 1.0);
|
||||||
if (newRatio != _pullRatio) {
|
if (_storiesRevealController.isAnimating) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!_storiesDockedOpen && offset.abs() >= _kStoriesPullTriggerPx) {
|
||||||
|
_startStoriesAutoReveal(dragRatio);
|
||||||
|
} else if (!_storiesDockedOpen) {
|
||||||
|
if (dragRatio != _pullRatio) {
|
||||||
|
setState(() {
|
||||||
|
_pullRatio = dragRatio;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (_storiesDockedOpen || _storiesRevealController.isAnimating) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_pullRatio > 0) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_pullRatio = newRatio;
|
_pullRatio = 0.0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (_pullRatio > 0) {
|
|
||||||
setState(() {
|
|
||||||
_pullRatio = 0.0;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -209,6 +340,10 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
_stateSub?.cancel();
|
_stateSub?.cancel();
|
||||||
_fabController.dispose();
|
_fabController.dispose();
|
||||||
|
_storiesRevealController
|
||||||
|
..removeListener(_onStoriesRevealTick)
|
||||||
|
..removeStatusListener(_onStoriesRevealStatus)
|
||||||
|
..dispose();
|
||||||
_scrollController.dispose();
|
_scrollController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
@@ -242,328 +377,340 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
if (_scrollController.hasClients &&
|
if (_scrollController.hasClients &&
|
||||||
_scrollController.offset <= 0) {
|
_scrollController.offset <= 0) {
|
||||||
if (pointerSignal.scrollDelta.dy < 0) {
|
if (pointerSignal.scrollDelta.dy < 0) {
|
||||||
// Scrolled UP (pulling down)
|
_startStoriesAutoReveal(max(_pullRatio, 0.18));
|
||||||
setState(() {
|
|
||||||
_pullRatio = (_pullRatio + 0.2).clamp(0.0, 1.0);
|
|
||||||
});
|
|
||||||
} else if (pointerSignal.scrollDelta.dy > 0 &&
|
} else if (pointerSignal.scrollDelta.dy > 0 &&
|
||||||
_pullRatio > 0) {
|
_pullRatio > 0) {
|
||||||
// Scrolled DOWN (folding up)
|
_startStoriesAutoClose();
|
||||||
setState(() {
|
|
||||||
_pullRatio = (_pullRatio - 0.2).clamp(0.0, 1.0);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: CustomScrollView(
|
child: NotificationListener<ScrollNotification>(
|
||||||
controller: _scrollController,
|
onNotification: _onStoriesScrollNotification,
|
||||||
physics: const BouncingScrollPhysics(
|
child: CustomScrollView(
|
||||||
parent: AlwaysScrollableScrollPhysics(),
|
controller: _scrollController,
|
||||||
),
|
physics: const BouncingScrollPhysics(
|
||||||
slivers: [
|
parent: AlwaysScrollableScrollPhysics(),
|
||||||
SliverToBoxAdapter(
|
),
|
||||||
child: AnimatedContainer(
|
slivers: [
|
||||||
duration: const Duration(milliseconds: 200),
|
SliverToBoxAdapter(
|
||||||
curve: Curves.easeOutCubic,
|
child: ClipRect(
|
||||||
height: _shouldCollapseSearch
|
clipBehavior: Clip.hardEdge,
|
||||||
? 0
|
child: AnimatedSize(
|
||||||
: (100 + (96 * _pullRatio)),
|
duration: const Duration(milliseconds: 200),
|
||||||
color: Colors.transparent,
|
curve: Curves.easeOutCubic,
|
||||||
clipBehavior: Clip.hardEdge,
|
alignment: Alignment.topCenter,
|
||||||
child: AnimatedContainer(
|
child: _shouldCollapseSearch
|
||||||
duration: const Duration(milliseconds: 300),
|
? const SizedBox(
|
||||||
curve: Curves.easeOutCubic,
|
width: double.infinity,
|
||||||
transform: Matrix4.translationValues(
|
height: 0,
|
||||||
0,
|
)
|
||||||
_shouldCollapseSearch ? -100 : 0,
|
: Column(
|
||||||
0,
|
mainAxisSize: MainAxisSize.min,
|
||||||
),
|
crossAxisAlignment:
|
||||||
child: Column(
|
CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(
|
padding: const EdgeInsets.fromLTRB(
|
||||||
20,
|
20,
|
||||||
12,
|
12,
|
||||||
20,
|
20,
|
||||||
2,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment:
|
|
||||||
MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
if (_pullRatio < 0.8)
|
|
||||||
Opacity(
|
|
||||||
opacity: 1.0 - _pullRatio,
|
|
||||||
child: Container(
|
|
||||||
width: 50 * (1.0 - _pullRatio),
|
|
||||||
height: 32,
|
|
||||||
margin: const EdgeInsets.only(
|
|
||||||
right: 8,
|
|
||||||
),
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
_buildFoldedStory(
|
|
||||||
'https://i.pravatar.cc/150?u=dasha',
|
|
||||||
0,
|
|
||||||
),
|
|
||||||
_buildFoldedStory(
|
|
||||||
'https://i.pravatar.cc/150?u=mastika',
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
_buildFoldedStory(
|
|
||||||
'https://i.pravatar.cc/150?u=stas',
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
_sessionState == SessionState.online
|
|
||||||
? (_profile?.firstName ?? 'Чат')
|
|
||||||
: 'Подключение...',
|
|
||||||
style: TextStyle(
|
|
||||||
color: cs.onSurface,
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
fontFamily: 'Outfit',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
PopupMenuButton<int>(
|
|
||||||
icon: Icon(
|
|
||||||
Symbols.more_vert,
|
|
||||||
color: cs.outline,
|
|
||||||
weight: 400,
|
|
||||||
),
|
|
||||||
offset: const Offset(0, 48),
|
|
||||||
elevation: 4,
|
|
||||||
color: cs.surfaceContainerHigh,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(
|
|
||||||
16,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
itemBuilder: (context) => [
|
|
||||||
_buildPopupMenuItem(
|
|
||||||
1,
|
|
||||||
'Кнопка 1',
|
|
||||||
Symbols.settings,
|
|
||||||
),
|
|
||||||
_buildPopupMenuItem(
|
|
||||||
2,
|
2,
|
||||||
'Кнопка 2',
|
|
||||||
Symbols.notifications,
|
|
||||||
),
|
),
|
||||||
_buildPopupMenuItem(
|
child: Row(
|
||||||
3,
|
mainAxisAlignment:
|
||||||
'Кнопка 3',
|
MainAxisAlignment.spaceBetween,
|
||||||
Symbols.shield,
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
if (_pullRatio < 0.8)
|
||||||
|
Opacity(
|
||||||
|
opacity: 1.0 - _pullRatio,
|
||||||
|
child: Container(
|
||||||
|
width:
|
||||||
|
50 *
|
||||||
|
(1.0 - _pullRatio),
|
||||||
|
height: 32,
|
||||||
|
margin:
|
||||||
|
const EdgeInsets.only(
|
||||||
|
right: 8,
|
||||||
|
),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
_buildFoldedStory(
|
||||||
|
'https://i.pravatar.cc/150?u=dasha',
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
_buildFoldedStory(
|
||||||
|
'https://i.pravatar.cc/150?u=mastika',
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
_buildFoldedStory(
|
||||||
|
'https://i.pravatar.cc/150?u=stas',
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
_sessionState ==
|
||||||
|
SessionState.online
|
||||||
|
? (_profile
|
||||||
|
?.firstName ??
|
||||||
|
'Чат')
|
||||||
|
: 'Подключение...',
|
||||||
|
style: TextStyle(
|
||||||
|
color: cs.onSurface,
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight:
|
||||||
|
FontWeight.w600,
|
||||||
|
fontFamily: 'Outfit',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
PopupMenuButton<int>(
|
||||||
|
icon: Icon(
|
||||||
|
Symbols.more_vert,
|
||||||
|
color: cs.outline,
|
||||||
|
weight: 400,
|
||||||
|
),
|
||||||
|
offset: const Offset(0, 48),
|
||||||
|
elevation: 4,
|
||||||
|
color: cs.surfaceContainerHigh,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
itemBuilder: (context) => [
|
||||||
|
_buildPopupMenuItem(
|
||||||
|
1,
|
||||||
|
'Кнопка 1',
|
||||||
|
Symbols.settings,
|
||||||
|
),
|
||||||
|
_buildPopupMenuItem(
|
||||||
|
2,
|
||||||
|
'Кнопка 2',
|
||||||
|
Symbols.notifications,
|
||||||
|
),
|
||||||
|
_buildPopupMenuItem(
|
||||||
|
3,
|
||||||
|
'Кнопка 3',
|
||||||
|
Symbols.shield,
|
||||||
|
),
|
||||||
|
_buildPopupMenuItem(
|
||||||
|
4,
|
||||||
|
'Кнопка 4',
|
||||||
|
Symbols.info,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
_buildPopupMenuItem(
|
),
|
||||||
4,
|
SizedBox(
|
||||||
'Кнопка 4',
|
height: 96 * _pullRatio,
|
||||||
Symbols.info,
|
child: Opacity(
|
||||||
|
opacity: _pullRatio,
|
||||||
|
child: ListView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(
|
||||||
|
horizontal: 20,
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
_buildStoryItem(
|
||||||
|
'Даша',
|
||||||
|
'https://i.pravatar.cc/150?u=dasha',
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
_buildStoryItem(
|
||||||
|
'Мастика',
|
||||||
|
'https://i.pravatar.cc/150?u=mastika',
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
height: 96 * _pullRatio,
|
|
||||||
child: Opacity(
|
|
||||||
opacity: _pullRatio,
|
|
||||||
child: ListView(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 20,
|
|
||||||
),
|
|
||||||
children: [
|
|
||||||
_buildStoryItem(
|
|
||||||
'Даша',
|
|
||||||
'https://i.pravatar.cc/150?u=dasha',
|
|
||||||
true,
|
|
||||||
),
|
),
|
||||||
_buildStoryItem(
|
if (_showCacheWarning)
|
||||||
'Мастика',
|
Padding(
|
||||||
'https://i.pravatar.cc/150?u=mastika',
|
padding: const EdgeInsets.fromLTRB(
|
||||||
false,
|
20,
|
||||||
),
|
0,
|
||||||
],
|
20,
|
||||||
),
|
8,
|
||||||
),
|
),
|
||||||
),
|
child: Container(
|
||||||
if (_showCacheWarning)
|
padding:
|
||||||
Padding(
|
const EdgeInsets.symmetric(
|
||||||
padding: const EdgeInsets.fromLTRB(
|
horizontal: 16,
|
||||||
20,
|
vertical: 8,
|
||||||
0,
|
),
|
||||||
20,
|
decoration: BoxDecoration(
|
||||||
8,
|
color: cs.errorContainer
|
||||||
),
|
.withOpacity(0.3),
|
||||||
child: Container(
|
borderRadius:
|
||||||
padding: const EdgeInsets.symmetric(
|
BorderRadius.circular(12),
|
||||||
horizontal: 16,
|
border: Border.all(
|
||||||
vertical: 8,
|
color: cs.error.withOpacity(
|
||||||
),
|
0.2,
|
||||||
decoration: BoxDecoration(
|
),
|
||||||
color: cs.errorContainer.withOpacity(
|
),
|
||||||
0.3,
|
),
|
||||||
),
|
child: Row(
|
||||||
borderRadius: BorderRadius.circular(12),
|
children: [
|
||||||
border: Border.all(
|
Icon(
|
||||||
color: cs.error.withOpacity(0.2),
|
Symbols.cloud_off,
|
||||||
),
|
size: 18,
|
||||||
),
|
color: cs.error,
|
||||||
child: Row(
|
),
|
||||||
children: [
|
const SizedBox(width: 12),
|
||||||
Icon(
|
const Expanded(
|
||||||
Symbols.cloud_off,
|
child: Text(
|
||||||
size: 18,
|
'Ошибка соединения, сейчас вы смотрите КЕШ',
|
||||||
color: cs.error,
|
style: TextStyle(
|
||||||
),
|
fontSize: 12,
|
||||||
const SizedBox(width: 12),
|
fontWeight:
|
||||||
const Expanded(
|
FontWeight.w500,
|
||||||
child: Text(
|
),
|
||||||
'Ошибка соединения, сейчас вы смотрите КЕШ',
|
),
|
||||||
style: TextStyle(
|
),
|
||||||
fontSize: 12,
|
],
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
Padding(
|
||||||
),
|
padding: const EdgeInsets.fromLTRB(
|
||||||
),
|
20,
|
||||||
),
|
2,
|
||||||
Padding(
|
20,
|
||||||
padding: const EdgeInsets.fromLTRB(
|
8,
|
||||||
20,
|
),
|
||||||
2,
|
child: Container(
|
||||||
20,
|
height: 44,
|
||||||
8,
|
decoration: BoxDecoration(
|
||||||
),
|
color: cs.surfaceContainerHighest,
|
||||||
child: Container(
|
borderRadius:
|
||||||
height: 44,
|
BorderRadius.circular(50),
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: cs.surfaceContainerHighest,
|
|
||||||
borderRadius: BorderRadius.circular(50),
|
|
||||||
),
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Symbols.search,
|
|
||||||
color: cs.outline,
|
|
||||||
size: 20,
|
|
||||||
weight: 400,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
Expanded(
|
|
||||||
child: TextField(
|
|
||||||
style: TextStyle(
|
|
||||||
color: cs.onSurface,
|
|
||||||
fontSize: 15,
|
|
||||||
),
|
),
|
||||||
decoration: InputDecoration(
|
padding: const EdgeInsets.symmetric(
|
||||||
hintText: 'Поиск',
|
horizontal: 16,
|
||||||
hintStyle: TextStyle(
|
),
|
||||||
color: cs.outline,
|
child: Row(
|
||||||
fontSize: 15,
|
children: [
|
||||||
),
|
Icon(
|
||||||
border: InputBorder.none,
|
Symbols.search,
|
||||||
isDense: true,
|
color: cs.outline,
|
||||||
contentPadding: EdgeInsets.zero,
|
size: 20,
|
||||||
|
weight: 400,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
style: TextStyle(
|
||||||
|
color: cs.onSurface,
|
||||||
|
fontSize: 15,
|
||||||
|
),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Поиск',
|
||||||
|
hintStyle: TextStyle(
|
||||||
|
color: cs.outline,
|
||||||
|
fontSize: 15,
|
||||||
|
),
|
||||||
|
border: InputBorder.none,
|
||||||
|
isDense: true,
|
||||||
|
contentPadding:
|
||||||
|
EdgeInsets.zero,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
SliverPadding(
|
||||||
SliverPadding(
|
padding: EdgeInsets.zero,
|
||||||
padding: EdgeInsets.zero,
|
sliver: SliverToBoxAdapter(
|
||||||
sliver: SliverToBoxAdapter(
|
child: AnimatedContainer(
|
||||||
child: AnimatedContainer(
|
duration: const Duration(milliseconds: 300),
|
||||||
duration: const Duration(milliseconds: 300),
|
curve: Curves.easeOutCubic,
|
||||||
curve: Curves.easeOutCubic,
|
height: 48,
|
||||||
height: 48,
|
child: ScrollConfiguration(
|
||||||
child: ScrollConfiguration(
|
behavior: ScrollConfiguration.of(context)
|
||||||
behavior: ScrollConfiguration.of(context)
|
.copyWith(
|
||||||
.copyWith(
|
dragDevices: {
|
||||||
dragDevices: {
|
ui.PointerDeviceKind.touch,
|
||||||
ui.PointerDeviceKind.touch,
|
ui.PointerDeviceKind.mouse,
|
||||||
ui.PointerDeviceKind.mouse,
|
ui.PointerDeviceKind.trackpad,
|
||||||
ui.PointerDeviceKind.trackpad,
|
},
|
||||||
},
|
),
|
||||||
|
child: ListView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 20,
|
||||||
|
vertical: 4,
|
||||||
),
|
),
|
||||||
child: ListView(
|
physics: const BouncingScrollPhysics(),
|
||||||
scrollDirection: Axis.horizontal,
|
children: [
|
||||||
padding: const EdgeInsets.symmetric(
|
_buildFolderChip('Все чаты'),
|
||||||
horizontal: 20,
|
const SizedBox(width: 8),
|
||||||
vertical: 4,
|
_buildFolderChip('Контакты'),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_buildFolderChip('Пидоры'),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_buildFolderChip('Каналы'),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_buildFolderChip('Группы'),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_buildFolderChip('Боты'),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_buildFolderChip('Избранное'),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_buildFolderChip('Архив'),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
physics: const BouncingScrollPhysics(),
|
|
||||||
children: [
|
|
||||||
_buildFolderChip('Все чаты'),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
_buildFolderChip('Контакты'),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
_buildFolderChip('Пидоры'),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
_buildFolderChip('Каналы'),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
_buildFolderChip('Группы'),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
_buildFolderChip('Боты'),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
_buildFolderChip('Избранное'),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
_buildFolderChip('Архив'),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
SliverList(
|
||||||
SliverList(
|
delegate: SliverChildBuilderDelegate(
|
||||||
delegate: SliverChildBuilderDelegate((context, index) {
|
(context, index) {
|
||||||
if (_isInitialLoading) {
|
if (_isInitialLoading) {
|
||||||
return _buildChatShimmer();
|
return _buildChatShimmer();
|
||||||
}
|
}
|
||||||
final chat = _chats[index];
|
final chat = _chats[index];
|
||||||
return _buildChatItem(
|
return _buildChatItem(
|
||||||
chat.id.toString(),
|
chat.id.toString(),
|
||||||
chat.title ?? 'Чат',
|
chat.title ?? 'Чат',
|
||||||
chat.lastMsgText ?? '',
|
chat.lastMsgText ?? '',
|
||||||
_formatTime(chat.lastMsgTime),
|
_formatTime(chat.lastMsgTime),
|
||||||
(chat.iconUrl != null && chat.iconUrl!.isNotEmpty)
|
(chat.iconUrl != null &&
|
||||||
? chat.iconUrl!
|
chat.iconUrl!.isNotEmpty)
|
||||||
: '',
|
? chat.iconUrl!
|
||||||
isOnline: chat.isOnline,
|
: '',
|
||||||
unreadCount: chat.unreadCount,
|
isOnline: chat.isOnline,
|
||||||
isMuted: chat.dontDisturbUntil > 0,
|
unreadCount: chat.unreadCount,
|
||||||
);
|
isMuted: chat.dontDisturbUntil > 0,
|
||||||
}, childCount: _isInitialLoading ? 10 : _chats.length),
|
);
|
||||||
),
|
},
|
||||||
const SliverPadding(
|
childCount: _isInitialLoading ? 10 : _chats.length,
|
||||||
padding: EdgeInsets.only(bottom: 100),
|
),
|
||||||
),
|
),
|
||||||
],
|
const SliverPadding(
|
||||||
), // CustomScrollView
|
padding: EdgeInsets.only(bottom: 100),
|
||||||
), // Listener
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
const CallsTab(),
|
const CallsTab(),
|
||||||
const ContactsTab(),
|
const ContactsTab(),
|
||||||
const SettingsTab(),
|
const SettingsTab(),
|
||||||
@@ -785,31 +932,39 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
final cs = Theme.of(context).colorScheme;
|
final cs = Theme.of(context).colorScheme;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(right: 16),
|
padding: const EdgeInsets.only(right: 16),
|
||||||
child: Column(
|
child: SizedBox(
|
||||||
children: [
|
width: 68,
|
||||||
Container(
|
child: FittedBox(
|
||||||
padding: const EdgeInsets.all(2.5),
|
fit: BoxFit.scaleDown,
|
||||||
decoration: BoxDecoration(
|
alignment: Alignment.topCenter,
|
||||||
shape: BoxShape.circle,
|
child: Column(
|
||||||
border: hasUpdate
|
mainAxisSize: MainAxisSize.min,
|
||||||
? Border.all(color: cs.primary, width: 2)
|
children: [
|
||||||
: Border.all(color: cs.outlineVariant),
|
Container(
|
||||||
),
|
padding: const EdgeInsets.all(2.5),
|
||||||
child: CircleAvatar(
|
decoration: BoxDecoration(
|
||||||
radius: 26,
|
shape: BoxShape.circle,
|
||||||
backgroundImage: NetworkImage(imageUrl),
|
border: hasUpdate
|
||||||
),
|
? Border.all(color: cs.primary, width: 2)
|
||||||
|
: Border.all(color: cs.outlineVariant),
|
||||||
|
),
|
||||||
|
child: CircleAvatar(
|
||||||
|
radius: 26,
|
||||||
|
backgroundImage: NetworkImage(imageUrl),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Text(
|
||||||
|
name,
|
||||||
|
style: TextStyle(
|
||||||
|
color: cs.onSurface,
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
),
|
||||||
Text(
|
|
||||||
name,
|
|
||||||
style: TextStyle(
|
|
||||||
color: cs.onSurface,
|
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user