From 0d6424e1553e838f569eccb5bd7e4cb8e8721b73 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Thu, 12 Jun 2025 23:56:01 +0800 Subject: [PATCH] :sparkles: Fortune graph --- assets/i18n/en-US.json | 13 +- assets/i18n/zh-CN.json | 7 +- ios/Runner/Runner.entitlements | 2 + lib/pods/event_calendar.dart | 56 ++++ .../account/me => pods}/event_calendar.g.dart | 92 ++--- lib/screens/account/me/event_calendar.dart | 316 +++++++----------- .../account/me/event_calendar.freezed.dart | 148 -------- lib/screens/account/profile.dart | 2 +- lib/screens/posts/pub_profile.dart | 22 +- lib/widgets/account/event_calendar.dart | 193 +++++++++++ lib/widgets/account/fortune_graph.dart | 258 ++++++++++++++ .../content/cloud_file_collection.dart | 1 - macos/Runner.xcodeproj/project.pbxproj | 3 + macos/Runner/DebugProfile.entitlements | 2 + macos/Runner/Release.entitlements | 2 + pubspec.lock | 10 +- pubspec.yaml | 1 + 17 files changed, 729 insertions(+), 399 deletions(-) create mode 100644 lib/pods/event_calendar.dart rename lib/{screens/account/me => pods}/event_calendar.g.dart (56%) delete mode 100644 lib/screens/account/me/event_calendar.freezed.dart create mode 100644 lib/widgets/account/event_calendar.dart create mode 100644 lib/widgets/account/fortune_graph.dart diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index 105f305..6c9859d 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -134,14 +134,11 @@ "checkInResultLevel2": "A Normal Day", "checkInResultLevel3": "Good Luck", "checkInResultLevel4": "Best Luck", - "checkInResultLevelShort0": "Wrost", - "checkInResultLevelShort1": "Bad", - "checkInResultLevelShort2": "Normal", - "checkInResultLevelShort3": "Good", - "checkInResultLevelShort4": "Best", "checkInActivityTitle": "{} checked in on {} and got a {}", "eventCalander": "Event Calander", "eventCalanderEmpty": "No events on that day.", + "fortuneGraph": "Fortune Trend", + "noFortuneData": "No fortune data available for this month.", "creatorHub": "Creator Hub", "creatorHubDescription": "Manage posts, analytics, and more.", "developerPortal": "Developer Portal", @@ -423,5 +420,9 @@ "timeZone": "Time Zone", "birthday": "Birthday", "selectADate": "Select a date", - "useDeviceTimeZone": "Use device time zone" + "checkInResultT0": "Worst", + "checkInResultT1": "Poor", + "checkInResultT2": "Mid", + "checkInResultT3": "Good", + "checkInResultT4": "Best" } diff --git a/assets/i18n/zh-CN.json b/assets/i18n/zh-CN.json index 47b8401..4d83e3b 100644 --- a/assets/i18n/zh-CN.json +++ b/assets/i18n/zh-CN.json @@ -307,5 +307,10 @@ "chatBreakCleared": "聊天暂停已清除。", "chatBreakCustom": "自定义时长", "chatBreakEnterMinutes": "输入分钟数", - "chatBreakNone": "无" + "chatBreakNone": "无", + "checkInResultT0": "大凶", + "checkInResultT1": "凶", + "checkInResultT2": "中平", + "checkInResultT3": "吉", + "checkInResultT4": "大吉" } \ No newline at end of file diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements index 29326e3..e14e33c 100644 --- a/ios/Runner/Runner.entitlements +++ b/ios/Runner/Runner.entitlements @@ -4,6 +4,8 @@ aps-environment development + com.apple.developer.device-information.user-assigned-device-name + com.apple.developer.usernotifications.communication diff --git a/lib/pods/event_calendar.dart b/lib/pods/event_calendar.dart new file mode 100644 index 0000000..8a1a988 --- /dev/null +++ b/lib/pods/event_calendar.dart @@ -0,0 +1,56 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/models/activity.dart'; +import 'package:island/pods/network.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'event_calendar.g.dart'; + +/// Query parameters for fetching event calendar data +class EventCalendarQuery { + /// Username to fetch calendar for, null means current user ('me') + final String? uname; + + /// Year to fetch calendar for + final int year; + + /// Month to fetch calendar for + final int month; + + const EventCalendarQuery({ + required this.uname, + required this.year, + required this.month, + }); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is EventCalendarQuery && + runtimeType == other.runtimeType && + uname == other.uname && + year == other.year && + month == other.month; + + @override + int get hashCode => uname.hashCode ^ year.hashCode ^ month.hashCode; +} + +/// Provider for fetching event calendar data +/// This can be used anywhere in the app where calendar data is needed +@riverpod +Future> eventCalendar( + Ref ref, + EventCalendarQuery query, +) async { + final client = ref.watch(apiClientProvider); + final resp = await client.get('/accounts/${query.uname ?? 'me'}/calendar', + queryParameters: { + 'year': query.year, + 'month': query.month, + }, + ); + return resp.data + .map((e) => SnEventCalendarEntry.fromJson(e)) + .cast() + .toList(); +} \ No newline at end of file diff --git a/lib/screens/account/me/event_calendar.g.dart b/lib/pods/event_calendar.g.dart similarity index 56% rename from lib/screens/account/me/event_calendar.g.dart rename to lib/pods/event_calendar.g.dart index 01d8726..4812c10 100644 --- a/lib/screens/account/me/event_calendar.g.dart +++ b/lib/pods/event_calendar.g.dart @@ -6,8 +6,7 @@ part of 'event_calendar.dart'; // RiverpodGenerator // ************************************************************************** -String _$accountEventCalendarHash() => - r'57405caaf53a83d121b6bb4b70540134fb581525'; +String _$eventCalendarHash() => r'6f2454404fa8660b96334d654490e1a40ee53e10'; /// Copied from Dart SDK class _SystemHash { @@ -30,24 +29,36 @@ class _SystemHash { } } -/// See also [accountEventCalendar]. -@ProviderFor(accountEventCalendar) -const accountEventCalendarProvider = AccountEventCalendarFamily(); +/// Provider for fetching event calendar data +/// This can be used anywhere in the app where calendar data is needed +/// +/// Copied from [eventCalendar]. +@ProviderFor(eventCalendar) +const eventCalendarProvider = EventCalendarFamily(); -/// See also [accountEventCalendar]. -class AccountEventCalendarFamily +/// Provider for fetching event calendar data +/// This can be used anywhere in the app where calendar data is needed +/// +/// Copied from [eventCalendar]. +class EventCalendarFamily extends Family>> { - /// See also [accountEventCalendar]. - const AccountEventCalendarFamily(); + /// Provider for fetching event calendar data + /// This can be used anywhere in the app where calendar data is needed + /// + /// Copied from [eventCalendar]. + const EventCalendarFamily(); - /// See also [accountEventCalendar]. - AccountEventCalendarProvider call(EventCalendarQuery query) { - return AccountEventCalendarProvider(query); + /// Provider for fetching event calendar data + /// This can be used anywhere in the app where calendar data is needed + /// + /// Copied from [eventCalendar]. + EventCalendarProvider call(EventCalendarQuery query) { + return EventCalendarProvider(query); } @override - AccountEventCalendarProvider getProviderOverride( - covariant AccountEventCalendarProvider provider, + EventCalendarProvider getProviderOverride( + covariant EventCalendarProvider provider, ) { return call(provider.query); } @@ -64,29 +75,35 @@ class AccountEventCalendarFamily _allTransitiveDependencies; @override - String? get name => r'accountEventCalendarProvider'; + String? get name => r'eventCalendarProvider'; } -/// See also [accountEventCalendar]. -class AccountEventCalendarProvider +/// Provider for fetching event calendar data +/// This can be used anywhere in the app where calendar data is needed +/// +/// Copied from [eventCalendar]. +class EventCalendarProvider extends AutoDisposeFutureProvider> { - /// See also [accountEventCalendar]. - AccountEventCalendarProvider(EventCalendarQuery query) + /// Provider for fetching event calendar data + /// This can be used anywhere in the app where calendar data is needed + /// + /// Copied from [eventCalendar]. + EventCalendarProvider(EventCalendarQuery query) : this._internal( - (ref) => accountEventCalendar(ref as AccountEventCalendarRef, query), - from: accountEventCalendarProvider, - name: r'accountEventCalendarProvider', + (ref) => eventCalendar(ref as EventCalendarRef, query), + from: eventCalendarProvider, + name: r'eventCalendarProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null - : _$accountEventCalendarHash, - dependencies: AccountEventCalendarFamily._dependencies, + : _$eventCalendarHash, + dependencies: EventCalendarFamily._dependencies, allTransitiveDependencies: - AccountEventCalendarFamily._allTransitiveDependencies, + EventCalendarFamily._allTransitiveDependencies, query: query, ); - AccountEventCalendarProvider._internal( + EventCalendarProvider._internal( super._createNotifier, { required super.name, required super.dependencies, @@ -100,15 +117,13 @@ class AccountEventCalendarProvider @override Override overrideWith( - FutureOr> Function( - AccountEventCalendarRef provider, - ) + FutureOr> Function(EventCalendarRef provider) create, ) { return ProviderOverride( origin: this, - override: AccountEventCalendarProvider._internal( - (ref) => create(ref as AccountEventCalendarRef), + override: EventCalendarProvider._internal( + (ref) => create(ref as EventCalendarRef), from: from, name: null, dependencies: null, @@ -121,12 +136,12 @@ class AccountEventCalendarProvider @override AutoDisposeFutureProviderElement> createElement() { - return _AccountEventCalendarProviderElement(this); + return _EventCalendarProviderElement(this); } @override bool operator ==(Object other) { - return other is AccountEventCalendarProvider && other.query == query; + return other is EventCalendarProvider && other.query == query; } @override @@ -140,20 +155,19 @@ class AccountEventCalendarProvider @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element -mixin AccountEventCalendarRef +mixin EventCalendarRef on AutoDisposeFutureProviderRef> { /// The parameter `query` of this provider. EventCalendarQuery get query; } -class _AccountEventCalendarProviderElement +class _EventCalendarProviderElement extends AutoDisposeFutureProviderElement> - with AccountEventCalendarRef { - _AccountEventCalendarProviderElement(super.provider); + with EventCalendarRef { + _EventCalendarProviderElement(super.provider); @override - EventCalendarQuery get query => - (origin as AccountEventCalendarProvider).query; + EventCalendarQuery get query => (origin as EventCalendarProvider).query; } // ignore_for_file: type=lint diff --git a/lib/screens/account/me/event_calendar.dart b/lib/screens/account/me/event_calendar.dart index 52d9a25..fa8181e 100644 --- a/lib/screens/account/me/event_calendar.dart +++ b/lib/screens/account/me/event_calendar.dart @@ -2,43 +2,15 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:island/models/activity.dart'; -import 'package:island/pods/network.dart'; +import 'package:island/pods/event_calendar.dart'; import 'package:island/screens/account/profile.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/content/cloud_files.dart'; -import 'package:material_symbols_icons/symbols.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:island/widgets/account/event_calendar.dart'; +import 'package:island/widgets/account/fortune_graph.dart'; import 'package:styled_widget/styled_widget.dart'; -import 'package:table_calendar/table_calendar.dart'; - -part 'event_calendar.g.dart'; -part 'event_calendar.freezed.dart'; - -@freezed -sealed class EventCalendarQuery with _$EventCalendarQuery { - const factory EventCalendarQuery({ - required String? uname, - required int year, - required int month, - }) = _EventCalendarQuery; -} - -@riverpod -Future> accountEventCalendar( - Ref ref, - EventCalendarQuery query, -) async { - final client = ref.watch(apiClientProvider); - final resp = await client.get('/accounts/${query.uname ?? 'me'}/calendar'); - return resp.data - .map((e) => SnEventCalendarEntry.fromJson(e)) - .cast() - .toList(); -} @RoutePage() class EventCalanderScreen extends HookConsumerWidget { @@ -47,170 +19,33 @@ class EventCalanderScreen extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final selectedMonth = useState(DateTime.now().month); - final selectedYear = useState(DateTime.now().year); + // Get the current date + final now = DateTime.now(); - final selectedDay = useState(DateTime.now()); + // Create the query for the current month + final query = useState( + EventCalendarQuery(uname: name, year: now.year, month: now.month), + ); + // Watch the event calendar data + final events = ref.watch(eventCalendarProvider(query.value)); final user = ref.watch(accountProvider(name)); - final events = ref.watch( - accountEventCalendarProvider( - EventCalendarQuery( - uname: name, - year: selectedYear.value, - month: selectedMonth.value, - ), - ), - ); - final content = Column( - children: [ - TableCalendar( - locale: EasyLocalization.of(context)!.locale.toString(), - firstDay: DateTime.now().add(Duration(days: -3650)), - lastDay: DateTime.now().add(Duration(days: 3650)), - focusedDay: DateTime.utc( - selectedYear.value, - selectedMonth.value, - DateTime.now().day, - ), - calendarFormat: CalendarFormat.month, - selectedDayPredicate: (day) { - return isSameDay(selectedDay.value, day); - }, - onDaySelected: (value, _) { - selectedDay.value = value; - }, - onPageChanged: (focusedDay) { - selectedMonth.value = focusedDay.month; - selectedYear.value = focusedDay.year; - }, - eventLoader: (day) { - return events.value - ?.where((e) => isSameDay(e.date, day)) - .expand((e) => [...e.statuses, e.checkInResult]) - .where((e) => e != null) - .toList() ?? - []; - }, - calendarBuilders: CalendarBuilders( - dowBuilder: (context, day) { - final text = DateFormat.EEEEE().format(day); - return Center(child: Text(text)); - }, - markerBuilder: (context, day, events) { - var checkInResult = - events.whereType().firstOrNull; - if (checkInResult != null) { - return Positioned( - top: 32, - child: Text( - ['大凶', '凶', '中平', '吉', '大吉'][checkInResult.level], - style: TextStyle( - fontSize: 9, - color: - isSameDay(selectedDay.value, day) - ? Theme.of(context).colorScheme.onPrimaryContainer - : isSameDay(DateTime.now(), day) - ? Theme.of( - context, - ).colorScheme.onSecondaryContainer - : Theme.of(context).colorScheme.onSurface, - ), - ), - ); - } - return null; - }, - ), - ), - const Divider(height: 1).padding(top: 8), - AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: Builder( - builder: (context) { - final event = - events.value - ?.where((e) => isSameDay(e.date, selectedDay.value)) - .firstOrNull; - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text(DateFormat.EEEE().format(selectedDay.value)) - .fontSize(16) - .bold() - .textColor( - Theme.of(context).colorScheme.onSecondaryContainer, - ), - Text(DateFormat.yMd().format(selectedDay.value)) - .fontSize(12) - .textColor( - Theme.of(context).colorScheme.onSecondaryContainer, - ), - const Gap(16), - if (event?.checkInResult != null) - Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - 'checkInResultLevel${event!.checkInResult!.level}', - ).tr().fontSize(16).bold(), - for (final tip in event.checkInResult!.tips) - Row( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 8, - children: [ - Icon( - Symbols.circle, - size: 12, - fill: 1, - ).padding(top: 4, right: 4), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(tip.title).bold(), - Text(tip.content), - ], - ), - ), - ], - ).padding(top: 8), - ], - ), - if (event?.checkInResult == null && - (event?.statuses.isEmpty ?? true)) - Text('eventCalanderEmpty').tr(), - ], - ).padding(vertical: 24, horizontal: 24); - }, - ), - ), - if (name != 'me' && user.hasValue) - Container( - decoration: BoxDecoration( - border: Border.all( - width: 1 / MediaQuery.of(context).devicePixelRatio, - color: Theme.of(context).dividerColor, - ), - borderRadius: BorderRadius.all(Radius.circular(8)), - ), - margin: EdgeInsets.all(16), - child: Card( - margin: EdgeInsets.zero, - elevation: 0, - color: Colors.transparent, - child: ListTile( - leading: ProfilePictureWidget( - fileId: user.value!.profile.picture?.id, - ), - title: Text(user.value!.nick).bold(), - subtitle: Text('@${user.value!.name}'), - ), - ), - ), - ], - ); + // Track the selected day for synchronizing between widgets + final selectedDay = useState(now); + + void onMonthChanged(int year, int month) { + query.value = EventCalendarQuery( + uname: query.value.uname, + year: year, + month: month, + ); + } + + // Function to handle day selection for synchronizing between widgets + void onDaySelected(DateTime day) { + selectedDay.value = day; + } return AppScaffold( noBackground: false, @@ -223,9 +58,104 @@ class EventCalanderScreen extends HookConsumerWidget { MediaQuery.of(context).size.width > 480 ? ConstrainedBox( constraints: BoxConstraints(maxWidth: 480), - child: Card(margin: EdgeInsets.all(16), child: content), + child: Column( + children: [ + Card( + margin: EdgeInsets.all(16), + child: Column( + children: [ + // Use the reusable EventCalendarWidget + EventCalendarWidget( + events: events, + initialDate: now, + showEventDetails: true, + onMonthChanged: onMonthChanged, + onDaySelected: onDaySelected, + ), + ], + ), + ), + + // Add the fortune graph widget + const Divider(height: 1), + FortuneGraphWidget( + events: events, + constrainWidth: true, + onPointSelected: onDaySelected, + ), + + // Show user profile if viewing someone else's calendar + if (name != 'me' && user.hasValue) + Container( + decoration: BoxDecoration( + border: Border.all( + width: + 1 / MediaQuery.of(context).devicePixelRatio, + color: Theme.of(context).dividerColor, + ), + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + margin: EdgeInsets.all(16), + child: Card( + margin: EdgeInsets.zero, + elevation: 0, + color: Colors.transparent, + child: ListTile( + leading: ProfilePictureWidget( + fileId: user.value!.profile.picture?.id, + ), + title: Text(user.value!.nick).bold(), + subtitle: Text('@${user.value!.name}'), + ), + ), + ), + ], + ), ).center() - : content, + : Column( + children: [ + // Use the reusable EventCalendarWidget + EventCalendarWidget( + events: events, + initialDate: now, + showEventDetails: true, + onMonthChanged: onMonthChanged, + onDaySelected: onDaySelected, + ), + + // Add the fortune graph widget + const Divider(height: 1), + FortuneGraphWidget( + events: events, + onPointSelected: onDaySelected, + ).padding(horizontal: 8, vertical: 4), + + // Show user profile if viewing someone else's calendar + if (name != 'me' && user.hasValue) + Container( + decoration: BoxDecoration( + border: Border.all( + width: 1 / MediaQuery.of(context).devicePixelRatio, + color: Theme.of(context).dividerColor, + ), + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + margin: EdgeInsets.all(16), + child: Card( + margin: EdgeInsets.zero, + elevation: 0, + color: Colors.transparent, + child: ListTile( + leading: ProfilePictureWidget( + fileId: user.value!.profile.picture?.id, + ), + title: Text(user.value!.nick).bold(), + subtitle: Text('@${user.value!.name}'), + ), + ), + ), + ], + ), ), ); } diff --git a/lib/screens/account/me/event_calendar.freezed.dart b/lib/screens/account/me/event_calendar.freezed.dart deleted file mode 100644 index 023e078..0000000 --- a/lib/screens/account/me/event_calendar.freezed.dart +++ /dev/null @@ -1,148 +0,0 @@ -// dart format width=80 -// coverage:ignore-file -// GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'event_calendar.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -// dart format off -T _$identity(T value) => value; -/// @nodoc -mixin _$EventCalendarQuery { - - String? get uname; int get year; int get month; -/// Create a copy of EventCalendarQuery -/// with the given fields replaced by the non-null parameter values. -@JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -$EventCalendarQueryCopyWith get copyWith => _$EventCalendarQueryCopyWithImpl(this as EventCalendarQuery, _$identity); - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is EventCalendarQuery&&(identical(other.uname, uname) || other.uname == uname)&&(identical(other.year, year) || other.year == year)&&(identical(other.month, month) || other.month == month)); -} - - -@override -int get hashCode => Object.hash(runtimeType,uname,year,month); - -@override -String toString() { - return 'EventCalendarQuery(uname: $uname, year: $year, month: $month)'; -} - - -} - -/// @nodoc -abstract mixin class $EventCalendarQueryCopyWith<$Res> { - factory $EventCalendarQueryCopyWith(EventCalendarQuery value, $Res Function(EventCalendarQuery) _then) = _$EventCalendarQueryCopyWithImpl; -@useResult -$Res call({ - String? uname, int year, int month -}); - - - - -} -/// @nodoc -class _$EventCalendarQueryCopyWithImpl<$Res> - implements $EventCalendarQueryCopyWith<$Res> { - _$EventCalendarQueryCopyWithImpl(this._self, this._then); - - final EventCalendarQuery _self; - final $Res Function(EventCalendarQuery) _then; - -/// Create a copy of EventCalendarQuery -/// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? uname = freezed,Object? year = null,Object? month = null,}) { - return _then(_self.copyWith( -uname: freezed == uname ? _self.uname : uname // ignore: cast_nullable_to_non_nullable -as String?,year: null == year ? _self.year : year // ignore: cast_nullable_to_non_nullable -as int,month: null == month ? _self.month : month // ignore: cast_nullable_to_non_nullable -as int, - )); -} - -} - - -/// @nodoc - - -class _EventCalendarQuery implements EventCalendarQuery { - const _EventCalendarQuery({required this.uname, required this.year, required this.month}); - - -@override final String? uname; -@override final int year; -@override final int month; - -/// Create a copy of EventCalendarQuery -/// with the given fields replaced by the non-null parameter values. -@override @JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -_$EventCalendarQueryCopyWith<_EventCalendarQuery> get copyWith => __$EventCalendarQueryCopyWithImpl<_EventCalendarQuery>(this, _$identity); - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _EventCalendarQuery&&(identical(other.uname, uname) || other.uname == uname)&&(identical(other.year, year) || other.year == year)&&(identical(other.month, month) || other.month == month)); -} - - -@override -int get hashCode => Object.hash(runtimeType,uname,year,month); - -@override -String toString() { - return 'EventCalendarQuery(uname: $uname, year: $year, month: $month)'; -} - - -} - -/// @nodoc -abstract mixin class _$EventCalendarQueryCopyWith<$Res> implements $EventCalendarQueryCopyWith<$Res> { - factory _$EventCalendarQueryCopyWith(_EventCalendarQuery value, $Res Function(_EventCalendarQuery) _then) = __$EventCalendarQueryCopyWithImpl; -@override @useResult -$Res call({ - String? uname, int year, int month -}); - - - - -} -/// @nodoc -class __$EventCalendarQueryCopyWithImpl<$Res> - implements _$EventCalendarQueryCopyWith<$Res> { - __$EventCalendarQueryCopyWithImpl(this._self, this._then); - - final _EventCalendarQuery _self; - final $Res Function(_EventCalendarQuery) _then; - -/// Create a copy of EventCalendarQuery -/// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? uname = freezed,Object? year = null,Object? month = null,}) { - return _then(_EventCalendarQuery( -uname: freezed == uname ? _self.uname : uname // ignore: cast_nullable_to_non_nullable -as String?,year: null == year ? _self.year : year // ignore: cast_nullable_to_non_nullable -as int,month: null == month ? _self.month : month // ignore: cast_nullable_to_non_nullable -as int, - )); -} - - -} - -// dart format on diff --git a/lib/screens/account/profile.dart b/lib/screens/account/profile.dart index 3babe52..a26cf6b 100644 --- a/lib/screens/account/profile.dart +++ b/lib/screens/account/profile.dart @@ -71,7 +71,7 @@ class AccountProfileScreen extends HookConsumerWidget { final appbarColor = ref.watch(accountAppbarForcegroundColorProvider(name)); final appbarShadow = Shadow( - color: appbarColor.value?.invert ?? Colors.black54, + color: appbarColor.value?.invert ?? Colors.transparent, blurRadius: 5.0, offset: Offset(1.0, 1.0), ); diff --git a/lib/screens/posts/pub_profile.dart b/lib/screens/posts/pub_profile.dart index 20b239b..af3bf4c 100644 --- a/lib/screens/posts/pub_profile.dart +++ b/lib/screens/posts/pub_profile.dart @@ -10,6 +10,7 @@ import 'package:island/models/user.dart'; import 'package:island/pods/config.dart'; import 'package:island/pods/network.dart'; import 'package:island/services/color.dart'; +import 'package:island/widgets/account/account_name.dart'; import 'package:island/widgets/account/badge.dart'; import 'package:island/widgets/account/status.dart'; import 'package:island/widgets/alert.dart'; @@ -113,7 +114,7 @@ class PublisherProfileScreen extends HookConsumerWidget { } final appbarShadow = Shadow( - color: appbarColor.value?.invert ?? Colors.black54, + color: appbarColor.value?.invert ?? Colors.transparent, blurRadius: 5.0, offset: Offset(1.0, 1.0), ); @@ -245,14 +246,17 @@ class PublisherProfileScreen extends HookConsumerWidget { ], ).padding(horizontal: 24, top: 24, bottom: 24), ), - if (badges.value?.isNotEmpty ?? false) - SliverToBoxAdapter( - child: BadgeList( - badges: badges.value!, - ).padding(horizontal: 24, bottom: 24), - ) - else - const SliverGap(16), + SliverToBoxAdapter( + child: Column( + spacing: 24, + children: [ + if (badges.value?.isNotEmpty ?? false) + BadgeList(badges: badges.value!), + if (data.verification != null) + VerificationStatusCard(mark: data.verification!), + ], + ).padding(horizontal: 24, bottom: 24), + ), SliverToBoxAdapter(child: const Divider(height: 1)), if (data.bio.isNotEmpty) SliverToBoxAdapter( diff --git a/lib/widgets/account/event_calendar.dart b/lib/widgets/account/event_calendar.dart new file mode 100644 index 0000000..09c8322 --- /dev/null +++ b/lib/widgets/account/event_calendar.dart @@ -0,0 +1,193 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:gap/gap.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/models/activity.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'package:styled_widget/styled_widget.dart'; +import 'package:table_calendar/table_calendar.dart'; + +/// A reusable widget for displaying an event calendar with event details +/// This can be used in various places throughout the app +class EventCalendarWidget extends HookConsumerWidget { + /// The list of calendar entries to display + final AsyncValue> events; + + /// Initial date to focus on + final DateTime? initialDate; + + /// Whether to show the event details below the calendar + final bool showEventDetails; + + /// Whether to constrain the width of the calendar + final bool constrainWidth; + + /// Maximum width constraint when constrainWidth is true + final double maxWidth; + + /// Callback when a day is selected + final void Function(DateTime)? onDaySelected; + + /// Callback when the focused month changes + final void Function(int year, int month)? onMonthChanged; + + const EventCalendarWidget({ + super.key, + required this.events, + this.initialDate, + this.showEventDetails = true, + this.constrainWidth = false, + this.maxWidth = 480, + this.onDaySelected, + this.onMonthChanged, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedMonth = useState(initialDate?.month ?? DateTime.now().month); + final selectedYear = useState(initialDate?.year ?? DateTime.now().year); + final selectedDay = useState(initialDate ?? DateTime.now()); + + final content = Column( + children: [ + TableCalendar( + locale: EasyLocalization.of(context)!.locale.toString(), + firstDay: DateTime.now().add(Duration(days: -3650)), + lastDay: DateTime.now().add(Duration(days: 3650)), + focusedDay: DateTime.utc( + selectedYear.value, + selectedMonth.value, + selectedDay.value.day, + ), + weekNumbersVisible: false, + calendarFormat: CalendarFormat.month, + selectedDayPredicate: (day) { + return isSameDay(selectedDay.value, day); + }, + onDaySelected: (value, _) { + selectedDay.value = value; + onDaySelected?.call(value); + }, + onPageChanged: (focusedDay) { + selectedMonth.value = focusedDay.month; + selectedYear.value = focusedDay.year; + onMonthChanged?.call(focusedDay.year, focusedDay.month); + }, + eventLoader: (day) { + return events.value + ?.where((e) => isSameDay(e.date, day)) + .expand((e) => [...e.statuses, e.checkInResult]) + .where((e) => e != null) + .toList() ?? + []; + }, + calendarBuilders: CalendarBuilders( + dowBuilder: (context, day) { + final text = DateFormat.EEEEE().format(day); + return Center(child: Text(text)); + }, + markerBuilder: (context, day, events) { + var checkInResult = + events.whereType().firstOrNull; + if (checkInResult != null) { + return Positioned( + top: 32, + child: Text( + 'checkInResultT${checkInResult.level}'.tr(), + style: TextStyle( + fontSize: 9, + color: + isSameDay(selectedDay.value, day) + ? Theme.of(context).colorScheme.onPrimaryContainer + : isSameDay(DateTime.now(), day) + ? Theme.of( + context, + ).colorScheme.onSecondaryContainer + : Theme.of(context).colorScheme.onSurface, + ), + ), + ); + } + return null; + }, + ), + ), + if (showEventDetails) ...[ + const Divider(height: 1).padding(top: 8), + AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: Builder( + builder: (context) { + final event = + events.value + ?.where((e) => isSameDay(e.date, selectedDay.value)) + .firstOrNull; + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text(DateFormat.EEEE().format(selectedDay.value)) + .fontSize(16) + .bold() + .textColor( + Theme.of(context).colorScheme.onSecondaryContainer, + ), + Text(DateFormat.yMd().format(selectedDay.value)) + .fontSize(12) + .textColor( + Theme.of(context).colorScheme.onSecondaryContainer, + ), + const Gap(16), + if (event?.checkInResult != null) + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + 'checkInResultLevel${event!.checkInResult!.level}', + ).tr().fontSize(16).bold(), + for (final tip in event.checkInResult!.tips) + Row( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8, + children: [ + Icon( + Symbols.circle, + size: 12, + fill: 1, + ).padding(top: 4, right: 4), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text(tip.title).bold(), + Text(tip.content), + ], + ), + ), + ], + ).padding(top: 8), + ], + ), + if (event?.checkInResult == null && + (event?.statuses.isEmpty ?? true)) + Text('eventCalanderEmpty').tr(), + ], + ).padding(vertical: 24, horizontal: 24); + }, + ), + ), + ], + ], + ); + + if (constrainWidth) { + return ConstrainedBox( + constraints: BoxConstraints(maxWidth: maxWidth), + child: Card(margin: EdgeInsets.all(16), child: content), + ).center(); + } + + return content; + } +} diff --git a/lib/widgets/account/fortune_graph.dart b/lib/widgets/account/fortune_graph.dart new file mode 100644 index 0000000..46da9ea --- /dev/null +++ b/lib/widgets/account/fortune_graph.dart @@ -0,0 +1,258 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/models/activity.dart'; +import 'package:styled_widget/styled_widget.dart'; + +/// A widget that displays a graph of fortune levels over time +/// This can be used alongside the EventCalendarWidget to provide a different visualization +class FortuneGraphWidget extends HookConsumerWidget { + /// The list of calendar entries to display + final AsyncValue> events; + + /// Whether to constrain the width of the graph + final bool constrainWidth; + + /// Maximum width constraint when constrainWidth is true + final double maxWidth; + + /// Height of the graph + final double height; + + /// Callback when a point is selected + final void Function(DateTime)? onPointSelected; + + const FortuneGraphWidget({ + super.key, + required this.events, + this.constrainWidth = false, + this.maxWidth = double.infinity, + this.height = 180, + this.onPointSelected, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + // Filter events to only include those with check-in results + final filteredEvents = events.whenData( + (data) => + data + .where((event) => event.checkInResult != null) + .toList() + .cast() + // Sort by date + ..sort((a, b) => a.date.compareTo(b.date)), + ); + + final content = Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + 'fortuneGraph', + ).tr().fontSize(18).bold().padding(all: 16, bottom: 24), + SizedBox( + height: height, + child: filteredEvents.when( + data: (data) { + if (data.isEmpty) { + return Center(child: Text('noFortuneData').tr()); + } + + // Create spots for the line chart + final spots = + data + .map( + (e) => FlSpot( + e.date.millisecondsSinceEpoch.toDouble(), + e.checkInResult!.level.toDouble(), + ), + ) + .toList(); + + // Get min and max dates for the x-axis + final minDate = data.first.date; + final maxDate = data.last.date; + + return Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 24), + child: LineChart( + LineChartData( + gridData: FlGridData( + show: true, + horizontalInterval: 1, + drawVerticalLine: false, + ), + titlesData: FlTitlesData( + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 30, + interval: _calculateDateInterval(minDate, maxDate), + getTitlesWidget: (value, meta) { + final date = DateTime.fromMillisecondsSinceEpoch( + value.toInt(), + ); + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + DateFormat.MMMd().format(date), + style: TextStyle(fontSize: 10), + ), + ); + }, + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + interval: 1, + reservedSize: 40, + getTitlesWidget: (value, meta) { + final level = value.toInt(); + if (level < 0 || level > 4) return const SizedBox(); + return Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text( + 'checkInResultT$level'.tr(), + style: TextStyle(fontSize: 10), + ), + ); + }, + ), + ), + topTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + rightTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + ), + borderData: FlBorderData( + show: true, + border: Border( + bottom: BorderSide( + color: Theme.of(context).dividerColor, + ), + left: BorderSide(color: Theme.of(context).dividerColor), + ), + ), + minX: minDate.millisecondsSinceEpoch.toDouble(), + maxX: maxDate.millisecondsSinceEpoch.toDouble(), + minY: 0, + maxY: 4, + lineTouchData: LineTouchData( + touchTooltipData: LineTouchTooltipData( + getTooltipItems: (touchedSpots) { + return touchedSpots.map((spot) { + final date = DateTime.fromMillisecondsSinceEpoch( + spot.x.toInt(), + ); + final level = spot.y.toInt(); + return LineTooltipItem( + '${DateFormat.yMMMd().format(date)}\n', + TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.bold, + ), + children: [ + TextSpan( + text: 'checkInResultLevel$level'.tr(), + style: TextStyle( + color: + Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.normal, + ), + ), + ], + ); + }).toList(); + }, + ), + touchCallback: ( + FlTouchEvent event, + LineTouchResponse? response, + ) { + if (event is FlTapUpEvent && + response != null && + response.lineBarSpots != null && + response.lineBarSpots!.isNotEmpty) { + final spot = response.lineBarSpots!.first; + final date = DateTime.fromMillisecondsSinceEpoch( + spot.x.toInt(), + ); + onPointSelected?.call(date); + } + }, + ), + lineBarsData: [ + LineChartBarData( + spots: spots, + isCurved: true, + color: Theme.of(context).colorScheme.primary, + barWidth: 3, + isStrokeCapRound: true, + dotData: FlDotData( + show: true, + getDotPainter: (spot, percent, barData, index) { + return FlDotCirclePainter( + radius: 4, + color: Theme.of(context).colorScheme.primary, + strokeWidth: 2, + strokeColor: + Theme.of(context).colorScheme.surface, + ); + }, + ), + belowBarData: BarAreaData( + show: true, + color: Theme.of( + context, + ).colorScheme.primary.withOpacity(0.2), + ), + ), + ], + ), + ), + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stack) => Center(child: Text('Error: $error')), + ), + ), + ], + ); + + if (constrainWidth) { + return ConstrainedBox( + constraints: BoxConstraints(maxWidth: maxWidth), + child: Card(margin: EdgeInsets.all(16), child: content), + ).center(); + } + + return content; + } + + /// Calculate an appropriate interval for date labels based on the date range + double _calculateDateInterval(DateTime minDate, DateTime maxDate) { + final difference = maxDate.difference(minDate).inDays; + + // If less than 7 days, show all days + if (difference <= 7) { + return 24 * 60 * 60 * 1000; // One day in milliseconds + } + + // If less than a month, show every 3 days + if (difference <= 30) { + return 3 * 24 * 60 * 60 * 1000; // Three days in milliseconds + } + + // If less than 3 months, show weekly + if (difference <= 90) { + return 7 * 24 * 60 * 60 * 1000; // One week in milliseconds + } + + // Otherwise show every 2 weeks + return 14 * 24 * 60 * 60 * 1000; // Two weeks in milliseconds + } +} diff --git a/lib/widgets/content/cloud_file_collection.dart b/lib/widgets/content/cloud_file_collection.dart index 0e17ea3..0053a66 100644 --- a/lib/widgets/content/cloud_file_collection.dart +++ b/lib/widgets/content/cloud_file_collection.dart @@ -17,7 +17,6 @@ import 'package:path_provider/path_provider.dart'; import 'package:photo_view/photo_view.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:uuid/uuid.dart'; -import 'package:dio/dio.dart'; class CloudFileList extends HookConsumerWidget { final List files; diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index d30fc74..85590e4 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -591,6 +591,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = W7HPZ53V6B; @@ -728,6 +729,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = W7HPZ53V6B; @@ -753,6 +755,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = W7HPZ53V6B; diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements index 5cdcb7b..157dc8e 100644 --- a/macos/Runner/DebugProfile.entitlements +++ b/macos/Runner/DebugProfile.entitlements @@ -18,5 +18,7 @@ com.apple.security.network.server + com.apple.developer.device-information.user-assigned-device-name + diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements index 986d2a4..e29630b 100644 --- a/macos/Runner/Release.entitlements +++ b/macos/Runner/Release.entitlements @@ -16,5 +16,7 @@ com.apple.security.network.server + com.apple.developer.device-information.user-assigned-device-name + diff --git a/pubspec.lock b/pubspec.lock index 363ed12..2667b91 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -681,6 +681,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + fl_chart: + dependency: "direct main" + description: + name: fl_chart + sha256: "577aeac8ca414c25333334d7c4bb246775234c0e44b38b10a82b559dd4d764e7" + url: "https://pub.dev" + source: hosted + version: "1.0.0" flutter: dependency: "direct main" description: flutter @@ -2469,4 +2477,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.8.0 <4.0.0" - flutter: ">=3.27.0" + flutter: ">=3.27.4" diff --git a/pubspec.yaml b/pubspec.yaml index 503bd06..0d9f8da 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -112,6 +112,7 @@ dependencies: flutter_popup_card: ^0.0.6 timezone: ^0.10.1 flutter_timezone: ^4.1.1 + fl_chart: ^1.0.0 dev_dependencies: flutter_test: