✨ Dashboard basis
This commit is contained in:
@@ -52,7 +52,13 @@ Future<SnNotableDay?> nextNotableDay(Ref ref) async {
|
||||
class CheckInWidget extends HookConsumerWidget {
|
||||
final EdgeInsets? margin;
|
||||
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
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -122,57 +128,77 @@ class CheckInWidget extends HookConsumerWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 6,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
switch (DateTime.now().weekday) {
|
||||
6 || 7 => Symbols.weekend,
|
||||
_ => isAdult ? Symbols.work : Symbols.school,
|
||||
if (checkInOnly)
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: todayResult.when(
|
||||
data: (result) {
|
||||
return Text(
|
||||
result == null
|
||||
? 'checkInNone'
|
||||
: 'checkInResultLevel${result.level}',
|
||||
textAlign: TextAlign.start,
|
||||
).tr().fontSize(15).bold();
|
||||
},
|
||||
fill: 1,
|
||||
size: 16,
|
||||
).padding(right: 2),
|
||||
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,
|
||||
),
|
||||
),
|
||||
loading: () =>
|
||||
Text('checkInNone').tr().fontSize(15).bold(),
|
||||
error: (err, stack) =>
|
||||
Text('error').tr().fontSize(15).bold(),
|
||||
),
|
||||
],
|
||||
),
|
||||
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(),
|
||||
).padding(right: 4),
|
||||
if (!checkInOnly)
|
||||
Row(
|
||||
spacing: 6,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
switch (DateTime.now().weekday) {
|
||||
6 || 7 => Symbols.weekend,
|
||||
_ => isAdult ? Symbols.work : Symbols.school,
|
||||
},
|
||||
fill: 1,
|
||||
size: 16,
|
||||
).padding(right: 2),
|
||||
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),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
@@ -213,14 +239,13 @@ class CheckInWidget extends HookConsumerWidget {
|
||||
);
|
||||
},
|
||||
loading: () => Text('checkInNoneHint').tr().fontSize(11),
|
||||
error:
|
||||
(err, stack) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('error').tr().fontSize(15).bold(),
|
||||
Text(err.toString()).fontSize(11),
|
||||
],
|
||||
),
|
||||
error: (err, stack) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('error').tr().fontSize(15).bold(),
|
||||
Text(err.toString()).fontSize(11),
|
||||
],
|
||||
),
|
||||
),
|
||||
).alignment(Alignment.centerLeft),
|
||||
],
|
||||
@@ -231,21 +256,23 @@ class CheckInWidget extends HookConsumerWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
spacing: 4,
|
||||
children: [
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: todayResult.when(
|
||||
data: (result) {
|
||||
return Text(
|
||||
result == null
|
||||
? 'checkInNone'
|
||||
: 'checkInResultLevel${result.level}',
|
||||
textAlign: TextAlign.start,
|
||||
).tr().fontSize(15).bold();
|
||||
},
|
||||
loading: () => Text('checkInNone').tr().fontSize(15).bold(),
|
||||
error: (err, stack) => Text('error').tr().fontSize(15).bold(),
|
||||
),
|
||||
).padding(right: 4),
|
||||
if (!checkInOnly)
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: todayResult.when(
|
||||
data: (result) {
|
||||
return Text(
|
||||
result == null
|
||||
? 'checkInNone'
|
||||
: 'checkInResultLevel${result.level}',
|
||||
textAlign: TextAlign.start,
|
||||
).tr().fontSize(15).bold();
|
||||
},
|
||||
loading: () => Text('checkInNone').tr().fontSize(15).bold(),
|
||||
error: (err, stack) =>
|
||||
Text('error').tr().fontSize(15).bold(),
|
||||
),
|
||||
).padding(right: 4),
|
||||
IconButton.outlined(
|
||||
iconSize: 16,
|
||||
visualDensity: const VisualDensity(
|
||||
@@ -259,27 +286,22 @@ class CheckInWidget extends HookConsumerWidget {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => SheetScaffold(
|
||||
titleText: 'eventCalendar'.tr(),
|
||||
child: EventCalendarContent(
|
||||
name: 'me',
|
||||
isSheet: true,
|
||||
),
|
||||
),
|
||||
builder: (context) => SheetScaffold(
|
||||
titleText: 'eventCalendar'.tr(),
|
||||
child: EventCalendarContent(name: 'me', isSheet: true),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
icon: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: todayResult.when(
|
||||
data:
|
||||
(result) => Icon(
|
||||
result == null
|
||||
? Symbols.local_fire_department
|
||||
: Symbols.event,
|
||||
key: ValueKey(result != null),
|
||||
),
|
||||
data: (result) => Icon(
|
||||
result == null
|
||||
? Symbols.local_fire_department
|
||||
: Symbols.event,
|
||||
key: ValueKey(result != null),
|
||||
),
|
||||
loading: () => const Icon(Symbols.refresh),
|
||||
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