Skip to content

Commit d4d46b8

Browse files
committed
refactor: manage calendar selected day state with Riverpod and optimize event display and range updates.
1 parent 561d296 commit d4d46b8

File tree

8 files changed

+215
-134
lines changed

8 files changed

+215
-134
lines changed

lib/i18n/en-US.i18n.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ nav:
5050
portal: Portals
5151
calendar: Calendar
5252
profile: Me
53+
calendar:
54+
month: Month
5355
courseTable:
5456
notFound: Course table not found
5557
dayOfWeek(map):

lib/i18n/strings.g.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
/// To regenerate, run: `dart run slang`
55
///
66
/// Locales: 2
7-
/// Strings: 210 (105 per locale)
7+
/// Strings: 212 (106 per locale)
88
99
// coverage:ignore-file
1010
// ignore_for_file: type=lint, unused_import

lib/i18n/strings_en_US.g.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ class TranslationsEnUs extends Translations with BaseTranslations<AppLocale, Tra
4444
@override late final _TranslationsIntroEnUs intro = _TranslationsIntroEnUs._(_root);
4545
@override late final _TranslationsLoginEnUs login = _TranslationsLoginEnUs._(_root);
4646
@override late final _TranslationsNavEnUs nav = _TranslationsNavEnUs._(_root);
47+
@override late final _TranslationsCalendarEnUs calendar = _TranslationsCalendarEnUs._(_root);
4748
@override late final _TranslationsCourseTableEnUs courseTable = _TranslationsCourseTableEnUs._(_root);
4849
@override late final _TranslationsProfileEnUs profile = _TranslationsProfileEnUs._(_root);
4950
@override late final _TranslationsEnrollmentStatusEnUs enrollmentStatus = _TranslationsEnrollmentStatusEnUs._(_root);
@@ -134,6 +135,16 @@ class _TranslationsNavEnUs extends TranslationsNavZhTw {
134135
@override String get profile => 'Me';
135136
}
136137

