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,
'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 {

View File

@ -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() {

View File

@ -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,24 +103,36 @@ 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(
return RefreshIndicator(
onRefresh: _pullData,
child: ListView(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
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())),
],
).paddingOnly(top: 8, left: 18, right: 18, bottom: 12),
@ -129,8 +154,8 @@ class _DashboardScreenState extends State<DashboardScreen> {
children: [
Text(
DateFormat('dd').format(DateTime.now()),
style:
GoogleFonts.robotoMono(fontSize: 22, height: 1.2),
style: GoogleFonts.robotoMono(
fontSize: 22, height: 1.2),
),
Text(
DateFormat('yy/MM').format(DateTime.now()),
@ -170,6 +195,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
),
).paddingSymmetric(horizontal: 8),
const Divider(thickness: 0.3).paddingSymmetric(vertical: 8),
// Unread notifications
Obx(
() => Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -210,21 +236,25 @@ class _DashboardScreenState extends State<DashboardScreen> {
if (_ws.notifications.isNotEmpty)
SizedBox(
height: 76,
width: width,
child: ListView.builder(
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: min(_ws.notifications.length, 3),
padding: const EdgeInsets.symmetric(horizontal: 8),
itemCount: min(_pendingNotifications.length, 10),
itemBuilder: (context, idx) {
final x = _ws.notifications[idx];
final x = _pendingNotifications[idx];
return SizedBox(
width: width,
width: min(360, width),
child: Card(
child: ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 4,
),
title: Text(x.title),
title: Text(
x.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -233,15 +263,17 @@ class _DashboardScreenState extends State<DashboardScreen> {
],
),
),
).paddingSymmetric(horizontal: 8),
),
);
},
separatorBuilder: (_, __) => const Gap(4),
),
)
else
Card(
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
contentPadding:
const EdgeInsets.symmetric(horizontal: 24),
trailing: const Icon(Icons.inbox_outlined),
title: Text('notifyEmpty'.tr),
subtitle: Text('notifyEmptyCaption'.tr),
@ -250,6 +282,8 @@ class _DashboardScreenState extends State<DashboardScreen> {
],
).paddingOnly(bottom: 12),
),
/// Unread friends / followed people posts
if (_currentPosts?.isNotEmpty ?? false)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -284,14 +318,13 @@ class _DashboardScreenState extends State<DashboardScreen> {
).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,
width: min(480, width),
child: Card(
child: PostListEntryWidget(
item: item,
@ -301,8 +334,9 @@ class _DashboardScreenState extends State<DashboardScreen> {
onUpdate: (_) {
_pullPosts();
},
backgroundColor:
Theme.of(context).colorScheme.surfaceContainerLow,
backgroundColor: Theme.of(context)
.colorScheme
.surfaceContainerLow,
),
).paddingSymmetric(horizontal: 8),
);
@ -311,6 +345,8 @@ class _DashboardScreenState extends State<DashboardScreen> {
)
],
),
/// Unread messages part
if (_currentMessages?.isNotEmpty ?? false)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -344,15 +380,17 @@ class _DashboardScreenState extends State<DashboardScreen> {
],
).paddingOnly(left: 18, right: 18, bottom: 8),
SizedBox(
height: 240,
width: width,
height: 360,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: _currentMessages!.length,
itemCount: _currentGroupedMessages!.length,
itemBuilder: (context, idx) {
final item = _currentMessages![idx];
final channel =
_currentGroupedMessages!.keys.elementAt(idx);
final itemList =
_currentGroupedMessages!.values.elementAt(idx);
return SizedBox(
width: width,
width: min(520, width),
child: Card(
child: Column(
children: [
@ -366,37 +404,45 @@ class _DashboardScreenState extends State<DashboardScreen> {
topRight: Radius.circular(8),
)),
leading: CircleAvatar(
backgroundColor: item.channel!.realmId == null
backgroundColor: 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
color: channel.realmId == null
? Theme.of(context)
.colorScheme
.onPrimary
: Theme.of(context).colorScheme.primary,
size: 16,
),
),
contentPadding:
const EdgeInsets.symmetric(horizontal: 16),
title: Text(item.channel!.name),
subtitle: Text(item.channel!.description),
title: Text(channel.name),
subtitle: Text(channel.description),
onTap: () {
AppRouter.instance.pushNamed(
'channelChat',
pathParameters: {
'alias': item.channel!.alias
},
pathParameters: {'alias': channel.alias},
queryParameters: {
if (item.channel!.realmId != null)
'realm': item.channel!.realm!.alias,
if (channel.realmId != null)
'realm': channel.realm!.alias,
},
);
},
),
ChatEvent(item: item).paddingOnly(
bottom: 8, top: 16, left: 8, right: 8),
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);
},
),
),
],
),
).paddingSymmetric(horizontal: 8),
@ -415,7 +461,13 @@ class _DashboardScreenState extends State<DashboardScreen> {
),
Text(
'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,
fontSize: 12,
),
@ -423,6 +475,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
],
).paddingAll(8),
],
),
);
}
}

View File

@ -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(

View File

@ -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:

View File

@ -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: