✨ Better dashboard design for large screen (and mobile device)
This commit is contained in:
parent
a75f42e440
commit
1a0721ba3a
@ -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,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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user