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: