✨ Better dashboard design for large screen (and mobile device)
This commit is contained in:
		| @@ -72,6 +72,15 @@ class Channel { | ||||
|         'realm_id': realmId, | ||||
|         'is_encrypted': isEncrypted, | ||||
|       }; | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     if (other is! Channel) return false; | ||||
|     return id == other.id; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   int get hashCode => id; | ||||
| } | ||||
|  | ||||
| class ChannelMember { | ||||
|   | ||||
| @@ -12,14 +12,20 @@ class LastReadProvider extends GetxController { | ||||
|  | ||||
|   set feedLastReadAt(int? value) { | ||||
|     if (value == _feedLastReadAt) return; | ||||
|     _feedLastReadAt = max(_feedLastReadAt ?? 0, value ?? 0); | ||||
|     if (value != _feedLastReadAt) _saveToStorage(); | ||||
|     final newValue = max(_feedLastReadAt ?? 0, value ?? 0); | ||||
|     if (newValue != _feedLastReadAt) { | ||||
|       _feedLastReadAt = newValue; | ||||
|       _saveToStorage(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   set messagesLastReadAt(int? value) { | ||||
|     if (value == _messagesLastReadAt) return; | ||||
|     _messagesLastReadAt = max(_messagesLastReadAt ?? 0, value ?? 0); | ||||
|     if (value != _messagesLastReadAt) _saveToStorage(); | ||||
|     final newValue = max(_messagesLastReadAt ?? 0, value ?? 0); | ||||
|     if (newValue != _messagesLastReadAt) { | ||||
|       _messagesLastReadAt = newValue; | ||||
|       _saveToStorage(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   LastReadProvider() { | ||||
|   | ||||
| @@ -1,16 +1,21 @@ | ||||
| import 'dart:developer'; | ||||
| import 'dart:math' hide log; | ||||
|  | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:collection/collection.dart'; | ||||
| import 'package:flutter/material.dart' hide Notification; | ||||
| import 'package:font_awesome_flutter/font_awesome_flutter.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:get/get.dart'; | ||||
| import 'package:google_fonts/google_fonts.dart'; | ||||
| import 'package:intl/intl.dart'; | ||||
| import 'package:solian/exts.dart'; | ||||
| import 'package:solian/models/channel.dart'; | ||||
| import 'package:solian/models/daily_sign.dart'; | ||||
| import 'package:solian/models/event.dart'; | ||||
| import 'package:solian/models/notification.dart'; | ||||
| import 'package:solian/models/pagination.dart'; | ||||
| import 'package:solian/models/post.dart'; | ||||
| import 'package:solian/providers/auth.dart'; | ||||
| import 'package:solian/providers/content/posts.dart'; | ||||
| import 'package:solian/providers/daily_sign.dart'; | ||||
| import 'package:solian/providers/last_read.dart'; | ||||
| @@ -29,6 +34,7 @@ class DashboardScreen extends StatefulWidget { | ||||
| } | ||||
|  | ||||
| class _DashboardScreenState extends State<DashboardScreen> { | ||||
|   late final AuthProvider _auth = Get.find(); | ||||
|   late final LastReadProvider _lastRead = Get.find(); | ||||
|   late final WebSocketProvider _ws = Get.find(); | ||||
|   late final PostProvider _posts = Get.find(); | ||||
| @@ -37,6 +43,10 @@ class _DashboardScreenState extends State<DashboardScreen> { | ||||
|   Color get _unFocusColor => | ||||
|       Theme.of(context).colorScheme.onSurface.withOpacity(0.75); | ||||
|  | ||||
|   List<Notification> get _pendingNotifications => | ||||
|       List<Notification>.from(_ws.notifications) | ||||
|         ..sort((a, b) => b.createdAt.compareTo(a.createdAt)); | ||||
|  | ||||
|   List<Post>? _currentPosts; | ||||
|   int? _currentPostsCount; | ||||
|  | ||||
| @@ -54,6 +64,9 @@ class _DashboardScreenState extends State<DashboardScreen> { | ||||
|   List<Event>? _currentMessages; | ||||
|   int? _currentMessagesCount; | ||||
|  | ||||
|   Map<Channel, List<Event>>? get _currentGroupedMessages => | ||||
|       _currentMessages?.groupListsBy((x) => x.channel!); | ||||
|  | ||||
|   Future<void> _pullMessages() async { | ||||
|     if (_lastRead.messagesLastReadAt == null) return; | ||||
|     log('[Dashboard] Pulling messages with pivot: ${_lastRead.messagesLastReadAt}'); | ||||
| @@ -90,339 +103,379 @@ class _DashboardScreenState extends State<DashboardScreen> { | ||||
|     setState(() => _signingDaily = false); | ||||
|   } | ||||
|  | ||||
|   Future<void> _pullData() async { | ||||
|     if (!_auth.isAuthorized.value) return; | ||||
|     await Future.wait([ | ||||
|       _pullPosts(), | ||||
|       _pullMessages(), | ||||
|       _pullDaily(), | ||||
|     ]); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     _pullPosts(); | ||||
|     _pullMessages(); | ||||
|     _pullDaily(); | ||||
|     _pullData(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final width = MediaQuery.of(context).size.width; | ||||
|  | ||||
|     return ListView( | ||||
|       children: [ | ||||
|         Column( | ||||
|           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|           children: [ | ||||
|             Text('today'.tr, style: Theme.of(context).textTheme.headlineSmall), | ||||
|             Text(DateFormat('yyyy/MM/dd').format(DateTime.now())), | ||||
|           ], | ||||
|         ).paddingOnly(top: 8, left: 18, right: 18, bottom: 12), | ||||
|         Card( | ||||
|           child: ListTile( | ||||
|             leading: AnimatedSwitcher( | ||||
|               switchInCurve: Curves.fastOutSlowIn, | ||||
|               switchOutCurve: Curves.fastOutSlowIn, | ||||
|               duration: const Duration(milliseconds: 300), | ||||
|               transitionBuilder: (child, animation) { | ||||
|                 return ScaleTransition( | ||||
|                   scale: animation, | ||||
|                   child: child, | ||||
|                 ); | ||||
|               }, | ||||
|               child: _signRecord == null | ||||
|                   ? Column( | ||||
|                       mainAxisAlignment: MainAxisAlignment.center, | ||||
|                       children: [ | ||||
|                         Text( | ||||
|                           DateFormat('dd').format(DateTime.now()), | ||||
|                           style: | ||||
|                               GoogleFonts.robotoMono(fontSize: 22, height: 1.2), | ||||
|                         ), | ||||
|                         Text( | ||||
|                           DateFormat('yy/MM').format(DateTime.now()), | ||||
|                           style: GoogleFonts.robotoMono(fontSize: 12), | ||||
|                         ), | ||||
|                       ], | ||||
|                     ) | ||||
|                   : Text( | ||||
|                       _signRecord!.symbol, | ||||
|                       style: GoogleFonts.notoSerifHk(fontSize: 20, height: 1), | ||||
|                     ).paddingSymmetric(horizontal: 9), | ||||
|             ).paddingOnly(left: 4), | ||||
|             title: _signRecord == null | ||||
|                 ? Text('dailySign'.tr) | ||||
|                 : Text(_signRecord!.overviewSuggestion), | ||||
|             subtitle: _signRecord == null | ||||
|                 ? Text('dailySignNone'.tr) | ||||
|                 : Text('+${_signRecord!.resultExperience} EXP'), | ||||
|             trailing: AnimatedSwitcher( | ||||
|               switchInCurve: Curves.fastOutSlowIn, | ||||
|               switchOutCurve: Curves.fastOutSlowIn, | ||||
|               duration: const Duration(milliseconds: 300), | ||||
|               transitionBuilder: (child, animation) { | ||||
|                 return ScaleTransition( | ||||
|                   scale: animation, | ||||
|                   child: child, | ||||
|                 ); | ||||
|               }, | ||||
|               child: _signRecord == null | ||||
|                   ? IconButton( | ||||
|                       tooltip: '上香求签', | ||||
|                       icon: const Icon(Icons.local_fire_department), | ||||
|                       onPressed: _signingDaily ? null : _signDaily, | ||||
|                     ) | ||||
|                   : const SizedBox(), | ||||
|             ), | ||||
|           ), | ||||
|         ).paddingSymmetric(horizontal: 8), | ||||
|         const Divider(thickness: 0.3).paddingSymmetric(vertical: 8), | ||||
|         Obx( | ||||
|           () => Column( | ||||
|     return RefreshIndicator( | ||||
|       onRefresh: _pullData, | ||||
|       child: ListView( | ||||
|         children: [ | ||||
|           Column( | ||||
|             crossAxisAlignment: CrossAxisAlignment.start, | ||||
|             children: [ | ||||
|               Row( | ||||
|                 mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|                 children: [ | ||||
|                   Column( | ||||
|                     crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                     children: [ | ||||
|                       Text( | ||||
|                         'notification'.tr, | ||||
|                         style: Theme.of(context) | ||||
|                             .textTheme | ||||
|                             .titleMedium! | ||||
|                             .copyWith(fontSize: 18), | ||||
|                       ), | ||||
|                       Text( | ||||
|                         'notificationUnreadCount'.trParams({ | ||||
|                           'count': _ws.notifications.length.toString(), | ||||
|                         }), | ||||
|                       ), | ||||
|                     ], | ||||
|                   ), | ||||
|                   IconButton( | ||||
|                     icon: const Icon(Icons.more_horiz), | ||||
|                     onPressed: () { | ||||
|                       showModalBottomSheet( | ||||
|                         useRootNavigator: true, | ||||
|                         isScrollControlled: true, | ||||
|                         context: context, | ||||
|                         builder: (context) => const NotificationScreen(), | ||||
|                       ).then((_) => _ws.notificationUnread.value = 0); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ], | ||||
|               ).paddingOnly(left: 18, right: 18, bottom: 8), | ||||
|               if (_ws.notifications.isNotEmpty) | ||||
|               Text( | ||||
|                 'today'.tr, | ||||
|                 style: Theme.of(context).textTheme.headlineSmall, | ||||
|               ), | ||||
|               Text(DateFormat('yyyy/MM/dd').format(DateTime.now())), | ||||
|             ], | ||||
|           ).paddingOnly(top: 8, left: 18, right: 18, bottom: 12), | ||||
|           Card( | ||||
|             child: ListTile( | ||||
|               leading: AnimatedSwitcher( | ||||
|                 switchInCurve: Curves.fastOutSlowIn, | ||||
|                 switchOutCurve: Curves.fastOutSlowIn, | ||||
|                 duration: const Duration(milliseconds: 300), | ||||
|                 transitionBuilder: (child, animation) { | ||||
|                   return ScaleTransition( | ||||
|                     scale: animation, | ||||
|                     child: child, | ||||
|                   ); | ||||
|                 }, | ||||
|                 child: _signRecord == null | ||||
|                     ? Column( | ||||
|                         mainAxisAlignment: MainAxisAlignment.center, | ||||
|                         children: [ | ||||
|                           Text( | ||||
|                             DateFormat('dd').format(DateTime.now()), | ||||
|                             style: GoogleFonts.robotoMono( | ||||
|                                 fontSize: 22, height: 1.2), | ||||
|                           ), | ||||
|                           Text( | ||||
|                             DateFormat('yy/MM').format(DateTime.now()), | ||||
|                             style: GoogleFonts.robotoMono(fontSize: 12), | ||||
|                           ), | ||||
|                         ], | ||||
|                       ) | ||||
|                     : Text( | ||||
|                         _signRecord!.symbol, | ||||
|                         style: GoogleFonts.notoSerifHk(fontSize: 20, height: 1), | ||||
|                       ).paddingSymmetric(horizontal: 9), | ||||
|               ).paddingOnly(left: 4), | ||||
|               title: _signRecord == null | ||||
|                   ? Text('dailySign'.tr) | ||||
|                   : Text(_signRecord!.overviewSuggestion), | ||||
|               subtitle: _signRecord == null | ||||
|                   ? Text('dailySignNone'.tr) | ||||
|                   : Text('+${_signRecord!.resultExperience} EXP'), | ||||
|               trailing: AnimatedSwitcher( | ||||
|                 switchInCurve: Curves.fastOutSlowIn, | ||||
|                 switchOutCurve: Curves.fastOutSlowIn, | ||||
|                 duration: const Duration(milliseconds: 300), | ||||
|                 transitionBuilder: (child, animation) { | ||||
|                   return ScaleTransition( | ||||
|                     scale: animation, | ||||
|                     child: child, | ||||
|                   ); | ||||
|                 }, | ||||
|                 child: _signRecord == null | ||||
|                     ? IconButton( | ||||
|                         tooltip: '上香求签', | ||||
|                         icon: const Icon(Icons.local_fire_department), | ||||
|                         onPressed: _signingDaily ? null : _signDaily, | ||||
|                       ) | ||||
|                     : const SizedBox(), | ||||
|               ), | ||||
|             ), | ||||
|           ).paddingSymmetric(horizontal: 8), | ||||
|           const Divider(thickness: 0.3).paddingSymmetric(vertical: 8), | ||||
|           // Unread notifications | ||||
|           Obx( | ||||
|             () => Column( | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|               children: [ | ||||
|                 Row( | ||||
|                   mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|                   children: [ | ||||
|                     Column( | ||||
|                       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                       children: [ | ||||
|                         Text( | ||||
|                           'notification'.tr, | ||||
|                           style: Theme.of(context) | ||||
|                               .textTheme | ||||
|                               .titleMedium! | ||||
|                               .copyWith(fontSize: 18), | ||||
|                         ), | ||||
|                         Text( | ||||
|                           'notificationUnreadCount'.trParams({ | ||||
|                             'count': _ws.notifications.length.toString(), | ||||
|                           }), | ||||
|                         ), | ||||
|                       ], | ||||
|                     ), | ||||
|                     IconButton( | ||||
|                       icon: const Icon(Icons.more_horiz), | ||||
|                       onPressed: () { | ||||
|                         showModalBottomSheet( | ||||
|                           useRootNavigator: true, | ||||
|                           isScrollControlled: true, | ||||
|                           context: context, | ||||
|                           builder: (context) => const NotificationScreen(), | ||||
|                         ).then((_) => _ws.notificationUnread.value = 0); | ||||
|                       }, | ||||
|                     ), | ||||
|                   ], | ||||
|                 ).paddingOnly(left: 18, right: 18, bottom: 8), | ||||
|                 if (_ws.notifications.isNotEmpty) | ||||
|                   SizedBox( | ||||
|                     height: 76, | ||||
|                     child: ListView.separated( | ||||
|                       scrollDirection: Axis.horizontal, | ||||
|                       padding: const EdgeInsets.symmetric(horizontal: 8), | ||||
|                       itemCount: min(_pendingNotifications.length, 10), | ||||
|                       itemBuilder: (context, idx) { | ||||
|                         final x = _pendingNotifications[idx]; | ||||
|                         return SizedBox( | ||||
|                           width: min(360, width), | ||||
|                           child: Card( | ||||
|                             child: ListTile( | ||||
|                               contentPadding: const EdgeInsets.symmetric( | ||||
|                                 horizontal: 24, | ||||
|                                 vertical: 4, | ||||
|                               ), | ||||
|                               title: Text( | ||||
|                                 x.title, | ||||
|                                 maxLines: 1, | ||||
|                                 overflow: TextOverflow.ellipsis, | ||||
|                               ), | ||||
|                               subtitle: Column( | ||||
|                                 crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                                 children: [ | ||||
|                                   if (x.subtitle != null) Text(x.subtitle!), | ||||
|                                   Text(x.body), | ||||
|                                 ], | ||||
|                               ), | ||||
|                             ), | ||||
|                           ), | ||||
|                         ); | ||||
|                       }, | ||||
|                       separatorBuilder: (_, __) => const Gap(4), | ||||
|                     ), | ||||
|                   ) | ||||
|                 else | ||||
|                   Card( | ||||
|                     child: ListTile( | ||||
|                       contentPadding: | ||||
|                           const EdgeInsets.symmetric(horizontal: 24), | ||||
|                       trailing: const Icon(Icons.inbox_outlined), | ||||
|                       title: Text('notifyEmpty'.tr), | ||||
|                       subtitle: Text('notifyEmptyCaption'.tr), | ||||
|                     ), | ||||
|                   ).paddingSymmetric(horizontal: 8), | ||||
|               ], | ||||
|             ).paddingOnly(bottom: 12), | ||||
|           ), | ||||
|  | ||||
|           /// Unread friends / followed people posts | ||||
|           if (_currentPosts?.isNotEmpty ?? false) | ||||
|             Column( | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|               children: [ | ||||
|                 Row( | ||||
|                   mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|                   children: [ | ||||
|                     Column( | ||||
|                       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                       children: [ | ||||
|                         Text( | ||||
|                           'feed'.tr, | ||||
|                           style: Theme.of(context) | ||||
|                               .textTheme | ||||
|                               .titleMedium! | ||||
|                               .copyWith(fontSize: 18), | ||||
|                         ), | ||||
|                         Text( | ||||
|                           'feedUnreadCount'.trParams({ | ||||
|                             'count': (_currentPostsCount ?? 0).toString(), | ||||
|                           }), | ||||
|                         ), | ||||
|                       ], | ||||
|                     ), | ||||
|                     IconButton( | ||||
|                       icon: const Icon(Icons.arrow_forward), | ||||
|                       onPressed: () { | ||||
|                         AppRouter.instance.goNamed('feed'); | ||||
|                       }, | ||||
|                     ), | ||||
|                   ], | ||||
|                 ).paddingOnly(left: 18, right: 18, bottom: 8), | ||||
|                 SizedBox( | ||||
|                   height: 76, | ||||
|                   width: width, | ||||
|                   height: 360, | ||||
|                   child: ListView.builder( | ||||
|                     scrollDirection: Axis.horizontal, | ||||
|                     itemCount: min(_ws.notifications.length, 3), | ||||
|                     itemCount: _currentPosts!.length, | ||||
|                     itemBuilder: (context, idx) { | ||||
|                       final x = _ws.notifications[idx]; | ||||
|                       final item = _currentPosts![idx]; | ||||
|                       return SizedBox( | ||||
|                         width: width, | ||||
|                         width: min(480, width), | ||||
|                         child: Card( | ||||
|                           child: ListTile( | ||||
|                             contentPadding: const EdgeInsets.symmetric( | ||||
|                               horizontal: 24, | ||||
|                               vertical: 4, | ||||
|                             ), | ||||
|                             title: Text(x.title), | ||||
|                             subtitle: Column( | ||||
|                               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                               children: [ | ||||
|                                 if (x.subtitle != null) Text(x.subtitle!), | ||||
|                                 Text(x.body), | ||||
|                               ], | ||||
|                             ), | ||||
|                           child: PostListEntryWidget( | ||||
|                             item: item, | ||||
|                             isClickable: true, | ||||
|                             isShowEmbed: true, | ||||
|                             isNestedClickable: true, | ||||
|                             onUpdate: (_) { | ||||
|                               _pullPosts(); | ||||
|                             }, | ||||
|                             backgroundColor: Theme.of(context) | ||||
|                                 .colorScheme | ||||
|                                 .surfaceContainerLow, | ||||
|                           ), | ||||
|                         ).paddingSymmetric(horizontal: 8), | ||||
|                       ); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ) | ||||
|               else | ||||
|                 Card( | ||||
|                   child: ListTile( | ||||
|                     contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||
|                     trailing: const Icon(Icons.inbox_outlined), | ||||
|                     title: Text('notifyEmpty'.tr), | ||||
|                     subtitle: Text('notifyEmptyCaption'.tr), | ||||
|                   ), | ||||
|                 ).paddingSymmetric(horizontal: 8), | ||||
|             ], | ||||
|           ).paddingOnly(bottom: 12), | ||||
|         ), | ||||
|         if (_currentPosts?.isNotEmpty ?? false) | ||||
|           Column( | ||||
|             crossAxisAlignment: CrossAxisAlignment.start, | ||||
|             children: [ | ||||
|               Row( | ||||
|                 mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|                 children: [ | ||||
|                   Column( | ||||
|                     crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                     children: [ | ||||
|                       Text( | ||||
|                         'feed'.tr, | ||||
|                         style: Theme.of(context) | ||||
|                             .textTheme | ||||
|                             .titleMedium! | ||||
|                             .copyWith(fontSize: 18), | ||||
|                       ), | ||||
|                       Text( | ||||
|                         'feedUnreadCount'.trParams({ | ||||
|                           'count': (_currentPostsCount ?? 0).toString(), | ||||
|                         }), | ||||
|                       ), | ||||
|                     ], | ||||
|                   ), | ||||
|                   IconButton( | ||||
|                     icon: const Icon(Icons.arrow_forward), | ||||
|                     onPressed: () { | ||||
|                       AppRouter.instance.goNamed('feed'); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ], | ||||
|               ).paddingOnly(left: 18, right: 18, bottom: 8), | ||||
|               SizedBox( | ||||
|                 height: 360, | ||||
|                 width: width, | ||||
|                 child: ListView.builder( | ||||
|                   scrollDirection: Axis.horizontal, | ||||
|                   itemCount: _currentPosts!.length, | ||||
|                   itemBuilder: (context, idx) { | ||||
|                     final item = _currentPosts![idx]; | ||||
|                     return SizedBox( | ||||
|                       width: width, | ||||
|                       child: Card( | ||||
|                         child: PostListEntryWidget( | ||||
|                           item: item, | ||||
|                           isClickable: true, | ||||
|                           isShowEmbed: true, | ||||
|                           isNestedClickable: true, | ||||
|                           onUpdate: (_) { | ||||
|                             _pullPosts(); | ||||
|                           }, | ||||
|                           backgroundColor: | ||||
|                               Theme.of(context).colorScheme.surfaceContainerLow, | ||||
|               ], | ||||
|             ), | ||||
|  | ||||
|           /// Unread messages part | ||||
|           if (_currentMessages?.isNotEmpty ?? false) | ||||
|             Column( | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|               children: [ | ||||
|                 Row( | ||||
|                   mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|                   children: [ | ||||
|                     Column( | ||||
|                       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                       children: [ | ||||
|                         Text( | ||||
|                           'messages'.tr, | ||||
|                           style: Theme.of(context) | ||||
|                               .textTheme | ||||
|                               .titleMedium! | ||||
|                               .copyWith(fontSize: 18), | ||||
|                         ), | ||||
|                       ).paddingSymmetric(horizontal: 8), | ||||
|                     ); | ||||
|                   }, | ||||
|                 ), | ||||
|               ) | ||||
|             ], | ||||
|           ), | ||||
|         if (_currentMessages?.isNotEmpty ?? false) | ||||
|           Column( | ||||
|             crossAxisAlignment: CrossAxisAlignment.start, | ||||
|             children: [ | ||||
|               Row( | ||||
|                 mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|                 children: [ | ||||
|                   Column( | ||||
|                     crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                     children: [ | ||||
|                       Text( | ||||
|                         'messages'.tr, | ||||
|                         style: Theme.of(context) | ||||
|                             .textTheme | ||||
|                             .titleMedium! | ||||
|                             .copyWith(fontSize: 18), | ||||
|                       ), | ||||
|                       Text( | ||||
|                         'messagesUnreadCount'.trParams({ | ||||
|                           'count': (_currentMessagesCount ?? 0).toString(), | ||||
|                         }), | ||||
|                       ), | ||||
|                     ], | ||||
|                   ), | ||||
|                   IconButton( | ||||
|                     icon: const Icon(Icons.arrow_forward), | ||||
|                     onPressed: () { | ||||
|                       AppRouter.instance.goNamed('chat'); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ], | ||||
|               ).paddingOnly(left: 18, right: 18, bottom: 8), | ||||
|               SizedBox( | ||||
|                 height: 240, | ||||
|                 width: width, | ||||
|                 child: ListView.builder( | ||||
|                   scrollDirection: Axis.horizontal, | ||||
|                   itemCount: _currentMessages!.length, | ||||
|                   itemBuilder: (context, idx) { | ||||
|                     final item = _currentMessages![idx]; | ||||
|                     return SizedBox( | ||||
|                       width: width, | ||||
|                       child: Card( | ||||
|                         child: Column( | ||||
|                           children: [ | ||||
|                             ListTile( | ||||
|                               tileColor: Theme.of(context) | ||||
|                                   .colorScheme | ||||
|                                   .surfaceContainerHigh, | ||||
|                               shape: const RoundedRectangleBorder( | ||||
|                                   borderRadius: BorderRadius.only( | ||||
|                                 topLeft: Radius.circular(8), | ||||
|                                 topRight: Radius.circular(8), | ||||
|                               )), | ||||
|                               leading: CircleAvatar( | ||||
|                                 backgroundColor: item.channel!.realmId == null | ||||
|                                     ? Theme.of(context).colorScheme.primary | ||||
|                                     : Colors.transparent, | ||||
|                                 radius: 20, | ||||
|                                 child: FaIcon( | ||||
|                                   FontAwesomeIcons.hashtag, | ||||
|                                   color: item.channel!.realmId == null | ||||
|                                       ? Theme.of(context).colorScheme.onPrimary | ||||
|                                       : Theme.of(context).colorScheme.primary, | ||||
|                                   size: 16, | ||||
|                         Text( | ||||
|                           'messagesUnreadCount'.trParams({ | ||||
|                             'count': (_currentMessagesCount ?? 0).toString(), | ||||
|                           }), | ||||
|                         ), | ||||
|                       ], | ||||
|                     ), | ||||
|                     IconButton( | ||||
|                       icon: const Icon(Icons.arrow_forward), | ||||
|                       onPressed: () { | ||||
|                         AppRouter.instance.goNamed('chat'); | ||||
|                       }, | ||||
|                     ), | ||||
|                   ], | ||||
|                 ).paddingOnly(left: 18, right: 18, bottom: 8), | ||||
|                 SizedBox( | ||||
|                   height: 360, | ||||
|                   child: ListView.builder( | ||||
|                     scrollDirection: Axis.horizontal, | ||||
|                     itemCount: _currentGroupedMessages!.length, | ||||
|                     itemBuilder: (context, idx) { | ||||
|                       final channel = | ||||
|                           _currentGroupedMessages!.keys.elementAt(idx); | ||||
|                       final itemList = | ||||
|                           _currentGroupedMessages!.values.elementAt(idx); | ||||
|                       return SizedBox( | ||||
|                         width: min(520, width), | ||||
|                         child: Card( | ||||
|                           child: Column( | ||||
|                             children: [ | ||||
|                               ListTile( | ||||
|                                 tileColor: Theme.of(context) | ||||
|                                     .colorScheme | ||||
|                                     .surfaceContainerHigh, | ||||
|                                 shape: const RoundedRectangleBorder( | ||||
|                                     borderRadius: BorderRadius.only( | ||||
|                                   topLeft: Radius.circular(8), | ||||
|                                   topRight: Radius.circular(8), | ||||
|                                 )), | ||||
|                                 leading: CircleAvatar( | ||||
|                                   backgroundColor: channel.realmId == null | ||||
|                                       ? Theme.of(context).colorScheme.primary | ||||
|                                       : Colors.transparent, | ||||
|                                   radius: 20, | ||||
|                                   child: FaIcon( | ||||
|                                     FontAwesomeIcons.hashtag, | ||||
|                                     color: channel.realmId == null | ||||
|                                         ? Theme.of(context) | ||||
|                                             .colorScheme | ||||
|                                             .onPrimary | ||||
|                                         : Theme.of(context).colorScheme.primary, | ||||
|                                     size: 16, | ||||
|                                   ), | ||||
|                                 ), | ||||
|                                 contentPadding: | ||||
|                                     const EdgeInsets.symmetric(horizontal: 16), | ||||
|                                 title: Text(channel.name), | ||||
|                                 subtitle: Text(channel.description), | ||||
|                                 onTap: () { | ||||
|                                   AppRouter.instance.pushNamed( | ||||
|                                     'channelChat', | ||||
|                                     pathParameters: {'alias': channel.alias}, | ||||
|                                     queryParameters: { | ||||
|                                       if (channel.realmId != null) | ||||
|                                         'realm': channel.realm!.alias, | ||||
|                                     }, | ||||
|                                   ); | ||||
|                                 }, | ||||
|                               ), | ||||
|                               Expanded( | ||||
|                                 child: ListView.builder( | ||||
|                                   itemCount: itemList.length, | ||||
|                                   itemBuilder: (context, idx) { | ||||
|                                     final item = itemList[idx]; | ||||
|                                     return ChatEvent(item: item).paddingOnly( | ||||
|                                         bottom: 8, top: 16, left: 8, right: 8); | ||||
|                                   }, | ||||
|                                 ), | ||||
|                               ), | ||||
|                               contentPadding: | ||||
|                                   const EdgeInsets.symmetric(horizontal: 16), | ||||
|                               title: Text(item.channel!.name), | ||||
|                               subtitle: Text(item.channel!.description), | ||||
|                               onTap: () { | ||||
|                                 AppRouter.instance.pushNamed( | ||||
|                                   'channelChat', | ||||
|                                   pathParameters: { | ||||
|                                     'alias': item.channel!.alias | ||||
|                                   }, | ||||
|                                   queryParameters: { | ||||
|                                     if (item.channel!.realmId != null) | ||||
|                                       'realm': item.channel!.realm!.alias, | ||||
|                                   }, | ||||
|                                 ); | ||||
|                               }, | ||||
|                             ), | ||||
|                             ChatEvent(item: item).paddingOnly( | ||||
|                                 bottom: 8, top: 16, left: 8, right: 8), | ||||
|                           ], | ||||
|                         ), | ||||
|                       ).paddingSymmetric(horizontal: 8), | ||||
|                     ); | ||||
|                   }, | ||||
|                 ), | ||||
|                             ], | ||||
|                           ), | ||||
|                         ).paddingSymmetric(horizontal: 8), | ||||
|                       ); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ) | ||||
|               ], | ||||
|             ), | ||||
|           Column( | ||||
|             mainAxisAlignment: MainAxisAlignment.center, | ||||
|             children: [ | ||||
|               Text( | ||||
|                 'Powered by Solar Network', | ||||
|                 style: TextStyle(color: _unFocusColor, fontSize: 12), | ||||
|               ), | ||||
|               Text( | ||||
|                 'dashboardFooter'.tr, | ||||
|                 style: [const Locale('zh', 'CN'), const Locale('zh', 'HK')] | ||||
|                         .contains(Get.deviceLocale) | ||||
|                     ? GoogleFonts.notoSerifHk( | ||||
|                         color: _unFocusColor, | ||||
|                         fontSize: 12, | ||||
|                       ) | ||||
|                     : TextStyle( | ||||
|                         color: _unFocusColor, | ||||
|                         fontSize: 12, | ||||
|                       ), | ||||
|               ) | ||||
|             ], | ||||
|           ), | ||||
|         Column( | ||||
|           mainAxisAlignment: MainAxisAlignment.center, | ||||
|           children: [ | ||||
|             Text( | ||||
|               'Powered by Solar Network', | ||||
|               style: TextStyle(color: _unFocusColor, fontSize: 12), | ||||
|             ), | ||||
|             Text( | ||||
|               'dashboardFooter'.tr, | ||||
|               style: GoogleFonts.notoSerifHk( | ||||
|                 color: _unFocusColor, | ||||
|                 fontSize: 12, | ||||
|               ), | ||||
|             ) | ||||
|           ], | ||||
|         ).paddingAll(8), | ||||
|       ], | ||||
|           ).paddingAll(8), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -26,7 +26,7 @@ class AppNavigationDrawer extends StatefulWidget { | ||||
|  | ||||
| class _AppNavigationDrawerState extends State<AppNavigationDrawer> | ||||
|     with TickerProviderStateMixin { | ||||
|   bool _isCollapsed = false; | ||||
|   bool _isCollapsed = true; | ||||
|  | ||||
|   late final AnimationController _drawerAnimationController = | ||||
|       AnimationController( | ||||
|   | ||||
| @@ -885,6 +885,14 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.3.0" | ||||
|   gap: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: gap | ||||
|       sha256: f19387d4e32f849394758b91377f9153a1b41d79513ef7668c088c77dbc6955d | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.0.1" | ||||
|   get: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|   | ||||
| @@ -76,6 +76,7 @@ dependencies: | ||||
|   google_fonts: ^6.2.1 | ||||
|   freezed_annotation: ^2.4.4 | ||||
|   json_annotation: ^4.9.0 | ||||
|   gap: ^3.0.1 | ||||
|  | ||||
| dev_dependencies: | ||||
|   flutter_test: | ||||
|   | ||||
		Reference in New Issue
	
	Block a user