✨ 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,
|
'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 {
|
||||||
|
@ -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() {
|
||||||
|
@ -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),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(
|
||||||
|
@ -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:
|
||||||
|
@ -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:
|
||||||
|
Loading…
Reference in New Issue
Block a user