✨ 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