diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index 1747d60..41eabda 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -378,9 +378,12 @@ "dailyCheckNegativeHint5Description": "Lost connection at a crucial moment", "dailyCheckNegativeHint6": "Going out", "dailyCheckNegativeHint6Description": "Forgot your umbrella and got caught in the rain", - "happyBirthday": "Happy birthday, {}!", + "celebrateBirthday": "Happy birthday, {}!", "celebrateMerryXmas": "Merry christmas, {}!", "celebrateNewYear": "Happy new year, {}!", + "pendingBirthday": "Birthday in {}", + "pendingMerryXmas": "Christmas in {}", + "pendingNewYear": "New year in {}", "friendNew": "Add Friend", "friendRequests": "Friend Requests", "friendRequestsDescription": { diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index 6a427b6..43dd431 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -376,9 +376,12 @@ "dailyCheckNegativeHint5Description": "关键时刻断网", "dailyCheckNegativeHint6": "出门", "dailyCheckNegativeHint6Description": "忘带伞遇上大雨", - "happyBirthday": "生日快乐,{}!", + "celebrateBirthday": "生日快乐,{}!", "celebrateMerryXmas": "圣诞快乐,{}!", "celebrateNewYear": "新年快乐,{}!", + "pendingBirthday": "{} 过生日", + "pendingMerryXmas": "{} 过圣诞节", + "pendingNewYear": "{} 跨年", "friendNew": "添加好友", "friendRequests": "好友请求", "friendRequestsDescription": { diff --git a/assets/translations/zh-HK.json b/assets/translations/zh-HK.json index 9212e06..4d1a265 100644 --- a/assets/translations/zh-HK.json +++ b/assets/translations/zh-HK.json @@ -376,9 +376,12 @@ "dailyCheckNegativeHint5Description": "關鍵時刻斷網", "dailyCheckNegativeHint6": "出門", "dailyCheckNegativeHint6Description": "忘帶傘遇上大雨", - "happyBirthday": "生日快樂,{}!", + "celebrateBirthday": "生日快樂,{}!", "celebrateMerryXmas": "聖誕快樂,{}!", "celebrateNewYear": "新年快樂,{}!", + "pendingBirthday": "{} 過生日", + "pendingMerryXmas": "{} 過聖誕節", + "pendingNewYear": "{} 跨年", "friendNew": "添加好友", "friendRequests": "好友請求", "friendRequestsDescription": { diff --git a/assets/translations/zh-TW.json b/assets/translations/zh-TW.json index ed4c50d..2b93fe5 100644 --- a/assets/translations/zh-TW.json +++ b/assets/translations/zh-TW.json @@ -376,9 +376,12 @@ "dailyCheckNegativeHint5Description": "關鍵時刻斷網", "dailyCheckNegativeHint6": "出門", "dailyCheckNegativeHint6Description": "忘帶傘遇上大雨", - "happyBirthday": "生日快樂,{}!", + "celebrateBirthday": "生日快樂,{}!", "celebrateMerryXmas": "聖誕快樂,{}!", "celebrateNewYear": "新年快樂,{}!", + "pendingBirthday": "{} 過生日", + "pendingMerryXmas": "{} 過聖誕節", + "pendingNewYear": "{} 跨年", "friendNew": "新增好友", "friendRequests": "好友請求", "friendRequestsDescription": { diff --git a/lib/main.dart b/lib/main.dart index 9a47f28..f8e1368 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -30,6 +30,7 @@ import 'package:surface/providers/post.dart'; import 'package:surface/providers/relationship.dart'; import 'package:surface/providers/sn_attachment.dart'; import 'package:surface/providers/sn_network.dart'; +import 'package:surface/providers/special_day.dart'; import 'package:surface/providers/theme.dart'; import 'package:surface/providers/user_directory.dart'; import 'package:surface/providers/userinfo.dart'; @@ -148,6 +149,9 @@ class SolianApp extends StatelessWidget { ChangeNotifierProvider(create: (ctx) => NotificationProvider(ctx)), ChangeNotifierProvider(create: (ctx) => ChatChannelProvider(ctx)), ChangeNotifierProvider(create: (ctx) => ChatCallProvider(ctx)), + + // Additional helper layer + Provider(create: (ctx) => SpecialDayProvider(ctx)), ], child: _AppDelegate(), ), diff --git a/lib/providers/special_day.dart b/lib/providers/special_day.dart new file mode 100644 index 0000000..fbb3d5c --- /dev/null +++ b/lib/providers/special_day.dart @@ -0,0 +1,122 @@ +import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; +import 'package:surface/providers/userinfo.dart'; + +// Stored as key: month, day +const Map kSpecialDays = { + // Birthday is dynamically generated according to the user's profile + 'NewYear': (1, 1), + 'MerryXmas': (12, 25), +}; + +const Map kSpecialDaysSymbol = { + 'Birthday': '🎂', + 'NewYear': '🎉', + 'MerryXmas': '🎄', +}; + +class SpecialDayProvider { + late final UserProvider _user; + + SpecialDayProvider(BuildContext context) { + _user = context.read(); + } + + List getSpecialDays() { + final now = DateTime.now().toLocal(); + final birthday = _user.user?.profile?.birthday?.toLocal(); + final isBirthday = birthday != null && birthday.day == now.day && birthday.month == now.month; + + return [ + if (isBirthday) 'Birthday', + ...kSpecialDays.keys.where( + (key) => kSpecialDays[key]!.$1 == now.month && kSpecialDays[key]!.$2 == now.day, + ), + ]; + } + + (String, DateTime)? getLastSpecialDay() { + final now = DateTime.now().toLocal(); + final birthday = _user.user?.profile?.birthday?.toLocal(); + + final Map specialDays = { + if (birthday != null) 'Birthday': (birthday.month, birthday.day), + ...kSpecialDays, + }; + + DateTime? lastDate; + String? lastEvent; + + for (final entry in specialDays.entries) { + final eventName = entry.key; + final (month, day) = entry.value; + + var specialDayThisYear = DateTime(now.year, month, day); + var specialDayLastYear = DateTime(now.year - 1, month, day); + + if (specialDayThisYear.isBefore(now)) { + if (lastDate == null || specialDayThisYear.isAfter(lastDate)) { + lastDate = specialDayThisYear; + lastEvent = eventName; + } + } else if (specialDayLastYear.isBefore(now)) { + if (lastDate == null || specialDayLastYear.isAfter(lastDate)) { + lastDate = specialDayLastYear; + lastEvent = eventName; + } + } + } + + if (lastEvent != null && lastDate != null) { + return (lastEvent, lastDate); + } + + return null; + } + + (String, DateTime)? getNextSpecialDay() { + final now = DateTime.now().toLocal(); + final birthday = _user.user?.profile?.birthday?.toLocal(); + + // Stored as key: month, day + final Map specialDays = { + if (birthday != null) 'Birthday': (birthday.month, birthday.day), + ...kSpecialDays, + }; + + DateTime? closestDate; + String? closestEvent; + + for (final entry in specialDays.entries) { + final eventName = entry.key; + final (month, day) = entry.value; + + // Calculate the special day's DateTime in the current year + var specialDay = DateTime(now.year, month, day); + + // If the special day has already passed this year, consider it for the next year + if (specialDay.isBefore(now)) { + specialDay = DateTime(now.year + 1, month, day); + } + + // Check if this special day is closer than the previously found one + if (closestDate == null || specialDay.isBefore(closestDate)) { + closestDate = specialDay; + closestEvent = eventName; + } + } + + if (closestEvent != null && closestDate != null) { + return (closestEvent, closestDate); + } + + // No special day found + return null; + } + + double getSpecialDayProgress(DateTime last, DateTime next) { + final totalDuration = next.difference(last).inSeconds.toDouble(); + final elapsedDuration = DateTime.now().difference(last).inSeconds.toDouble(); + return (elapsedDuration / totalDuration).clamp(0.0, 1.0); + } +} diff --git a/lib/screens/home.dart b/lib/screens/home.dart index 1d50c77..a8c7983 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -11,11 +11,13 @@ import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; +import 'package:relative_time/relative_time.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:flutter/material.dart'; import 'package:surface/providers/config.dart'; import 'package:surface/providers/post.dart'; import 'package:surface/providers/sn_network.dart'; +import 'package:surface/providers/special_day.dart'; import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/widget.dart'; import 'package:surface/types/check_in.dart'; @@ -79,8 +81,8 @@ class _HomeScreenState extends State { child: Column( mainAxisAlignment: constraints.maxWidth > 640 ? MainAxisAlignment.center : MainAxisAlignment.start, children: [ - _HomeDashSpecialDayWidget().padding(bottom: 8, horizontal: 8), _HomeDashUpdateWidget(padding: const EdgeInsets.only(bottom: 8, left: 8, right: 8)), + _HomeDashSpecialDayWidget().padding(horizontal: 8), StaggeredGrid.extent( maxCrossAxisExtent: 280, mainAxisSpacing: 8, @@ -156,36 +158,55 @@ class _HomeDashSpecialDayWidget extends StatelessWidget { @override Widget build(BuildContext context) { final ua = context.watch(); - final today = DateTime.now(); - final birthday = ua.user?.profile?.birthday?.toLocal(); - final isBirthday = birthday != null && birthday.day == today.day && birthday.month == today.month; + final dayz = context.watch(); - return Column( - spacing: 8, - children: [ - if (isBirthday) - Card( - child: ListTile( - leading: Text('🎂').fontSize(24), - title: Text('happyBirthday').tr(args: [ua.user?.nick ?? 'user']), - ), - ).padding(bottom: 8), - if (today.month == 12 && today.day == 25) - Card( - child: ListTile( - leading: Text('🎄').fontSize(24), - title: Text('celebrateMerryXmas').tr(args: [ua.user?.nick ?? 'user']), - ), + final days = dayz.getSpecialDays(); + + if (days.isNotEmpty) { + return Column( + spacing: 8, + children: days.map((ele) { + final (name, date) = dayz.getNextSpecialDay()!; + return Card( + child: ListTile( + leading: Text(kSpecialDaysSymbol[name] ?? '🎉').fontSize(24), + title: Text('celebrate$name').tr(args: [ua.user?.nick ?? 'user']), + subtitle: Text(date.toString()), + ), + ).padding(bottom: 8); + }).toList()); + } + + final nextOne = dayz.getNextSpecialDay(); + final lastOne = dayz.getLastSpecialDay(); + + if (nextOne != null && lastOne != null) { + var (name, date) = nextOne; + date = date.add(Duration(days: 1)); + final progress = dayz.getSpecialDayProgress(lastOne.$2, date); + final diff = date.difference(lastOne.$2); + return Card( + child: ListTile( + leading: Text(kSpecialDaysSymbol[name] ?? '🎉').fontSize(24), + title: Text('pending$name').tr(args: [RelativeTime(context).format(date)]), + subtitle: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text('${diff.inDays}d · ${(progress * 100).toStringAsFixed(2)}%'), + const Gap(8), + Expanded( + child: LinearProgressIndicator( + value: progress, + borderRadius: BorderRadius.circular(8), + ), + ), + ], ), - if (today.month == 1 && today.day == 1) - Card( - child: ListTile( - leading: Text('🎉').fontSize(24), - title: Text('celebrateNewYear').tr(args: [ua.user?.nick ?? 'user']), - ), - ), - ], - ); + ), + ).padding(bottom: 8); + } + + return const SizedBox.shrink(); } }