564 lines
19 KiB
Dart
564 lines
19 KiB
Dart
import 'dart:math' as math;
|
|
|
|
import 'package:easy_localization/easy_localization.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
import 'package:island/pods/chat/chat_room.dart';
|
|
import 'package:island/pods/event_calendar.dart';
|
|
import 'package:island/pods/userinfo.dart';
|
|
import 'package:island/screens/chat/chat.dart';
|
|
import 'package:island/services/event_bus.dart';
|
|
import 'package:island/services/responsive.dart';
|
|
import 'package:island/widgets/account/fortune_graph.dart';
|
|
import 'package:island/widgets/account/friends_overview.dart';
|
|
import 'package:island/widgets/app_scaffold.dart';
|
|
import 'package:island/widgets/notification_tile.dart';
|
|
import 'package:island/widgets/post/post_featured.dart';
|
|
import 'package:island/widgets/post/post_item.dart';
|
|
import 'package:island/widgets/check_in.dart';
|
|
import 'package:island/screens/notification.dart';
|
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
|
import 'package:slide_countdown/slide_countdown.dart';
|
|
import 'package:styled_widget/styled_widget.dart';
|
|
import 'dart:async';
|
|
|
|
class DashboardScreen extends HookConsumerWidget {
|
|
const DashboardScreen({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
return AppScaffold(
|
|
isNoBackground: false,
|
|
body: Center(child: DashboardGrid()),
|
|
);
|
|
}
|
|
}
|
|
|
|
class DashboardGrid extends HookConsumerWidget {
|
|
const DashboardGrid({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final isWide = isWideScreen(context);
|
|
final devicePadding = MediaQuery.paddingOf(context);
|
|
|
|
final userInfo = ref.watch(userInfoProvider);
|
|
|
|
return Container(
|
|
constraints: BoxConstraints(
|
|
maxHeight: isWide
|
|
? math.min(640, MediaQuery.sizeOf(context).height * 0.65)
|
|
: MediaQuery.sizeOf(context).height,
|
|
),
|
|
padding: isWide
|
|
? EdgeInsets.only(top: devicePadding.top)
|
|
: EdgeInsets.only(top: 24 + devicePadding.top),
|
|
child: Column(
|
|
spacing: 16,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
// Clock card spans full width
|
|
ClockCard().padding(horizontal: isWide ? 24 : 16),
|
|
// Row with two cards side by side
|
|
Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: isWide ? 24 : 16),
|
|
child: SearchBar(
|
|
hintText: 'Search Anything...',
|
|
constraints: const BoxConstraints(minHeight: 56),
|
|
leading: const Icon(Symbols.search).padding(horizontal: 24),
|
|
readOnly: true,
|
|
onTap: () {
|
|
eventBus.fire(CommandPaletteTriggerEvent());
|
|
},
|
|
),
|
|
),
|
|
if (userInfo.value != null)
|
|
Expanded(
|
|
child:
|
|
SingleChildScrollView(
|
|
padding: isWide
|
|
? const EdgeInsets.symmetric(horizontal: 24)
|
|
: const EdgeInsets.only(bottom: 64),
|
|
scrollDirection: isWide
|
|
? Axis.horizontal
|
|
: Axis.vertical,
|
|
child: isWide
|
|
? _DashboardGridWide()
|
|
: _DashboardGridNarrow(),
|
|
)
|
|
.clipRRect(
|
|
topLeft: isWide ? 0 : 12,
|
|
topRight: isWide ? 0 : 12,
|
|
)
|
|
.padding(horizontal: isWide ? 0 : 16),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _DashboardGridWide extends HookConsumerWidget {
|
|
const _DashboardGridWide();
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
return Row(
|
|
spacing: 16,
|
|
children: [
|
|
SizedBox(
|
|
width: 400,
|
|
child: Column(
|
|
spacing: 16,
|
|
children: [
|
|
CheckInWidget(margin: EdgeInsets.zero, checkInOnly: true),
|
|
Card(
|
|
margin: EdgeInsets.zero,
|
|
child: FortuneGraphWidget(
|
|
events: ref.watch(
|
|
eventCalendarProvider(
|
|
EventCalendarQuery(
|
|
uname: 'me',
|
|
year: DateTime.now().year,
|
|
month: DateTime.now().month,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Expanded(child: FortuneCard()),
|
|
],
|
|
),
|
|
),
|
|
SizedBox(width: 400, child: FeaturedPostCard()),
|
|
SizedBox(
|
|
width: 400,
|
|
child: Column(
|
|
spacing: 16,
|
|
children: [
|
|
FriendsOverviewWidget(),
|
|
Expanded(child: NotificationsCard()),
|
|
],
|
|
),
|
|
),
|
|
SizedBox(
|
|
width: 400,
|
|
child: Column(
|
|
spacing: 16,
|
|
children: [Expanded(child: ChatListCard())],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _DashboardGridNarrow extends HookConsumerWidget {
|
|
const _DashboardGridNarrow();
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
return Column(
|
|
spacing: 16,
|
|
children: [
|
|
CheckInWidget(margin: EdgeInsets.zero, checkInOnly: true),
|
|
FortuneCard(),
|
|
SizedBox(height: 400, child: FeaturedPostCard()),
|
|
FriendsOverviewWidget(),
|
|
NotificationsCard(),
|
|
ChatListCard(),
|
|
Card(
|
|
margin: EdgeInsets.zero,
|
|
child: FortuneGraphWidget(
|
|
events: ref.watch(
|
|
eventCalendarProvider(
|
|
EventCalendarQuery(
|
|
uname: 'me',
|
|
year: DateTime.now().year,
|
|
month: DateTime.now().month,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class ClockCard extends HookConsumerWidget {
|
|
const ClockCard({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final time = useState(DateTime.now());
|
|
final timer = useRef<Timer?>(null);
|
|
final nextNotableDay = ref.watch(nextNotableDayProvider);
|
|
|
|
useEffect(() {
|
|
timer.value = Timer.periodic(const Duration(seconds: 1), (_) {
|
|
time.value = DateTime.now();
|
|
});
|
|
return () => timer.value?.cancel();
|
|
}, []);
|
|
|
|
return Card(
|
|
elevation: 0,
|
|
margin: EdgeInsets.zero,
|
|
color: Colors.transparent,
|
|
shape: const RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Symbols.schedule,
|
|
size: 32,
|
|
color: Theme.of(context).colorScheme.primary,
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Row(
|
|
spacing: 8,
|
|
crossAxisAlignment: CrossAxisAlignment.baseline,
|
|
textBaseline: TextBaseline.ideographic,
|
|
children: [
|
|
Flexible(
|
|
child: Text(
|
|
'${time.value.hour.toString().padLeft(2, '0')}:${time.value.minute.toString().padLeft(2, '0')}:${time.value.second.toString().padLeft(2, '0')}',
|
|
style: GoogleFonts.robotoMono(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
Flexible(
|
|
child: Text(
|
|
'${time.value.month.toString().padLeft(2, '0')}/${time.value.day.toString().padLeft(2, '0')}',
|
|
style: Theme.of(context).textTheme.bodyMedium
|
|
?.copyWith(
|
|
color: Theme.of(
|
|
context,
|
|
).colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
Row(
|
|
spacing: 5,
|
|
children: [
|
|
Text('notableDayNext')
|
|
.tr(
|
|
args: [
|
|
nextNotableDay.value?.localName ?? 'idk',
|
|
],
|
|
)
|
|
.fontSize(12),
|
|
if (nextNotableDay.value != null)
|
|
SlideCountdown(
|
|
decoration: const BoxDecoration(),
|
|
style: const TextStyle(fontSize: 12),
|
|
separatorStyle: const TextStyle(fontSize: 12),
|
|
padding: EdgeInsets.zero,
|
|
duration: nextNotableDay.value?.date.difference(
|
|
DateTime.now(),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class FeaturedPostCard extends HookConsumerWidget {
|
|
const FeaturedPostCard({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final featuredPostsAsync = ref.watch(featuredPostsProvider);
|
|
|
|
return ClipRRect(
|
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
|
child: Card(
|
|
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
|
margin: EdgeInsets.zero,
|
|
child: Column(
|
|
children: [
|
|
SizedBox(
|
|
height: 48,
|
|
child: Row(
|
|
spacing: 8,
|
|
children: [
|
|
const Icon(Symbols.highlight),
|
|
Text('highlightPost').tr(),
|
|
],
|
|
).padding(horizontal: 16, vertical: 8),
|
|
),
|
|
Expanded(
|
|
child: featuredPostsAsync.when(
|
|
loading: () => const Center(child: CircularProgressIndicator()),
|
|
error: (error, stack) => Center(child: Text('Error: $error')),
|
|
data: (posts) {
|
|
if (posts.isEmpty) {
|
|
return const Padding(
|
|
padding: EdgeInsets.all(16),
|
|
child: Center(child: Text('No featured posts available')),
|
|
);
|
|
}
|
|
return PageView.builder(
|
|
scrollDirection: Axis.horizontal,
|
|
itemCount: posts.length,
|
|
itemBuilder: (context, index) {
|
|
return SingleChildScrollView(
|
|
child: PostActionableItem(
|
|
item: posts[index],
|
|
borderRadius: 8,
|
|
),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class NotificationsCard extends HookConsumerWidget {
|
|
const NotificationsCard({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final notifications = ref.watch(notificationListProvider);
|
|
|
|
return Card(
|
|
margin: EdgeInsets.zero,
|
|
shape: const RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
|
),
|
|
child: InkWell(
|
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
|
onTap: () {
|
|
// Show notification sheet similar to explore.dart
|
|
showModalBottomSheet(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
useRootNavigator: true,
|
|
builder: (context) => const NotificationSheet(),
|
|
);
|
|
},
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Symbols.notifications,
|
|
size: 20,
|
|
color: Theme.of(context).colorScheme.primary,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
'Notifications',
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
).padding(horizontal: 16, vertical: 12),
|
|
notifications.when(
|
|
loading: () => const SkeletonNotificationTile(),
|
|
error: (error, stack) => Center(child: Text('Error: $error')),
|
|
data: (notificationList) {
|
|
if (notificationList.isEmpty) {
|
|
return const Center(child: Text('No notifications yet'));
|
|
}
|
|
// Get the most recent notification (first in the list)
|
|
final recentNotification = notificationList.first;
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Most Recent',
|
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
),
|
|
).padding(horizontal: 16),
|
|
const SizedBox(height: 8),
|
|
NotificationTile(
|
|
notification: recentNotification,
|
|
compact: true,
|
|
contentPadding: EdgeInsets.symmetric(horizontal: 16),
|
|
avatarRadius: 16.0,
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
Text(
|
|
'Tap to view all notifications',
|
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
),
|
|
).padding(horizontal: 16, vertical: 8),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class ChatListCard extends HookConsumerWidget {
|
|
const ChatListCard({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final chatRooms = ref.watch(chatRoomJoinedProvider);
|
|
|
|
return Card(
|
|
margin: EdgeInsets.zero,
|
|
shape: const RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Symbols.chat,
|
|
size: 20,
|
|
color: Theme.of(context).colorScheme.primary,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
'Recent Chats',
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
).padding(horizontal: 16, vertical: 16),
|
|
chatRooms.when(
|
|
loading: () => const Center(child: CircularProgressIndicator()),
|
|
error: (error, stack) => Center(child: Text('Error: $error')),
|
|
data: (rooms) {
|
|
if (rooms.isEmpty) {
|
|
return const Center(child: Text('No chat rooms available'));
|
|
}
|
|
// Take only the first 5 rooms
|
|
final recentRooms = rooms.take(5).toList();
|
|
return Column(
|
|
children: recentRooms.map((room) {
|
|
return ChatRoomListTile(
|
|
room: room,
|
|
isDirect: room.type == 1,
|
|
onTap: () {
|
|
context.pushNamed(
|
|
'chatRoom',
|
|
pathParameters: {'id': room.id},
|
|
);
|
|
},
|
|
);
|
|
}).toList(),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class FortuneCard extends HookWidget {
|
|
const FortuneCard({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final fortune = useMemoized(() {
|
|
const fortunes = [
|
|
{'text': '有的人活着,但他已经死了。', 'author': '—— 鲁迅'},
|
|
{'text': '天行健,君子以自强不息。', 'author': '—— 《周易》'},
|
|
{'text': '路漫漫其修远兮,吾将上下而求索。', 'author': '—— 屈原'},
|
|
{'text': '学海无涯苦作舟。', 'author': '—— 韩愈'},
|
|
{'text': '天道酬勤。', 'author': '—— 古语'},
|
|
{'text': '书山有路勤为径,学海无涯苦作舟。', 'author': '—— 韩愈'},
|
|
{'text': '莫等闲,白了少年头,空悲切。', 'author': '—— 岳飞'},
|
|
{
|
|
'text': 'The best way to predict the future is to create it.',
|
|
'author': '— Peter Drucker',
|
|
},
|
|
{'text': 'Fortune favors the bold.', 'author': '— Virgil'},
|
|
{
|
|
'text': 'A journey of a thousand miles begins with a single step.',
|
|
'author': '— Lao Tzu',
|
|
},
|
|
{
|
|
'text': 'The only way to do great work is to love what you do.',
|
|
'author': '— Steve Jobs',
|
|
},
|
|
{
|
|
'text': 'Believe you can and you\'re halfway there.',
|
|
'author': '— Theodore Roosevelt',
|
|
},
|
|
{
|
|
'text':
|
|
'The future belongs to those who believe in the beauty of their dreams.',
|
|
'author': '— Eleanor Roosevelt',
|
|
},
|
|
];
|
|
return fortunes[math.Random().nextInt(fortunes.length)];
|
|
});
|
|
|
|
return Card(
|
|
margin: EdgeInsets.zero,
|
|
shape: const RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
|
),
|
|
child: Row(
|
|
spacing: 8,
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
fortune['text']!,
|
|
maxLines: 2,
|
|
overflow: TextOverflow.fade,
|
|
),
|
|
),
|
|
Text(fortune['author']!).bold(),
|
|
],
|
|
).padding(horizontal: 16),
|
|
).height(48);
|
|
}
|
|
}
|