138+
// Path: calendar
139+
class _TranslationsCalendarEnUs extends TranslationsCalendarZhTw {
140+
_TranslationsCalendarEnUs._(TranslationsEnUs root) : this._root = root, super.internal(root);
141+
142+
final TranslationsEnUs _root; // ignore: unused_field
143+
144+
// Translations
145+
@override String get month => 'Month';
146+
}
147+
137148
// Path: courseTable
138149
class _TranslationsCourseTableEnUs extends TranslationsCourseTableZhTw {
139150
_TranslationsCourseTableEnUs._(TranslationsEnUs root) : this._root = root, super.internal(root);
@@ -398,6 +409,7 @@ extension on TranslationsEnUs {
398409
'nav.portal' => 'Portals',
399410
'nav.calendar' => 'Calendar',
400411
'nav.profile' => 'Me',
412+
'calendar.month' => 'Month',
401413
'courseTable.notFound' => 'Course table not found',
402414
'courseTable.dayOfWeek.sunday' => 'Sun',
403415
'courseTable.dayOfWeek.monday' => 'Mon',

lib/i18n/strings_zh_TW.g.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class Translations with BaseTranslations<AppLocale, Translations> {
4545
late final TranslationsIntroZhTw intro = TranslationsIntroZhTw.internal(_root);
4646
late final TranslationsLoginZhTw login = TranslationsLoginZhTw.internal(_root);
4747
late final TranslationsNavZhTw nav = TranslationsNavZhTw.internal(_root);
48+
late final TranslationsCalendarZhTw calendar = TranslationsCalendarZhTw.internal(_root);
4849
late final TranslationsCourseTableZhTw courseTable = TranslationsCourseTableZhTw.internal(_root);
4950
late final TranslationsProfileZhTw profile = TranslationsProfileZhTw.internal(_root);
5051
late final TranslationsEnrollmentStatusZhTw enrollmentStatus = TranslationsEnrollmentStatusZhTw.internal(_root);
@@ -193,6 +194,18 @@ class TranslationsNavZhTw {
193194
String get profile => '我';
194195
}
195196

197+
// Path: calendar
198+
class TranslationsCalendarZhTw {
199+
TranslationsCalendarZhTw.internal(this._root);
200+
201+
final Translations _root; // ignore: unused_field
202+
203+
// Translations
204+
205+
/// zh-TW: '月'
206+
String get month => '月';
207+
}
208+
196209
// Path: courseTable
197210
class TranslationsCourseTableZhTw {
198211
TranslationsCourseTableZhTw.internal(this._root);
@@ -585,6 +598,7 @@ extension on Translations {
585598
'nav.portal' => '傳送門',
586599
'nav.calendar' => '行事曆',
587600
'nav.profile' => '我',
601+
'calendar.month' => '月',
588602
'courseTable.notFound' => '找不到課表',
589603
'courseTable.dayOfWeek.sunday' => '日',
590604
'courseTable.dayOfWeek.monday' => '一',

lib/i18n/zh-TW.i18n.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ nav:
5050
portal: 傳送門
5151
calendar: 行事曆
5252
profile:
53+
calendar:
54+
month:
5355
courseTable:
5456
notFound: 找不到課表
5557
dayOfWeek(map):

lib/screens/main/calendar/calendar_providers.dart

Lines changed: 117 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -3,63 +3,133 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
33
import 'package:tattoo/database/database.dart';
44
import 'package:tattoo/repositories/calendar_repository.dart';
55

6-
class CalendarFocusedDayNotifier extends Notifier<DateTime> {
6+
// ---------------------------------------------------------------------------
7+
// Shared notifier — used for both focused-day and selected-day providers.
8+
// ---------------------------------------------------------------------------
9+
10+
class DateTimeNotifier extends Notifier<DateTime> {
711
@override
812
DateTime build() => DateTime.now();
913
void updateDate(DateTime day) => state = day;
1014
}
1115

12-
final calendarFocusedDayProvider = NotifierProvider<CalendarFocusedDayNotifier, DateTime>(
13-
CalendarFocusedDayNotifier.new,
16+
final calendarFocusedDayProvider = NotifierProvider<DateTimeNotifier, DateTime>(
17+
DateTimeNotifier.new,
1418
);
1519

20+
final calendarSelectedDayProvider =
21+
NotifierProvider<DateTimeNotifier, DateTime>(DateTimeNotifier.new);
22+
23+
// ---------------------------------------------------------------------------
24+
// Calendar event display helpers
25+
// ---------------------------------------------------------------------------
26+
27+
extension CalendarEventX on CalendarEvent {
28+
/// The inclusive last day/time of this event for display and bucketing.
29+
///
30+
/// For all-day events the portal stores `end` as the exclusive next-day
31+
/// midnight (e.g. an event on Jan 5 has end = Jan 6 00:00:00). We subtract
32+
/// 1 ms so that bucketing treats Jan 5 as the last day, not Jan 6.
33+
///
34+
/// For timed events the raw `end` is used as-is.
35+
///
36+
/// Note: the DB overlap query in CalendarRepository._eventsOverlapping uses
37+
/// the raw `end` column, so boundary-events may appear in a wider DB slice
38+
/// than the bucketing below. This intentional mismatch is safe — the display
39+
/// layer only places events in the correct day buckets.
40+
///
41+
/// Precondition: callers in [calendarEventsProvider] guard `event.start != null`
42+
/// before calling this getter.
43+
DateTime get displayEndDate {
44+
final e = end;
45+
if (e == null) return start!; // start is non-null at this call site
46+
47+
if (allDay) {
48+
// All-day end is the exclusive next-day midnight; subtract 1 ms to keep
49+
// it within the correct day.
50+
final startDay = DateTime(start!.year, start!.month, start!.day);
51+
final endDay = DateTime(e.year, e.month, e.day);
52+
return endDay.isAfter(startDay)
53+
? endDay.subtract(const Duration(milliseconds: 1))
54+
: startDay;
55+
}
56+
57+
return e;
58+
}
59+
}
60+
61+
// ---------------------------------------------------------------------------
62+
// Range notifier — tracks the ±1-month window around the focused month.
63+
// ---------------------------------------------------------------------------
64+
65+
/// Returns a [DateTimeRange] spanning from the start of the month before
66+
/// [focus] to the end of the month two months after [focus].
67+
///
68+
/// Uses Dart's normalising DateTime constructor (day 0 = last day of the
69+
/// previous month) to keep the arithmetic simple and correct across year
70+
/// boundaries (e.g. month - 1 when month == 1 yields December of the
71+
/// previous year).
72+
DateTimeRange threeMonthWindow(DateTime focus) {
73+
return DateTimeRange(
74+
start: DateTime(focus.year, focus.month - 1, 1),
75+
end: DateTime(focus.year, focus.month + 2, 0),
76+
);
77+
}
78+
1679
class CalendarRangeNotifier extends Notifier<DateTimeRange> {
1780
@override
18-
DateTimeRange build() {
19-
final now = DateTime.now();
20-
return DateTimeRange(
21-
start: DateTime(now.year, now.month - 1, 1),
22-
end: DateTime(now.year, now.month + 2, 0),
23-
);
24-
}
81+
DateTimeRange build() => threeMonthWindow(DateTime.now());
2582
void updateRange(DateTimeRange range) => state = range;
2683
}
2784

28-
final calendarRangeProvider = NotifierProvider<CalendarRangeNotifier, DateTimeRange>(
29-
CalendarRangeNotifier.new,
30-
);
85+
final calendarRangeProvider =
86+
NotifierProvider<CalendarRangeNotifier, DateTimeRange>(
87+
CalendarRangeNotifier.new,
88+
);
89+
90+
// ---------------------------------------------------------------------------
91+
// Events provider — maps normalized date integers to event lists.
92+
// ---------------------------------------------------------------------------
93+
94+
/// Encodes a calendar date as a compact integer key to avoid the pitfall of
95+
/// using [DateTime] objects (which include a time component) as map keys.
96+
int dateKey(int year, int month, int day) => year * 10000 + month * 100 + day;
97+
98+
final calendarEventsProvider = FutureProvider<Map<int, List<CalendarEvent>>>((
99+
ref,
100+
) async {
101+
final range = ref.watch(calendarRangeProvider);
102+
final repo = ref.watch(calendarRepositoryProvider);
31103

32-
final calendarEventsProvider =
33-
FutureProvider<Map<DateTime, List<CalendarEvent>>>((ref) async {
34-
final range = ref.watch(calendarRangeProvider);
35-
final repo = ref.watch(calendarRepositoryProvider);
36-
37-
final events = await repo.getCalendar(
38-
startDate: range.start,
39-
endDate: range.end,
40-
);
41-
42-
final map = <DateTime, List<CalendarEvent>>{};
43-
for (final event in events) {
44-
if (event.start == null || event.end == null) continue;
45-
var current = DateTime(
46-
event.start!.year,
47-
event.start!.month,
48-
event.start!.day,
49-
);
50-
// NTUT sets the `end` epoch to exactly 00:00 on the day *after* the event finishes.
51-
// We subtract 1 millisecond so that 'last' falls on the true final day.
52-
final adjustedEnd = event.end!.subtract(const Duration(milliseconds: 1));
53-
54-
// If start and end are exactly the same (a zero-duration event at midnight),
55-
// fallback to current to ensure it gets added for at least one day.
56-
final lastDay = adjustedEnd.isBefore(current) ? current : adjustedEnd;
57-
58-
final last = DateTime(lastDay.year, lastDay.month, lastDay.day);
59-
while (!current.isAfter(last)) {
60-
map.putIfAbsent(current, () => []).add(event);
61-
current = current.add(const Duration(days: 1));
62-
}
63-
}
64-
return map;
65-
});
104+
final events = await repo.getCalendar(
105+
startDate: range.start,
106+
endDate: range.end,
107+
);
108+
109+
final map = <int, List<CalendarEvent>>{};
110+
for (final event in events) {
111+
if (event.start == null) continue;
112+
113+
var current = DateTime(
114+
event.start!.year,
115+
event.start!.month,
116+
event.start!.day,
117+
);
118+
final adjustedEnd = event.displayEndDate;
119+
120+
// If start and end coincide (zero-duration event), emit for exactly one day.
121+
final lastDay = adjustedEnd.isBefore(current) ? current : adjustedEnd;
122+
final last = DateTime(lastDay.year, lastDay.month, lastDay.day);
123+
124+
while (!current.isAfter(last)) {
125+
map
126+
.putIfAbsent(
127+
dateKey(current.year, current.month, current.day),
128+
() => [],
129+
)
130+
.add(event);
131+
current = DateTime(current.year, current.month, current.day + 1);
132+
}
133+
}
134+
return map;
135+
});

0 commit comments

Comments
 (0)