✨ Fortune graph
This commit is contained in:
parent
0424eb0c2a
commit
0d6424e155
@ -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"
|
||||
}
|
||||
|
@ -307,5 +307,10 @@
|
||||
"chatBreakCleared": "聊天暂停已清除。",
|
||||
"chatBreakCustom": "自定义时长",
|
||||
"chatBreakEnterMinutes": "输入分钟数",
|
||||
"chatBreakNone": "无"
|
||||
"chatBreakNone": "无",
|
||||
"checkInResultT0": "大凶",
|
||||
"checkInResultT1": "凶",
|
||||
"checkInResultT2": "中平",
|
||||
"checkInResultT3": "吉",
|
||||
"checkInResultT4": "大吉"
|
||||
}
|
@ -4,6 +4,8 @@
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.device-information.user-assigned-device-name</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.usernotifications.communication</key>
|
||||
<true/>
|
||||
</dict>
|
||||
|
56
lib/pods/event_calendar.dart
Normal file
56
lib/pods/event_calendar.dart
Normal file
@ -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<List<SnEventCalendarEntry>> 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<SnEventCalendarEntry>()
|
||||
.toList();
|
||||
}
|
@ -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<AsyncValue<List<SnEventCalendarEntry>>> {
|
||||
/// 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<List<SnEventCalendarEntry>> {
|
||||
/// 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<List<SnEventCalendarEntry>> Function(
|
||||
AccountEventCalendarRef provider,
|
||||
)
|
||||
FutureOr<List<SnEventCalendarEntry>> 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<List<SnEventCalendarEntry>> 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<List<SnEventCalendarEntry>> {
|
||||
/// The parameter `query` of this provider.
|
||||
EventCalendarQuery get query;
|
||||
}
|
||||
|
||||
class _AccountEventCalendarProviderElement
|
||||
class _EventCalendarProviderElement
|
||||
extends AutoDisposeFutureProviderElement<List<SnEventCalendarEntry>>
|
||||
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
|
@ -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<List<SnEventCalendarEntry>> 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<SnEventCalendarEntry>()
|
||||
.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<SnCheckInResult>().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}'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -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>(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<EventCalendarQuery> get copyWith => _$EventCalendarQueryCopyWithImpl<EventCalendarQuery>(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
|
@ -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),
|
||||
);
|
||||
|
@ -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(
|
||||
|
193
lib/widgets/account/event_calendar.dart
Normal file
193
lib/widgets/account/event_calendar.dart
Normal file
@ -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<List<SnEventCalendarEntry>> 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<SnCheckInResult>().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;
|
||||
}
|
||||
}
|
258
lib/widgets/account/fortune_graph.dart
Normal file
258
lib/widgets/account/fortune_graph.dart
Normal file
@ -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<List<SnEventCalendarEntry>> 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<SnEventCalendarEntry>()
|
||||
// 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
|
||||
}
|
||||
}
|
@ -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<SnCloudFile> files;
|
||||
|
@ -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;
|
||||
|
@ -18,5 +18,7 @@
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.device-information.user-assigned-device-name</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -16,5 +16,7 @@
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.device-information.user-assigned-device-name</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
10
pubspec.lock
10
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"
|
||||
|
@ -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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user