Better dashboard design for large screen (and mobile device)

This commit is contained in:
LittleSheep 2024-09-05 20:25:17 +08:00
parent a75f42e440
commit 1a0721ba3a
6 changed files with 388 additions and 311 deletions

View File

@ -72,6 +72,15 @@ class Channel {
'realm_id': realmId, 'realm_id': realmId,
'is_encrypted': isEncrypted, '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 { class ChannelMember {

View File

@ -12,14 +12,20 @@ class LastReadProvider extends GetxController {
set feedLastReadAt(int? value) { set feedLastReadAt(int? value) {
if (value == _feedLastReadAt) return; if (value == _feedLastReadAt) return;
_feedLastReadAt = max(_feedLastReadAt ?? 0, value ?? 0); final newValue = max(_feedLastReadAt ?? 0, value ?? 0);
if (value != _feedLastReadAt) _saveToStorage(); if (newValue != _feedLastReadAt) {
_feedLastReadAt = newValue;
_saveToStorage();
}
} }
set messagesLastReadAt(int? value) { set messagesLastReadAt(int? value) {
if (value == _messagesLastReadAt) return; if (value == _messagesLastReadAt) return;
_messagesLastReadAt = max(_messagesLastReadAt ?? 0, value ?? 0); final newValue = max(_messagesLastReadAt ?? 0, value ?? 0);
if (value != _messagesLastReadAt) _saveToStorage(); if (newValue != _messagesLastReadAt) {
_messagesLastReadAt = newValue;
_saveToStorage();
}
} }
LastReadProvider() { LastReadProvider() {

View File

@ -1,16 +1,21 @@
import 'dart:developer'; import 'dart:developer';
import 'dart:math' hide log; 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:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/models/channel.dart';
import 'package:solian/models/daily_sign.dart'; import 'package:solian/models/daily_sign.dart';
import 'package:solian/models/event.dart'; import 'package:solian/models/event.dart';
import 'package:solian/models/notification.dart';
import 'package:solian/models/pagination.dart'; import 'package:solian/models/pagination.dart';
import 'package:solian/models/post.dart'; import 'package:solian/models/post.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/posts.dart'; import 'package:solian/providers/content/posts.dart';
import 'package:solian/providers/daily_sign.dart'; import 'package:solian/providers/daily_sign.dart';
import 'package:solian/providers/last_read.dart'; import 'package:solian/providers/last_read.dart';
@ -29,6 +34,7 @@ class DashboardScreen extends StatefulWidget {
} }
class _DashboardScreenState extends State<DashboardScreen> { class _DashboardScreenState extends State<DashboardScreen> {
late final AuthProvider _auth = Get.find();
late final LastReadProvider _lastRead = Get.find(); late final LastReadProvider _lastRead = Get.find();
late final WebSocketProvider _ws = Get.find(); late final WebSocketProvider _ws = Get.find();
late final PostProvider _posts = Get.find(); late final PostProvider _posts = Get.find();
@ -37,6 +43,10 @@ class _DashboardScreenState extends State<DashboardScreen> {
Color get _unFocusColor => Color get _unFocusColor =>
Theme.of(context).colorScheme.onSurface.withOpacity(0.75); 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; List<Post>? _currentPosts;
int? _currentPostsCount; int? _currentPostsCount;
@ -54,6 +64,9 @@ class _DashboardScreenState extends State<DashboardScreen> {
List<Event>? _currentMessages; List<Event>? _currentMessages;
int? _currentMessagesCount; int? _currentMessagesCount;
Map<Channel, List<Event>>? get _currentGroupedMessages =>
_currentMessages?.groupListsBy((x) => x.channel!);
Future<void> _pullMessages() async { Future<void> _pullMessages() async {
if (_lastRead.messagesLastReadAt == null) return; if (_lastRead.messagesLastReadAt == null) return;
log('[Dashboard] Pulling messages with pivot: ${_lastRead.messagesLastReadAt}'); log('[Dashboard] Pulling messages with pivot: ${_lastRead.messagesLastReadAt}');
@ -90,24 +103,36 @@ class _DashboardScreenState extends State<DashboardScreen> {
setState(() => _signingDaily = false); setState(() => _signingDaily = false);
} }
Future<void> _pullData() async {
if (!_auth.isAuthorized.value) return;
await Future.wait([
_pullPosts(),
_pullMessages(),
_pullDaily(),
]);
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_pullPosts(); _pullData();
_pullMessages();
_pullDaily();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width; final width = MediaQuery.of(context).size.width;
return ListView( return RefreshIndicator(
onRefresh: _pullData,
child: ListView(
children: [ children: [
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('today'.tr, style: Theme.of(context).textTheme.headlineSmall), Text(
'today'.tr,
style: Theme.of(context).textTheme.headlineSmall,
),
Text(DateFormat('yyyy/MM/dd').format(DateTime.now())), Text(DateFormat('yyyy/MM/dd').format(DateTime.now())),
], ],
).paddingOnly(top: 8, left: 18, right: 18, bottom: 12), ).paddingOnly(top: 8, left: 18, right: 18, bottom: 12),
@ -129,8 +154,8 @@ class _DashboardScreenState extends State<DashboardScreen> {
children: [ children: [
Text( Text(
DateFormat('dd').format(DateTime.now()), DateFormat('dd').format(DateTime.now()),
style: style: GoogleFonts.robotoMono(
GoogleFonts.robotoMono(fontSize: 22, height: 1.2), fontSize: 22, height: 1.2),
), ),
Text( Text(
DateFormat('yy/MM').format(DateTime.now()), DateFormat('yy/MM').format(DateTime.now()),
@ -170,6 +195,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
), ),
).paddingSymmetric(horizontal: 8), ).paddingSymmetric(horizontal: 8),
const Divider(thickness: 0.3).paddingSymmetric(vertical: 8), const Divider(thickness: 0.3).paddingSymmetric(vertical: 8),
// Unread notifications
Obx( Obx(
() => Column( () => Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -210,21 +236,25 @@ class _DashboardScreenState extends State<DashboardScreen> {
if (_ws.notifications.isNotEmpty) if (_ws.notifications.isNotEmpty)
SizedBox( SizedBox(
height: 76, height: 76,
width: width, child: ListView.separated(
child: ListView.builder(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
itemCount: min(_ws.notifications.length, 3), padding: const EdgeInsets.symmetric(horizontal: 8),
itemCount: min(_pendingNotifications.length, 10),
itemBuilder: (context, idx) { itemBuilder: (context, idx) {
final x = _ws.notifications[idx]; final x = _pendingNotifications[idx];
return SizedBox( return SizedBox(
width: width, width: min(360, width),
child: Card( child: Card(
child: ListTile( child: ListTile(
contentPadding: const EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
horizontal: 24, horizontal: 24,
vertical: 4, vertical: 4,
), ),
title: Text(x.title), title: Text(
x.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Column( subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -233,15 +263,17 @@ class _DashboardScreenState extends State<DashboardScreen> {
], ],
), ),
), ),
).paddingSymmetric(horizontal: 8), ),
); );
}, },
separatorBuilder: (_, __) => const Gap(4),
), ),
) )
else else
Card( Card(
child: ListTile( child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding:
const EdgeInsets.symmetric(horizontal: 24),
trailing: const Icon(Icons.inbox_outlined), trailing: const Icon(Icons.inbox_outlined),
title: Text('notifyEmpty'.tr), title: Text('notifyEmpty'.tr),
subtitle: Text('notifyEmptyCaption'.tr), subtitle: Text('notifyEmptyCaption'.tr),
@ -250,6 +282,8 @@ class _DashboardScreenState extends State<DashboardScreen> {
], ],
).paddingOnly(bottom: 12), ).paddingOnly(bottom: 12),
), ),
/// Unread friends / followed people posts
if (_currentPosts?.isNotEmpty ?? false) if (_currentPosts?.isNotEmpty ?? false)
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -284,14 +318,13 @@ class _DashboardScreenState extends State<DashboardScreen> {
).paddingOnly(left: 18, right: 18, bottom: 8), ).paddingOnly(left: 18, right: 18, bottom: 8),
SizedBox( SizedBox(
height: 360, height: 360,
width: width,
child: ListView.builder( child: ListView.builder(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
itemCount: _currentPosts!.length, itemCount: _currentPosts!.length,
itemBuilder: (context, idx) { itemBuilder: (context, idx) {
final item = _currentPosts![idx]; final item = _currentPosts![idx];
return SizedBox( return SizedBox(
width: width, width: min(480, width),
child: Card( child: Card(
child: PostListEntryWidget( child: PostListEntryWidget(
item: item, item: item,
@ -301,8 +334,9 @@ class _DashboardScreenState extends State<DashboardScreen> {
onUpdate: (_) { onUpdate: (_) {
_pullPosts(); _pullPosts();
}, },
backgroundColor: backgroundColor: Theme.of(context)
Theme.of(context).colorScheme.surfaceContainerLow, .colorScheme
.surfaceContainerLow,
), ),
).paddingSymmetric(horizontal: 8), ).paddingSymmetric(horizontal: 8),
); );
@ -311,6 +345,8 @@ class _DashboardScreenState extends State<DashboardScreen> {
) )
], ],
), ),
/// Unread messages part
if (_currentMessages?.isNotEmpty ?? false) if (_currentMessages?.isNotEmpty ?? false)
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -344,15 +380,17 @@ class _DashboardScreenState extends State<DashboardScreen> {
], ],
).paddingOnly(left: 18, right: 18, bottom: 8), ).paddingOnly(left: 18, right: 18, bottom: 8),
SizedBox( SizedBox(
height: 240, height: 360,
width: width,
child: ListView.builder( child: ListView.builder(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
itemCount: _currentMessages!.length, itemCount: _currentGroupedMessages!.length,
itemBuilder: (context, idx) { itemBuilder: (context, idx) {
final item = _currentMessages![idx]; final channel =
_currentGroupedMessages!.keys.elementAt(idx);
final itemList =
_currentGroupedMessages!.values.elementAt(idx);
return SizedBox( return SizedBox(
width: width, width: min(520, width),
child: Card( child: Card(
child: Column( child: Column(
children: [ children: [
@ -366,37 +404,45 @@ class _DashboardScreenState extends State<DashboardScreen> {
topRight: Radius.circular(8), topRight: Radius.circular(8),
)), )),
leading: CircleAvatar( leading: CircleAvatar(
backgroundColor: item.channel!.realmId == null backgroundColor: channel.realmId == null
? Theme.of(context).colorScheme.primary ? Theme.of(context).colorScheme.primary
: Colors.transparent, : Colors.transparent,
radius: 20, radius: 20,
child: FaIcon( child: FaIcon(
FontAwesomeIcons.hashtag, FontAwesomeIcons.hashtag,
color: item.channel!.realmId == null color: channel.realmId == null
? Theme.of(context).colorScheme.onPrimary ? Theme.of(context)
.colorScheme
.onPrimary
: Theme.of(context).colorScheme.primary, : Theme.of(context).colorScheme.primary,
size: 16, size: 16,
), ),
), ),
contentPadding: contentPadding:
const EdgeInsets.symmetric(horizontal: 16), const EdgeInsets.symmetric(horizontal: 16),
title: Text(item.channel!.name), title: Text(channel.name),
subtitle: Text(item.channel!.description), subtitle: Text(channel.description),
onTap: () { onTap: () {
AppRouter.instance.pushNamed( AppRouter.instance.pushNamed(
'channelChat', 'channelChat',
pathParameters: { pathParameters: {'alias': channel.alias},
'alias': item.channel!.alias
},
queryParameters: { queryParameters: {
if (item.channel!.realmId != null) if (channel.realmId != null)
'realm': item.channel!.realm!.alias, 'realm': channel.realm!.alias,
}, },
); );
}, },
), ),
ChatEvent(item: item).paddingOnly( Expanded(
bottom: 8, top: 16, left: 8, right: 8), 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);
},
),
),
], ],
), ),
).paddingSymmetric(horizontal: 8), ).paddingSymmetric(horizontal: 8),
@ -415,7 +461,13 @@ class _DashboardScreenState extends State<DashboardScreen> {
), ),
Text( Text(
'dashboardFooter'.tr, 'dashboardFooter'.tr,
style: GoogleFonts.notoSerifHk( style: [const Locale('zh', 'CN'), const Locale('zh', 'HK')]
.contains(Get.deviceLocale)
? GoogleFonts.notoSerifHk(
color: _unFocusColor,
fontSize: 12,
)
: TextStyle(
color: _unFocusColor, color: _unFocusColor,
fontSize: 12, fontSize: 12,
), ),
@ -423,6 +475,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
], ],
).paddingAll(8), ).paddingAll(8),
], ],
),
); );
} }
} }

View File

@ -26,7 +26,7 @@ class AppNavigationDrawer extends StatefulWidget {
class _AppNavigationDrawerState extends State<AppNavigationDrawer> class _AppNavigationDrawerState extends State<AppNavigationDrawer>
with TickerProviderStateMixin { with TickerProviderStateMixin {
bool _isCollapsed = false; bool _isCollapsed = true;
late final AnimationController _drawerAnimationController = late final AnimationController _drawerAnimationController =
AnimationController( AnimationController(

View File

@ -885,6 +885,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.0" version: "2.3.0"
gap:
dependency: "direct main"
description:
name: gap
sha256: f19387d4e32f849394758b91377f9153a1b41d79513ef7668c088c77dbc6955d
url: "https://pub.dev"
source: hosted
version: "3.0.1"
get: get:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -76,6 +76,7 @@ dependencies:
google_fonts: ^6.2.1 google_fonts: ^6.2.1
freezed_annotation: ^2.4.4 freezed_annotation: ^2.4.4
json_annotation: ^4.9.0 json_annotation: ^4.9.0
gap: ^3.0.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: