✨ Dashboard basis
This commit is contained in:
@@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart' show kIsWeb;
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/screens/about.dart';
|
import 'package:island/screens/about.dart';
|
||||||
|
import 'package:island/screens/dashboard/dash.dart';
|
||||||
import 'package:island/screens/developers/app_detail.dart';
|
import 'package:island/screens/developers/app_detail.dart';
|
||||||
import 'package:island/screens/developers/bot_detail.dart';
|
import 'package:island/screens/developers/bot_detail.dart';
|
||||||
import 'package:island/screens/developers/hub.dart';
|
import 'package:island/screens/developers/hub.dart';
|
||||||
@@ -185,10 +186,20 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
return TabsScreen(child: child);
|
return TabsScreen(child: child);
|
||||||
},
|
},
|
||||||
routes: [
|
routes: [
|
||||||
|
// Dashboard tab
|
||||||
|
GoRoute(
|
||||||
|
name: 'dashboard',
|
||||||
|
path: '/',
|
||||||
|
pageBuilder: (context, state) => CustomTransitionPage(
|
||||||
|
key: const ValueKey('dashboard'),
|
||||||
|
child: const DashboardScreen(),
|
||||||
|
transitionsBuilder: _tabPagesTransitionBuilder,
|
||||||
|
),
|
||||||
|
),
|
||||||
// Explore tab
|
// Explore tab
|
||||||
GoRoute(
|
GoRoute(
|
||||||
name: 'explore',
|
name: 'explore',
|
||||||
path: '/',
|
path: '/explore',
|
||||||
pageBuilder: (context, state) => CustomTransitionPage(
|
pageBuilder: (context, state) => CustomTransitionPage(
|
||||||
key: const ValueKey('explore'),
|
key: const ValueKey('explore'),
|
||||||
child: const ExploreScreen(),
|
child: const ExploreScreen(),
|
||||||
|
|||||||
519
lib/screens/dashboard/dash.dart
Normal file
519
lib/screens/dashboard/dash.dart
Normal file
@@ -0,0 +1,519 @@
|
|||||||
|
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/screens/chat/chat.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(body: Center(child: DashboardGrid()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DashboardGrid extends HookConsumerWidget {
|
||||||
|
const DashboardGrid({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return Container(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxHeight: math.min(640, MediaQuery.sizeOf(context).height * 0.65),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
spacing: 16,
|
||||||
|
children: [
|
||||||
|
// Clock card spans full width
|
||||||
|
ClockCard().padding(horizontal: 24),
|
||||||
|
// Row with two cards side by side
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
child: SearchBar(
|
||||||
|
hintText: 'Search Anything...',
|
||||||
|
constraints: const BoxConstraints(minHeight: 56),
|
||||||
|
leading: const Icon(Symbols.search).padding(horizontal: 24),
|
||||||
|
readOnly: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: 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 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(),
|
||||||
|
const Spacer(),
|
||||||
|
IconButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
onPressed: () {
|
||||||
|
// Navigation to previous post
|
||||||
|
},
|
||||||
|
icon: const Icon(Symbols.arrow_left),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
onPressed: () {
|
||||||
|
// Navigation to next post
|
||||||
|
},
|
||||||
|
icon: const Icon(Symbols.arrow_right),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).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(
|
||||||
|
elevation: 4,
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
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),
|
||||||
|
Expanded(
|
||||||
|
child: 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(
|
||||||
|
elevation: 4,
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
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),
|
||||||
|
Expanded(
|
||||||
|
child: 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 ListView.builder(
|
||||||
|
itemCount: recentRooms.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final room = recentRooms[index];
|
||||||
|
return ChatRoomListTile(
|
||||||
|
room: room,
|
||||||
|
isDirect: room.type == 1,
|
||||||
|
onTap: () {
|
||||||
|
context.pushNamed(
|
||||||
|
'chatRoom',
|
||||||
|
pathParameters: {'id': room.id},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
elevation: 4,
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
spacing: 8,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(child: Text(fortune['text']!)),
|
||||||
|
Text(fortune['author']!).bold(),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 24),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,23 +4,20 @@ import 'dart:math' as math;
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/account.dart';
|
import 'package:island/models/account.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/pods/paging.dart';
|
import 'package:island/pods/paging.dart';
|
||||||
import 'package:island/pods/websocket.dart';
|
import 'package:island/pods/websocket.dart';
|
||||||
import 'package:island/route.dart';
|
|
||||||
import 'package:island/widgets/content/cloud_files.dart';
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
import 'package:island/widgets/content/markdown.dart';
|
import 'package:island/widgets/content/markdown.dart';
|
||||||
import 'package:island/widgets/content/sheet.dart';
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
|
import 'package:island/widgets/notification_tile.dart';
|
||||||
import 'package:island/widgets/paging/pagination_list.dart';
|
import 'package:island/widgets/paging/pagination_list.dart';
|
||||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||||
import 'package:relative_time/relative_time.dart';
|
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
|
||||||
|
|
||||||
part 'notification.g.dart';
|
part 'notification.g.dart';
|
||||||
|
|
||||||
@@ -197,31 +194,6 @@ class NotificationListNotifier extends AsyncNotifier<List<SnNotification>>
|
|||||||
class NotificationSheet extends HookConsumerWidget {
|
class NotificationSheet extends HookConsumerWidget {
|
||||||
const NotificationSheet({super.key});
|
const NotificationSheet({super.key});
|
||||||
|
|
||||||
IconData _getNotificationIcon(String topic) {
|
|
||||||
switch (topic) {
|
|
||||||
case 'post.replies':
|
|
||||||
return Symbols.reply;
|
|
||||||
case 'wallet.transactions':
|
|
||||||
return Symbols.account_balance_wallet;
|
|
||||||
case 'relationships.friends.request':
|
|
||||||
return Symbols.person_add;
|
|
||||||
case 'invites.chat':
|
|
||||||
return Symbols.chat;
|
|
||||||
case 'invites.realm':
|
|
||||||
return Symbols.domain;
|
|
||||||
case 'auth.login':
|
|
||||||
return Symbols.login;
|
|
||||||
case 'posts.new':
|
|
||||||
return Symbols.post_add;
|
|
||||||
case 'wallet.orders.paid':
|
|
||||||
return Symbols.shopping_bag;
|
|
||||||
case 'posts.reactions.new':
|
|
||||||
return Symbols.add_reaction;
|
|
||||||
default:
|
|
||||||
return Symbols.notifications;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
// Refresh unread count when sheet opens to sync across devices
|
// Refresh unread count when sheet opens to sync across devices
|
||||||
@@ -265,109 +237,7 @@ class NotificationSheet extends HookConsumerWidget {
|
|||||||
notifier: notificationListProvider.notifier,
|
notifier: notificationListProvider.notifier,
|
||||||
footerSkeletonChild: const SkeletonNotificationTile(),
|
footerSkeletonChild: const SkeletonNotificationTile(),
|
||||||
itemBuilder: (context, index, notification) {
|
itemBuilder: (context, index, notification) {
|
||||||
final pfp = notification.meta['pfp'] as String?;
|
return NotificationTile(notification: notification);
|
||||||
final images = notification.meta['images'] as List?;
|
|
||||||
final imageIds = images?.cast<String>() ?? [];
|
|
||||||
|
|
||||||
return ListTile(
|
|
||||||
isThreeLine: true,
|
|
||||||
contentPadding: EdgeInsets.symmetric(
|
|
||||||
horizontal: 16,
|
|
||||||
vertical: 8,
|
|
||||||
),
|
|
||||||
leading: pfp != null
|
|
||||||
? ProfilePictureWidget(fileId: pfp, radius: 20)
|
|
||||||
: CircleAvatar(
|
|
||||||
backgroundColor: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.primaryContainer,
|
|
||||||
child: Icon(
|
|
||||||
_getNotificationIcon(notification.topic),
|
|
||||||
color: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.onPrimaryContainer,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
title: Text(notification.title),
|
|
||||||
subtitle: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
if (notification.subtitle.isNotEmpty)
|
|
||||||
Text(notification.subtitle).bold(),
|
|
||||||
Row(
|
|
||||||
spacing: 6,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
DateFormat().format(
|
|
||||||
notification.createdAt.toLocal(),
|
|
||||||
),
|
|
||||||
).fontSize(11),
|
|
||||||
Text('·').fontSize(11).bold(),
|
|
||||||
Text(
|
|
||||||
RelativeTime(
|
|
||||||
context,
|
|
||||||
).format(notification.createdAt.toLocal()),
|
|
||||||
).fontSize(11),
|
|
||||||
],
|
|
||||||
).opacity(0.75).padding(bottom: 4),
|
|
||||||
MarkdownTextContent(
|
|
||||||
content: notification.content,
|
|
||||||
textStyle: Theme.of(context).textTheme.bodyMedium
|
|
||||||
?.copyWith(
|
|
||||||
color: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.onSurface.withOpacity(0.8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (imageIds.isNotEmpty)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 8),
|
|
||||||
child: Wrap(
|
|
||||||
spacing: 8,
|
|
||||||
runSpacing: 8,
|
|
||||||
children: imageIds.map((imageId) {
|
|
||||||
return SizedBox(
|
|
||||||
width: 80,
|
|
||||||
height: 80,
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
child: CloudImageWidget(
|
|
||||||
fileId: imageId,
|
|
||||||
aspectRatio: 1,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
trailing: notification.viewedAt != null
|
|
||||||
? null
|
|
||||||
: Container(
|
|
||||||
width: 12,
|
|
||||||
height: 12,
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: Colors.blue,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
if (notification.meta['action_uri'] != null) {
|
|
||||||
var uri = notification.meta['action_uri'] as String;
|
|
||||||
if (uri.startsWith('/')) {
|
|
||||||
// In-app routes
|
|
||||||
rootNavigatorKey.currentContext?.push(
|
|
||||||
notification.meta['action_uri'],
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// External URLs
|
|
||||||
launchUrlString(uri);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -34,9 +34,10 @@ class CurrentRouteNotifier extends Notifier<String?> {
|
|||||||
const kWideScreenRouteStart = 4;
|
const kWideScreenRouteStart = 4;
|
||||||
const kTabRoutes = [
|
const kTabRoutes = [
|
||||||
'/',
|
'/',
|
||||||
|
'/explore',
|
||||||
'/chat',
|
'/chat',
|
||||||
'/realms',
|
|
||||||
'/account',
|
'/account',
|
||||||
|
'/realms',
|
||||||
'/files',
|
'/files',
|
||||||
'/thought',
|
'/thought',
|
||||||
'/creators',
|
'/creators',
|
||||||
@@ -67,6 +68,10 @@ class TabsScreen extends HookConsumerWidget {
|
|||||||
final wideScreen = isWideScreen(context);
|
final wideScreen = isWideScreen(context);
|
||||||
|
|
||||||
final destinations = [
|
final destinations = [
|
||||||
|
NavigationDestination(
|
||||||
|
label: 'dashboard'.tr(),
|
||||||
|
icon: const Icon(Symbols.dashboard_rounded),
|
||||||
|
),
|
||||||
NavigationDestination(
|
NavigationDestination(
|
||||||
label: 'explore'.tr(),
|
label: 'explore'.tr(),
|
||||||
icon: const Icon(Symbols.explore_rounded),
|
icon: const Icon(Symbols.explore_rounded),
|
||||||
@@ -79,10 +84,7 @@ class TabsScreen extends HookConsumerWidget {
|
|||||||
child: const Icon(Symbols.forum_rounded),
|
child: const Icon(Symbols.forum_rounded),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
NavigationDestination(
|
|
||||||
label: 'realms'.tr(),
|
|
||||||
icon: const Icon(Symbols.group_rounded),
|
|
||||||
),
|
|
||||||
NavigationDestination(
|
NavigationDestination(
|
||||||
label: 'account'.tr(),
|
label: 'account'.tr(),
|
||||||
icon: Badge.count(
|
icon: Badge.count(
|
||||||
@@ -105,6 +107,10 @@ class TabsScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
if (wideScreen)
|
if (wideScreen)
|
||||||
...([
|
...([
|
||||||
|
NavigationDestination(
|
||||||
|
label: 'realms'.tr(),
|
||||||
|
icon: const Icon(Symbols.group_rounded),
|
||||||
|
),
|
||||||
NavigationDestination(
|
NavigationDestination(
|
||||||
label: 'files'.tr(),
|
label: 'files'.tr(),
|
||||||
icon: const Icon(Symbols.folder_rounded),
|
icon: const Icon(Symbols.folder_rounded),
|
||||||
@@ -154,15 +160,14 @@ class TabsScreen extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
NavigationRail(
|
NavigationRail(
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
destinations:
|
destinations: destinations
|
||||||
destinations
|
.map(
|
||||||
.map(
|
(e) => NavigationRailDestination(
|
||||||
(e) => NavigationRailDestination(
|
icon: e.icon,
|
||||||
icon: e.icon,
|
label: Text(e.label),
|
||||||
label: Text(e.label),
|
),
|
||||||
),
|
)
|
||||||
)
|
.toList(),
|
||||||
.toList(),
|
|
||||||
selectedIndex: currentIndex,
|
selectedIndex: currentIndex,
|
||||||
onDestinationSelected: onDestinationSelected,
|
onDestinationSelected: onDestinationSelected,
|
||||||
trailingAtBottom: true,
|
trailingAtBottom: true,
|
||||||
@@ -195,10 +200,9 @@ class TabsScreen extends HookConsumerWidget {
|
|||||||
child: child ?? const SizedBox.shrink(),
|
child: child ?? const SizedBox.shrink(),
|
||||||
),
|
),
|
||||||
floatingActionButton: shouldShowFab ? const FabMenu() : null,
|
floatingActionButton: shouldShowFab ? const FabMenu() : null,
|
||||||
floatingActionButtonLocation:
|
floatingActionButtonLocation: shouldShowFab
|
||||||
shouldShowFab
|
? _DockedFabLocation(context, settings.fabPosition)
|
||||||
? _DockedFabLocation(context, settings.fabPosition)
|
: null,
|
||||||
: null,
|
|
||||||
bottomNavigationBar: ConditionalBottomNav(
|
bottomNavigationBar: ConditionalBottomNav(
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.only(
|
borderRadius: BorderRadius.only(
|
||||||
@@ -223,19 +227,19 @@ class TabsScreen extends HookConsumerWidget {
|
|||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: () {
|
children: () {
|
||||||
final navItems =
|
final navItems = destinations.asMap().entries.map<Widget>((
|
||||||
destinations.asMap().entries.map<Widget>((entry) {
|
entry,
|
||||||
int index = entry.key;
|
) {
|
||||||
NavigationDestination dest = entry.value;
|
int index = entry.key;
|
||||||
return IconButton(
|
NavigationDestination dest = entry.value;
|
||||||
icon: dest.icon,
|
return IconButton(
|
||||||
onPressed: () => onDestinationSelected(index),
|
icon: dest.icon,
|
||||||
color:
|
onPressed: () => onDestinationSelected(index),
|
||||||
index == currentIndex
|
color: index == currentIndex
|
||||||
? Theme.of(context).colorScheme.primary
|
? Theme.of(context).colorScheme.primary
|
||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
// Add mock item to leave space for FAB based on position
|
// Add mock item to leave space for FAB based on position
|
||||||
final gapIndex = switch (settings.fabPosition) {
|
final gapIndex = switch (settings.fabPosition) {
|
||||||
'left' => 0,
|
'left' => 0,
|
||||||
|
|||||||
@@ -52,7 +52,13 @@ Future<SnNotableDay?> nextNotableDay(Ref ref) async {
|
|||||||
class CheckInWidget extends HookConsumerWidget {
|
class CheckInWidget extends HookConsumerWidget {
|
||||||
final EdgeInsets? margin;
|
final EdgeInsets? margin;
|
||||||
final VoidCallback? onChecked;
|
final VoidCallback? onChecked;
|
||||||
const CheckInWidget({super.key, this.margin, this.onChecked});
|
final bool checkInOnly;
|
||||||
|
const CheckInWidget({
|
||||||
|
super.key,
|
||||||
|
this.margin,
|
||||||
|
this.onChecked,
|
||||||
|
this.checkInOnly = false,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -122,57 +128,77 @@ class CheckInWidget extends HookConsumerWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
if (checkInOnly)
|
||||||
spacing: 6,
|
AnimatedSwitcher(
|
||||||
mainAxisSize: MainAxisSize.min,
|
duration: const Duration(milliseconds: 300),
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
child: todayResult.when(
|
||||||
children: [
|
data: (result) {
|
||||||
Icon(
|
return Text(
|
||||||
switch (DateTime.now().weekday) {
|
result == null
|
||||||
6 || 7 => Symbols.weekend,
|
? 'checkInNone'
|
||||||
_ => isAdult ? Symbols.work : Symbols.school,
|
: 'checkInResultLevel${result.level}',
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
).tr().fontSize(15).bold();
|
||||||
},
|
},
|
||||||
fill: 1,
|
loading: () =>
|
||||||
size: 16,
|
Text('checkInNone').tr().fontSize(15).bold(),
|
||||||
).padding(right: 2),
|
error: (err, stack) =>
|
||||||
Text(
|
Text('error').tr().fontSize(15).bold(),
|
||||||
DateFormat('EEE').format(DateTime.now()),
|
|
||||||
).fontSize(16).bold(),
|
|
||||||
Text(
|
|
||||||
DateFormat('MM/dd').format(DateTime.now()),
|
|
||||||
).fontSize(16),
|
|
||||||
Tooltip(
|
|
||||||
message: timeLeftFormatted,
|
|
||||||
child: SizedBox(
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
trackGap: 0,
|
|
||||||
value: progress,
|
|
||||||
strokeWidth: 2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
).padding(right: 4),
|
||||||
),
|
if (!checkInOnly)
|
||||||
Row(
|
Row(
|
||||||
spacing: 5,
|
spacing: 6,
|
||||||
children: [
|
mainAxisSize: MainAxisSize.min,
|
||||||
Text('notableDayNext')
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
.tr(args: [nextNotableDay.value?.localName ?? 'idk'])
|
children: [
|
||||||
.fontSize(12),
|
Icon(
|
||||||
if (nextNotableDay.value != null)
|
switch (DateTime.now().weekday) {
|
||||||
SlideCountdown(
|
6 || 7 => Symbols.weekend,
|
||||||
decoration: const BoxDecoration(),
|
_ => isAdult ? Symbols.work : Symbols.school,
|
||||||
style: const TextStyle(fontSize: 12),
|
},
|
||||||
separatorStyle: const TextStyle(fontSize: 12),
|
fill: 1,
|
||||||
padding: EdgeInsets.zero,
|
size: 16,
|
||||||
duration: nextNotableDay.value?.date.difference(
|
).padding(right: 2),
|
||||||
DateTime.now(),
|
Text(
|
||||||
|
DateFormat('EEE').format(DateTime.now()),
|
||||||
|
).fontSize(16).bold(),
|
||||||
|
Text(
|
||||||
|
DateFormat('MM/dd').format(DateTime.now()),
|
||||||
|
).fontSize(16),
|
||||||
|
Tooltip(
|
||||||
|
message: timeLeftFormatted,
|
||||||
|
child: SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
trackGap: 0,
|
||||||
|
value: progress,
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
if (!checkInOnly)
|
||||||
|
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(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
const Gap(2),
|
const Gap(2),
|
||||||
AnimatedSwitcher(
|
AnimatedSwitcher(
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
@@ -213,14 +239,13 @@ class CheckInWidget extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
loading: () => Text('checkInNoneHint').tr().fontSize(11),
|
loading: () => Text('checkInNoneHint').tr().fontSize(11),
|
||||||
error:
|
error: (err, stack) => Column(
|
||||||
(err, stack) => Column(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
children: [
|
||||||
children: [
|
Text('error').tr().fontSize(15).bold(),
|
||||||
Text('error').tr().fontSize(15).bold(),
|
Text(err.toString()).fontSize(11),
|
||||||
Text(err.toString()).fontSize(11),
|
],
|
||||||
],
|
),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
).alignment(Alignment.centerLeft),
|
).alignment(Alignment.centerLeft),
|
||||||
],
|
],
|
||||||
@@ -231,21 +256,23 @@ class CheckInWidget extends HookConsumerWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
spacing: 4,
|
spacing: 4,
|
||||||
children: [
|
children: [
|
||||||
AnimatedSwitcher(
|
if (!checkInOnly)
|
||||||
duration: const Duration(milliseconds: 300),
|
AnimatedSwitcher(
|
||||||
child: todayResult.when(
|
duration: const Duration(milliseconds: 300),
|
||||||
data: (result) {
|
child: todayResult.when(
|
||||||
return Text(
|
data: (result) {
|
||||||
result == null
|
return Text(
|
||||||
? 'checkInNone'
|
result == null
|
||||||
: 'checkInResultLevel${result.level}',
|
? 'checkInNone'
|
||||||
textAlign: TextAlign.start,
|
: 'checkInResultLevel${result.level}',
|
||||||
).tr().fontSize(15).bold();
|
textAlign: TextAlign.start,
|
||||||
},
|
).tr().fontSize(15).bold();
|
||||||
loading: () => Text('checkInNone').tr().fontSize(15).bold(),
|
},
|
||||||
error: (err, stack) => Text('error').tr().fontSize(15).bold(),
|
loading: () => Text('checkInNone').tr().fontSize(15).bold(),
|
||||||
),
|
error: (err, stack) =>
|
||||||
).padding(right: 4),
|
Text('error').tr().fontSize(15).bold(),
|
||||||
|
),
|
||||||
|
).padding(right: 4),
|
||||||
IconButton.outlined(
|
IconButton.outlined(
|
||||||
iconSize: 16,
|
iconSize: 16,
|
||||||
visualDensity: const VisualDensity(
|
visualDensity: const VisualDensity(
|
||||||
@@ -259,27 +286,22 @@ class CheckInWidget extends HookConsumerWidget {
|
|||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
builder:
|
builder: (context) => SheetScaffold(
|
||||||
(context) => SheetScaffold(
|
titleText: 'eventCalendar'.tr(),
|
||||||
titleText: 'eventCalendar'.tr(),
|
child: EventCalendarContent(name: 'me', isSheet: true),
|
||||||
child: EventCalendarContent(
|
),
|
||||||
name: 'me',
|
|
||||||
isSheet: true,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
icon: AnimatedSwitcher(
|
icon: AnimatedSwitcher(
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
child: todayResult.when(
|
child: todayResult.when(
|
||||||
data:
|
data: (result) => Icon(
|
||||||
(result) => Icon(
|
result == null
|
||||||
result == null
|
? Symbols.local_fire_department
|
||||||
? Symbols.local_fire_department
|
: Symbols.event,
|
||||||
: Symbols.event,
|
key: ValueKey(result != null),
|
||||||
key: ValueKey(result != null),
|
),
|
||||||
),
|
|
||||||
loading: () => const Icon(Symbols.refresh),
|
loading: () => const Icon(Symbols.refresh),
|
||||||
error: (_, _) => const Icon(Symbols.error),
|
error: (_, _) => const Icon(Symbols.error),
|
||||||
),
|
),
|
||||||
|
|||||||
179
lib/widgets/notification_tile.dart
Normal file
179
lib/widgets/notification_tile.dart
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:island/models/account.dart';
|
||||||
|
import 'package:island/route.dart';
|
||||||
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
|
import 'package:island/widgets/content/markdown.dart';
|
||||||
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||||
|
import 'package:relative_time/relative_time.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
|
class NotificationTile extends StatelessWidget {
|
||||||
|
final SnNotification notification;
|
||||||
|
final double? avatarRadius;
|
||||||
|
final EdgeInsets? contentPadding;
|
||||||
|
final bool showImages;
|
||||||
|
final bool compact;
|
||||||
|
|
||||||
|
const NotificationTile({
|
||||||
|
super.key,
|
||||||
|
required this.notification,
|
||||||
|
this.avatarRadius,
|
||||||
|
this.contentPadding,
|
||||||
|
this.showImages = true,
|
||||||
|
this.compact = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
IconData _getNotificationIcon(String topic) {
|
||||||
|
switch (topic) {
|
||||||
|
case 'post.replies':
|
||||||
|
return Symbols.reply;
|
||||||
|
case 'wallet.transactions':
|
||||||
|
return Symbols.account_balance_wallet;
|
||||||
|
case 'relationships.friends.request':
|
||||||
|
return Symbols.person_add;
|
||||||
|
case 'invites.chat':
|
||||||
|
return Symbols.chat;
|
||||||
|
case 'invites.realm':
|
||||||
|
return Symbols.domain;
|
||||||
|
case 'auth.login':
|
||||||
|
return Symbols.login;
|
||||||
|
case 'posts.new':
|
||||||
|
return Symbols.post_add;
|
||||||
|
case 'wallet.orders.paid':
|
||||||
|
return Symbols.shopping_bag;
|
||||||
|
case 'posts.reactions.new':
|
||||||
|
return Symbols.add_reaction;
|
||||||
|
default:
|
||||||
|
return Symbols.notifications;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final pfp = notification.meta['pfp'] as String?;
|
||||||
|
final images = notification.meta['images'] as List?;
|
||||||
|
final imageIds = images?.cast<String>() ?? [];
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
isThreeLine: true,
|
||||||
|
contentPadding:
|
||||||
|
contentPadding ??
|
||||||
|
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
leading: pfp != null
|
||||||
|
? ProfilePictureWidget(
|
||||||
|
fileId: pfp,
|
||||||
|
radius: avatarRadius ?? (compact ? 16 : 20),
|
||||||
|
)
|
||||||
|
: CircleAvatar(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
|
||||||
|
radius: avatarRadius ?? (compact ? 16 : 20),
|
||||||
|
child: Icon(
|
||||||
|
_getNotificationIcon(notification.topic),
|
||||||
|
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
|
size: compact ? 16 : 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
notification.title,
|
||||||
|
style: compact
|
||||||
|
? Theme.of(context).textTheme.bodySmall
|
||||||
|
: Theme.of(context).textTheme.titleMedium,
|
||||||
|
maxLines: compact ? 2 : null,
|
||||||
|
overflow: compact ? TextOverflow.ellipsis : null,
|
||||||
|
),
|
||||||
|
subtitle: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
if (notification.subtitle.isNotEmpty && !compact)
|
||||||
|
Text(notification.subtitle).bold(),
|
||||||
|
Row(
|
||||||
|
spacing: 6,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
DateFormat().format(notification.createdAt.toLocal()),
|
||||||
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.bodySmall?.copyWith(fontSize: compact ? 10 : 11),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'·',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
fontSize: compact ? 10 : 11,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
RelativeTime(context).format(notification.createdAt.toLocal()),
|
||||||
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.bodySmall?.copyWith(fontSize: compact ? 10 : 11),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).opacity(0.75).padding(bottom: compact ? 2 : 4),
|
||||||
|
MarkdownTextContent(
|
||||||
|
content: notification.content,
|
||||||
|
textStyle:
|
||||||
|
(compact
|
||||||
|
? Theme.of(context).textTheme.bodySmall
|
||||||
|
: Theme.of(context).textTheme.bodyMedium)
|
||||||
|
?.copyWith(
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.onSurface.withOpacity(0.8),
|
||||||
|
fontSize: compact ? 11 : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (showImages && imageIds.isNotEmpty)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8),
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
|
children: imageIds.map((imageId) {
|
||||||
|
return SizedBox(
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: CloudImageWidget(
|
||||||
|
fileId: imageId,
|
||||||
|
aspectRatio: 1,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
trailing: notification.viewedAt != null
|
||||||
|
? null
|
||||||
|
: Container(
|
||||||
|
width: compact ? 8 : 12,
|
||||||
|
height: compact ? 8 : 12,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.blue,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
if (notification.meta['action_uri'] != null) {
|
||||||
|
var uri = notification.meta['action_uri'] as String;
|
||||||
|
if (uri.startsWith('/')) {
|
||||||
|
// In-app routes
|
||||||
|
rootNavigatorKey.currentContext?.push(
|
||||||
|
notification.meta['action_uri'],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// External URLs
|
||||||
|
launchUrlString(uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user