Skip to content

Commit 930ccab

Browse files
Open next week once there are no more school days for this week anymore (#2131)
Closes #1702
1 parent 26a9ddf commit 930ccab

22 files changed

Lines changed: 258 additions & 21 deletions

app/lib/navigation/models/navigation_item.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ extension NavigationItemExtension on NavigationItem {
192192
case NavigationItem.homework:
193193
return const HomeworkPage();
194194
case NavigationItem.timetable:
195-
return TimetablePage();
195+
return const TimetablePage();
196196
case NavigationItem.blackboard:
197197
return const BlackboardPage();
198198
case NavigationItem.grades:

app/lib/settings/src/subpages/timetable/timetable_settings_page.dart

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ class TimetableSettingsPage extends StatelessWidget {
5858
const Divider(),
5959
_TimetableEnabledWeekDaysField(),
6060
const Divider(),
61+
_OpenUpcomingWeekOnNonSchoolDays(),
62+
const Divider(),
6163
const _ICalLinks(),
6264
// We only show the time picker settings on iOS because on
6365
// other platforms we use the different time picker where we
@@ -240,6 +242,38 @@ class _TimetableEnabledWeekDaysField extends StatelessWidget {
240242
}
241243
}
242244

245+
class _OpenUpcomingWeekOnNonSchoolDays extends StatelessWidget {
246+
@override
247+
Widget build(BuildContext context) {
248+
final bloc = BlocProvider.of<UserSettingsBloc>(context);
249+
return StreamBuilder<UserSettings>(
250+
stream: bloc.streamUserSettings(),
251+
builder: (context, snapshot) {
252+
if (!snapshot.hasData) return const SizedBox.shrink();
253+
final userSettings = snapshot.data!;
254+
return SwitchListTile.adaptive(
255+
title: Text(
256+
context.l10n.timetableSettingsOpenUpcomingWeekOnNonSchoolDaysTitle,
257+
),
258+
subtitle: Text(
259+
context
260+
.l10n
261+
.timetableSettingsOpenUpcomingWeekOnNonSchoolDaysSubtitle,
262+
),
263+
value: userSettings.openUpcomingWeekOnNonSchoolDays,
264+
onChanged: (newValue) {
265+
bloc.updateSettings(
266+
userSettings.copyWith(
267+
openUpcomingWeekOnNonSchoolDays: newValue,
268+
),
269+
);
270+
},
271+
);
272+
},
273+
);
274+
}
275+
}
276+
243277
class LessonsLengthField extends StatelessWidget {
244278
const LessonsLengthField({
245279
super.key,

app/lib/timetable/src/bloc/timetable_bloc.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,10 @@ class TimetableConfig {
310310
return _getUserSettings().showAbbreviation;
311311
}
312312

313+
bool openUpcomingWeekOnNonSchoolDays() {
314+
return _getUserSettings().openUpcomingWeekOnNonSchoolDays;
315+
}
316+
313317
Time getTimetableStartTime() {
314318
return _getUserSettings().timetableStartTime;
315319
}

app/lib/timetable/src/logic/timetable_date_helper.dart

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@
66
//
77
// SPDX-License-Identifier: EUPL-1.2
88

9+
import 'dart:math';
10+
911
import 'package:date/date.dart';
1012
import 'package:user/user.dart';
1113

1214
class TimetableDateHelper {
13-
static Date dateBeginThisWeek() {
14-
Date today = Date.today();
15-
Date startOfThisWeek = today.addDays(-(today.weekDay - 1));
15+
static Date dateBeginThisWeek({Date? today}) {
16+
final currentDate = today ?? Date.today();
17+
Date startOfThisWeek = currentDate.addDays(-(currentDate.weekDay - 1));
1618
return startOfThisWeek;
1719
}
1820

@@ -24,6 +26,28 @@ class TimetableDateHelper {
2426
return dateAddDays(date, weeks * 7);
2527
}
2628

29+
static bool shouldOpenUpcomingWeek({
30+
required Date today,
31+
required EnabledWeekDays enabledWeekDays,
32+
required bool isFeatureEnabled,
33+
required Iterable<Date> eventDatesInCurrentWeek,
34+
}) {
35+
if (!isFeatureEnabled) return false;
36+
final enabledDays = enabledWeekDays.getEnabledWeekDaysList();
37+
if (enabledDays.isEmpty) return false;
38+
final lastEnabledIndex = enabledDays.map((day) => day.index).reduce(max);
39+
if (today.weekDayEnum.index <= lastEnabledIndex) return false;
40+
final hasUpcomingEventOnNonEnabledDay = eventDatesInCurrentWeek.any((
41+
eventDate,
42+
) {
43+
final isEnabledDay =
44+
enabledWeekDays.getValue(eventDate.weekDayEnum) ?? false;
45+
if (isEnabledDay) return false;
46+
return !eventDate.isBefore(today);
47+
});
48+
return !hasUpcomingEventOnNonEnabledDay;
49+
}
50+
2751
// THERE ARE MORE OPTIMAL CALCULATION METHODES, BUT I LIKE THIS DESIGN :)
2852
static List<Date> generateDaysList(
2953
Date startDate,

app/lib/timetable/timetable_page/timetable_page.dart

Lines changed: 72 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
//
77
// SPDX-License-Identifier: EUPL-1.2
88

9+
import 'dart:async';
910
import 'dart:math';
1011

1112
import 'package:bloc_provider/bloc_provider.dart';
13+
import 'package:date/date.dart';
1214
import 'package:flutter/cupertino.dart';
1315
import 'package:flutter/foundation.dart';
1416
import 'package:flutter/gestures.dart';
@@ -49,10 +51,9 @@ class TimetablePage extends StatelessWidget {
4951
static const tag = "timetable-page";
5052
static const days = 5;
5153

52-
final today = TimetableDateHelper.dateBeginThisWeek();
5354
static const timetableScale = 1.00;
5455

55-
TimetablePage({super.key});
56+
const TimetablePage({super.key});
5657

5758
String getAdUnitId(BuildContext context) {
5859
if (kDebugMode) {
@@ -143,8 +144,8 @@ class _SettingsIcon extends StatelessWidget {
143144
}
144145
}
145146

146-
class TimeTableUnit extends StatelessWidget {
147-
TimeTableUnit({
147+
class TimeTableUnit extends StatefulWidget {
148+
const TimeTableUnit({
148149
super.key,
149150
required this.groupInfos,
150151
required this.lessons,
@@ -155,12 +156,60 @@ class TimeTableUnit extends StatelessWidget {
155156
final List<Lesson> lessons;
156157
final TimetableConfig timetableConfig;
157158

158-
final today = TimetableDateHelper.dateBeginThisWeek();
159+
@override
160+
State<TimeTableUnit> createState() => _TimeTableUnitState();
161+
}
162+
163+
class _TimeTableUnitState extends State<TimeTableUnit> {
164+
late final Date _today = Date.today();
165+
late final Date _startOfCurrentWeek = TimetableDateHelper.dateBeginThisWeek(
166+
today: _today,
167+
);
168+
late final Date _endOfCurrentWeek = TimetableDateHelper.dateAddDays(
169+
_startOfCurrentWeek,
170+
6,
171+
);
172+
final PageController _pageController = PageController();
173+
late StreamSubscription<List<CalendricalEvent>>? _initialWeekSub;
174+
175+
void _maybeJumpToUpcomingWeek(Iterable<CalendricalEvent> events) {
176+
final shouldOpenUpcomingWeek = TimetableDateHelper.shouldOpenUpcomingWeek(
177+
today: _today,
178+
enabledWeekDays: widget.timetableConfig.getEnabledWeekDays(),
179+
isFeatureEnabled:
180+
widget.timetableConfig.openUpcomingWeekOnNonSchoolDays(),
181+
eventDatesInCurrentWeek: events.map((e) => e.date),
182+
);
183+
184+
if (!shouldOpenUpcomingWeek || !mounted) return;
185+
_pageController.jumpToPage(1);
186+
}
159187

160188
@override
161-
Widget build(BuildContext context) {
162-
final bloc = BlocProvider.of<TimetableBloc>(context);
189+
void initState() {
190+
super.initState();
191+
192+
WidgetsBinding.instance.addPostFrameCallback((_) {
193+
if (!mounted) return;
163194

195+
final bloc = context.read<TimetableBloc>();
196+
_initialWeekSub = bloc
197+
.events(_startOfCurrentWeek, endDate: _endOfCurrentWeek)
198+
.where((events) => events.isNotEmpty || true)
199+
.take(1)
200+
.listen(_maybeJumpToUpcomingWeek);
201+
});
202+
}
203+
204+
@override
205+
void dispose() {
206+
_initialWeekSub?.cancel();
207+
_pageController.dispose();
208+
super.dispose();
209+
}
210+
211+
@override
212+
Widget build(BuildContext context) {
164213
return PageView.builder(
165214
// Users expect to be able to drag the timetable horizontally to the
166215
// next/previous week via mouse as this is old behavior.
@@ -170,25 +219,31 @@ class TimeTableUnit extends StatelessWidget {
170219
// workaround to explicitly re-enable this behavior. See:
171220
// https://docs.flutter.dev/release/breaking-changes/default-scroll-behavior-drag
172221
//
173-
// In the future we might add explicit buttons to go forward or backwards
174-
// a week in the timetable - in this case desktop might remove this
175-
// behavior.
222+
// In the future we might add explicit buttons to go forward or
223+
// backwards a week in the timetable - in this case desktop might
224+
// remove this behavior.
176225
scrollBehavior: ScrollConfiguration.of(context).copyWith(
177226
dragDevices: {PointerDeviceKind.touch, PointerDeviceKind.mouse},
178227
),
228+
controller: _pageController,
179229
itemBuilder: (context, index) {
180-
final startOfWeek = TimetableDateHelper.dateAddWeeks(today, index);
230+
final startOfWeek = TimetableDateHelper.dateAddWeeks(
231+
_startOfCurrentWeek,
232+
index,
233+
);
181234
final endDate = TimetableDateHelper.dateAddDays(startOfWeek, 6);
182235
final daysList = TimetableDateHelper.generateDaysList(
183236
startOfWeek,
184237
endDate,
185-
timetableConfig.getEnabledWeekDays(),
238+
widget.timetableConfig.getEnabledWeekDays(),
186239
);
187240

188241
final filteredLessonsList = getFilteredLessonList(
189-
lessons,
190-
timetableConfig.getWeekType(daysList.first),
242+
widget.lessons,
243+
widget.timetableConfig.getWeekType(daysList.first),
191244
);
245+
246+
final bloc = BlocProvider.of<TimetableBloc>(context);
192247
return StreamBuilder<List<CalendricalEvent>>(
193248
stream: bloc.events(startOfWeek, endDate: endDate),
194249
builder: (context, snapshot) {
@@ -197,13 +252,13 @@ class TimeTableUnit extends StatelessWidget {
197252
filteredLessonsList,
198253
daysList,
199254
events,
200-
groupInfos,
255+
widget.groupInfos,
201256
);
202257
return TimetableWeekView(
203258
dates: daysList,
204259
elements: [...builder.buildElements()],
205-
config: timetableConfig,
206-
periods: timetableConfig.getPeriods(),
260+
config: widget.timetableConfig,
261+
periods: widget.timetableConfig.getPeriods(),
207262
);
208263
},
209264
);
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright (c) 2024 Sharezone UG (haftungsbeschränkt)
2+
// Licensed under the EUPL-1.2-or-later.
3+
//
4+
// You may obtain a copy of the Licence at:
5+
// https://joinup.ec.europa.eu/software/page/eupl
6+
//
7+
// SPDX-License-Identifier: EUPL-1.2
8+
9+
import 'package:date/date.dart';
10+
import 'package:date/weekday.dart';
11+
import 'package:flutter_test/flutter_test.dart';
12+
import 'package:sharezone/timetable/src/logic/timetable_date_helper.dart';
13+
import 'package:user/user.dart';
14+
15+
void main() {
16+
group(TimetableDateHelper.shouldOpenUpcomingWeek, () {
17+
test(
18+
'opens upcoming week after last enabled weekday when feature is enabled',
19+
() {
20+
final today = Date.parse('2024-08-10'); // Saturday
21+
final result = TimetableDateHelper.shouldOpenUpcomingWeek(
22+
today: today,
23+
enabledWeekDays: EnabledWeekDays.standard,
24+
isFeatureEnabled: true,
25+
eventDatesInCurrentWeek: <Date>[],
26+
);
27+
28+
expect(result, isTrue);
29+
},
30+
);
31+
32+
test('does not open upcoming week when feature is disabled', () {
33+
final today = Date.parse('2024-08-10'); // Saturday
34+
final result = TimetableDateHelper.shouldOpenUpcomingWeek(
35+
today: today,
36+
enabledWeekDays: EnabledWeekDays.standard,
37+
isFeatureEnabled: false,
38+
eventDatesInCurrentWeek: <Date>[],
39+
);
40+
41+
expect(result, isFalse);
42+
});
43+
44+
test('does not open upcoming week when today is an enabled weekday', () {
45+
final today = Date.parse('2024-08-10'); // Saturday
46+
final enabledWeekDays = EnabledWeekDays.fromEnabledWeekDaysList([
47+
WeekDay.monday,
48+
WeekDay.tuesday,
49+
WeekDay.wednesday,
50+
WeekDay.thursday,
51+
WeekDay.friday,
52+
WeekDay.saturday,
53+
]);
54+
final result = TimetableDateHelper.shouldOpenUpcomingWeek(
55+
today: today,
56+
enabledWeekDays: enabledWeekDays,
57+
isFeatureEnabled: true,
58+
eventDatesInCurrentWeek: <Date>[],
59+
);
60+
61+
expect(result, isFalse);
62+
});
63+
64+
test('does not open upcoming week when there is an upcoming event', () {
65+
final today = Date.parse('2024-08-10'); // Saturday
66+
final result = TimetableDateHelper.shouldOpenUpcomingWeek(
67+
today: today,
68+
enabledWeekDays: EnabledWeekDays.standard,
69+
isFeatureEnabled: true,
70+
eventDatesInCurrentWeek: [Date.parse('2024-08-10')],
71+
);
72+
73+
expect(result, isFalse);
74+
});
75+
});
76+
}
19.4 KB
Loading
18 KB
Loading
550 Bytes
Loading
16.6 KB
Loading

0 commit comments

Comments
 (0)