Restored full timetable implementation using SFCalendar

This commit is contained in:
Elias Müller 2023-05-20 16:30:06 +02:00
parent ba53da1b14
commit 9a1247de5f
9 changed files with 515 additions and 312 deletions

35
.idea/libraries/Flutter_Plugins.xml generated Normal file
View File

@ -0,0 +1,35 @@
<component name="libraryTable">
<library name="Flutter Plugins" type="FlutterPluginsLibraryType">
<CLASSES>
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/url_launcher_web-2.0.16" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/image_picker_for_web-2.1.12" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/url_launcher_android-6.0.34" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.2.2" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/url_launcher_macos-3.0.5" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/better_open_file-3.6.4" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/package_info-2.0.2" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/url_launcher-6.1.11" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.2.0" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/path_provider_foundation-2.2.3" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/shared_preferences_android-2.1.4" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/image_picker_android-0.8.6+16" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/path_provider-2.0.15" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/image_picker-0.8.7+5" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/shared_preferences_web-2.1.0" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/url_launcher_linux-3.0.5" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.2.0" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/url_launcher_windows-3.0.6" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/path_provider_android-2.0.27" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/url_launcher_ios-6.1.4" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/flutter_native_splash-2.3.0" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/path_provider_linux-2.1.10" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/sqflite-2.2.8+4" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/shared_preferences-2.1.1" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/image_picker_ios-0.8.7+4" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/file_picker-5.3.0" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/path_provider_windows-2.1.6" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>

View File

@ -14,7 +14,7 @@ import '../../api/webuntis/queries/getTimetable/getTimetableResponse.dart';
import '../../api/webuntis/webuntisError.dart'; import '../../api/webuntis/webuntisError.dart';
extension DateTimeExtension on DateTime { extension DateTimeExtension on DateTime {
DateTime next(int day) { DateTime jumpToNextWeekDay(int day) {
return add( return add(
Duration( Duration(
days: (day - weekday) % DateTime.daysPerWeek, days: (day - weekday) % DateTime.daysPerWeek,
@ -24,7 +24,7 @@ extension DateTimeExtension on DateTime {
} }
class TimetableProps extends DataHolder { class TimetableProps extends DataHolder {
var _queryWeek = DateTime.now(); final _queryWeek = DateTime.now().add(const Duration(days: 2));
late DateTime startDate = getDate(_queryWeek.subtract(Duration(days: _queryWeek.weekday - 1))); late DateTime startDate = getDate(_queryWeek.subtract(Duration(days: _queryWeek.weekday - 1)));
late DateTime endDate = getDate(_queryWeek.add(Duration(days: DateTime.daysPerWeek - _queryWeek.weekday))); late DateTime endDate = getDate(_queryWeek.add(Duration(days: DateTime.daysPerWeek - _queryWeek.weekday)));
@ -87,33 +87,18 @@ class TimetableProps extends DataHolder {
); );
} }
void nearest() {
_queryWeek = _queryWeek = DateTime.now();
if(_queryWeek.weekday == DateTime.saturday || _queryWeek.weekday == DateTime.sunday) _queryWeek = _queryWeek.add(const Duration(days: 2));
updateWeek();
}
void switchWeek({previous = false}) {
if(previous) {
_queryWeek = _queryWeek.subtract(const Duration(days: 7));
} else {
_queryWeek = _queryWeek.add(const Duration(days: 7));
}
updateWeek();
}
DateTime getDate(DateTime d) => DateTime(d.year, d.month, d.day); DateTime getDate(DateTime d) => DateTime(d.year, d.month, d.day);
bool isWeekend(DateTime queryDate) { bool isWeekend(DateTime queryDate) {
return queryDate.weekday == DateTime.saturday || queryDate.weekday == DateTime.sunday; return queryDate.weekday == DateTime.saturday || queryDate.weekday == DateTime.sunday;
} }
void updateWeek() { void updateWeek(DateTime start, DateTime end) {
properties().forEach((element) => element = null); properties().forEach((element) => element = null);
error = null; error = null;
notifyListeners(); notifyListeners();
startDate = getDate(_queryWeek.subtract(Duration(days: _queryWeek.weekday - 1))); startDate = start.subtract(const Duration(days: 7));
endDate = getDate(_queryWeek.add(Duration(days: DateTime.daysPerWeek - _queryWeek.weekday))); endDate = end.add(const Duration(days: 7));
try { try {
run(); run();
} on WebuntisError catch(e) { } on WebuntisError catch(e) {

View File

@ -8,6 +8,7 @@ import 'package:marianum_mobile/screen/login/login.dart';
import 'package:marianum_mobile/widget/errorView.dart'; import 'package:marianum_mobile/widget/errorView.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'app.dart'; import 'app.dart';
import 'data/chatList/chatListProps.dart'; import 'data/chatList/chatListProps.dart';
@ -71,6 +72,16 @@ class _MainState extends State<Main> {
return MaterialApp( return MaterialApp(
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
localizationsDelegates: const [
...GlobalMaterialLocalizations.delegates,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: const [
Locale('de'),
Locale('en'),
],
locale: const Locale('de'),
title: 'Marianum Fulda', title: 'Marianum Fulda',
theme: ThemeData( theme: ThemeData(
@ -79,7 +90,7 @@ class _MainState extends State<Main> {
colorScheme: const ColorScheme( colorScheme: const ColorScheme(
brightness: Brightness.light, brightness: Brightness.light,
surface: Colors.white, surface: Colors.white,
onSurface: Colors.white, onSurface: Colors.black,
onSecondary: Colors.white, onSecondary: Colors.white,
onPrimary: Colors.white, onPrimary: Colors.white,
onError: marianumRed, onError: marianumRed,
@ -91,7 +102,7 @@ class _MainState extends State<Main> {
), ),
hintColor: marianumRed, hintColor: marianumRed,
inputDecorationTheme: const InputDecorationTheme( inputDecorationTheme: const InputDecorationTheme(
border: UnderlineInputBorder(borderSide: BorderSide(color: marianumRed)), border: UnderlineInputBorder(borderSide: BorderSide(color: marianumRed)),
), ),
appBarTheme: const AppBarTheme( appBarTheme: const AppBarTheme(
backgroundColor: marianumRed, backgroundColor: marianumRed,

View File

@ -0,0 +1,112 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_calendar/calendar.dart';
class AppointmentComponent extends StatefulWidget {
final CalendarAppointmentDetails details;
const AppointmentComponent({Key? key, required this.details}) : super(key: key);
@override
State<AppointmentComponent> createState() => _AppointmentComponentState();
}
class _AppointmentComponentState extends State<AppointmentComponent> {
@override
Widget build(BuildContext context) {
final Appointment meeting = widget.details.appointments.first;
final appointmentHeight = widget.details.bounds.height;
double headerHeight = 50;
const double footerHeight = 5;
final double infoHeight = appointmentHeight - (headerHeight + footerHeight);
if(infoHeight < 0) headerHeight += infoHeight;
return Column(
children: [
Container(
padding: const EdgeInsets.all(3),
height: headerHeight,
alignment: Alignment.topLeft,
decoration: BoxDecoration(
shape: BoxShape.rectangle,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(5),
topRight: Radius.circular(5)),
color: meeting.color,
),
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FittedBox(
fit: BoxFit.fitWidth,
child: Text(
meeting.subject,
style: const TextStyle(
color: Colors.white,
fontSize: 15,
fontWeight: FontWeight.w500,
),
maxLines: 1,
softWrap: false,
),
),
FittedBox(
fit: BoxFit.fitWidth,
child: Text(
meeting.location ?? "?",
maxLines: 3,
overflow: TextOverflow.ellipsis,
softWrap: true,
style: const TextStyle(
color: Colors.white,
fontSize: 10,
),
),
)
],
)),
),
Visibility(
visible: meeting.notes != null && infoHeight > 10,
replacement: Container(
color: meeting.color,
height: infoHeight,
),
child: Container(
height: infoHeight,
padding: const EdgeInsets.fromLTRB(3, 5, 3, 2),
color: meeting.color.withOpacity(0.8),
alignment: Alignment.topLeft,
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
meeting.notes ?? "",
style: const TextStyle(
color: Colors.white,
fontSize: 10,
),
)
],
)),
),
),
Container(
height: footerHeight,
decoration: BoxDecoration(
shape: BoxShape.rectangle,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(5),
bottomRight: Radius.circular(5)),
color: meeting.color,
),
),
],
);
}
}

View File

@ -0,0 +1,90 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import 'package:marianum_mobile/api/webuntis/queries/getTimetable/getTimetableResponse.dart';
import 'package:marianum_mobile/data/timetable/timetableProps.dart';
import 'package:persistent_bottom_nav_bar/persistent_tab_view.dart';
import 'package:syncfusion_flutter_calendar/calendar.dart';
import '../../../api/webuntis/queries/getRooms/getRoomsResponse.dart';
import '../../../api/webuntis/queries/getSubjects/getSubjectsResponse.dart';
import '../../settings/debug/jsonViewer.dart';
import '../more/roomplan/roomplan.dart';
class AppointmentDetails {
static String _getEventPrefix(String? code) {
if(code == "cancelled") return "Entfällt: ";
if(code == "irregular") return "Änderung: ";
return code ?? "";
}
static void show(BuildContext context, TimetableProps webuntisData, Appointment appointment) {
GetTimetableResponseObject timetableData = appointment.id as GetTimetableResponseObject;
//GetTimetableResponseObject timetableData = webuntisData.getTimetableResponse.result.firstWhere((element) => element.id == timetableObject.id);
GetSubjectsResponseObject subject = webuntisData.getSubjectsResponse.result.firstWhere((subject) => subject.id == timetableData.su[0]['id']);
GetRoomsResponseObject room = webuntisData.getRoomsResponse.result.firstWhere((room) => room.id == timetableData.ro[0]['id']);
showModalBottomSheet(context: context, builder: (context) => Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 30),
child: Center(
child: Column(
children: [
Icon(Icons.info, color: appointment.color),
const SizedBox(height: 10),
Text("${_getEventPrefix(timetableData.code)}${subject.alternateName} - (${subject.longName})", style: const TextStyle(fontSize: 30)),
Text("${Jiffy(appointment.startTime).format("HH:mm")} - ${Jiffy(appointment.endTime).format("HH:mm")}", style: const TextStyle(fontSize: 15)),
],
),
),
),
Expanded(
child: ListView(
children: [
ListTile(
leading: const Icon(Icons.notifications_active),
title: Text("Status: ${timetableData.code != null ? "Geändert" : "Regulär"}"),
),
ListTile(
leading: const Icon(Icons.room),
title: Text("Raum: ${room.name}"),
trailing: IconButton(
icon: const Icon(Icons.house_outlined),
onPressed: () {
PersistentNavBarNavigator.pushNewScreen(context, withNavBar: false, screen: const Roomplan());
},
),
),
ListTile(
leading: const Icon(Icons.person),
title: Text("Lehrkraft: (${timetableData.te[0]['name']}) ${timetableData.te[0]['longname']}"),
trailing: IconButton(
icon: const Icon(Icons.textsms_outlined),
onPressed: () {
showDialog(context: context, builder: (context) => const AlertDialog(content: Text("Not implemented yet")));
},
),
),
ListTile(
leading: const Icon(Icons.abc),
title: Text("Typ: ${timetableData.activityType}"),
),
ListTile(
leading: const Icon(Icons.people),
title: Text("Klasse(n): ${timetableData.kl.map((e) => e['name']).join(", ")}"),
),
ListTile(
leading: const Icon(Icons.bug_report_outlined),
title: const Text("Webuntis Rohdaten zeigen"),
onTap: () => JsonViewer.asDialog(context, timetableData.toJson()),
)
],
),
)
],
));
}
}

View File

@ -0,0 +1,59 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_calendar/calendar.dart';
class TimeRegionComponent extends StatefulWidget {
final TimeRegionDetails details;
const TimeRegionComponent({Key? key, required this.details}) : super(key: key);
@override
State<TimeRegionComponent> createState() => _TimeRegionComponentState();
}
class _TimeRegionComponentState extends State<TimeRegionComponent> {
@override
Widget build(BuildContext context) {
String text = widget.details.region.text!;
Color? color = widget.details.region.color;
if (text == 'centerIcon') {
return Container(
color: color,
alignment: Alignment.center,
child: Icon(
widget.details.region.iconData,
size: 17,
color: Theme.of(context).primaryColor,
),
);
} else if(text.startsWith('holiday')) {
return Container(
color: color,
alignment: Alignment.center,
child: Column(
children: [
const SizedBox(height: 5),
const Icon(Icons.cake),
const Text("FREI"),
const SizedBox(height: 5),
RotatedBox(
quarterTurns: 1,
child: Text(
text.split(":").last,
maxLines: 1,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15,
decorationStyle: TextDecorationStyle.dashed,
letterSpacing: 2,
),
),
),
],
),
);
}
return const Placeholder();
}
}

View File

@ -1,10 +1,20 @@
import 'dart:developer';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:marianum_mobile/data/timetable/timetableProps.dart'; import 'package:marianum_mobile/screen/pages/timetable/appointmenetComponent.dart';
import 'package:marianum_mobile/screen/pages/timetable/weekView.dart'; import 'package:marianum_mobile/screen/pages/timetable/timeRegionComponent.dart';
import 'package:marianum_mobile/widget/errorView.dart'; import 'package:marianum_mobile/screen/pages/timetable/timetableEvents.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:syncfusion_flutter_calendar/calendar.dart';
import '../../../api/webuntis/queries/getHolidays/getHolidaysResponse.dart';
import '../../../api/webuntis/queries/getRooms/getRoomsResponse.dart';
import '../../../api/webuntis/queries/getSubjects/getSubjectsResponse.dart';
import '../../../data/timetable/timetableProps.dart';
import 'appointmentDetails.dart';
class Timetable extends StatefulWidget { class Timetable extends StatefulWidget {
const Timetable({Key? key}) : super(key: key); const Timetable({Key? key}) : super(key: key);
@ -14,44 +24,209 @@ class Timetable extends StatefulWidget {
} }
class _TimetableState extends State<Timetable> { class _TimetableState extends State<Timetable> {
bool draggable = true; CalendarController controller = CalendarController();
double elementScale = 40;
double baseElementScale = 40;
@override @override
void initState() { void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
Provider.of<TimetableProps>(context, listen: false).nearest(); Provider.of<TimetableProps>(context, listen: false).run();
}); });
super.initState();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
TimetableProps timetable = Provider.of<TimetableProps>(context, listen: false);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text("Vertretungsplan"), title: const Text("Stunden & Vertretungsplan"),
actions: [ actions: [
IconButton(onPressed: () => timetable.switchWeek(previous: true), icon: const Icon(Icons.chevron_left)), IconButton(
IconButton(onPressed: () => timetable.nearest(), icon: const Icon(Icons.home)), icon: const Icon(Icons.today),
IconButton(onPressed: () => timetable.switchWeek(), icon: const Icon(Icons.chevron_right)) onPressed: () {
controller.displayDate = DateTime.now().jumpToNextWeekDay(DateTime.monday);
//controller.displayDate = DateTime.now().add(Duration(days: 2));
//controller.selectedDate = DateTime.now();
}
),
], ],
), ),
body: Consumer<TimetableProps>( body: Consumer<TimetableProps>(
builder: (context, value, child) { builder: (context, value, child) {
if(value.primaryLoading()) return const Placeholder();
GetHolidaysResponse holidays = value.getHolidaysResponse;
if(value.hasError) { if(value.hasError) {
return ErrorView(icon: Icons.error, text: value.error?.message ?? "Unbekannter Fehler"); WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
showDialog(context: context, builder: (context) {
return AlertDialog(
title: Text("Webuntis error"),
content: Text(value.error.toString()),
);
});
});
} }
if(value.primaryLoading()) { return GestureDetector(
return const Center(child: CircularProgressIndicator()); onScaleStart: (details) => baseElementScale = elementScale,
} onScaleUpdate: (details) {
setState(() {
elementScale = (baseElementScale * details.scale).clamp(40, 80);
});
},
onScaleEnd: (details) {
// TODO save scale for later
},
return WeekView(value); child: SfCalendar(
view: CalendarView.workWeek,
dataSource: _buildTableEvents(value),
controller: controller,
onViewChanged: (ViewChangedDetails details) {
log(details.visibleDates.toString());
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
Provider.of<TimetableProps>(context, listen: false).updateWeek(details.visibleDates.first, details.visibleDates.last);
});
},
onTap: (calendarTapDetails) {
if(calendarTapDetails.appointments == null) return;
Appointment tapped = calendarTapDetails.appointments!.first;
AppointmentDetails.show(context, value, tapped);
log(tapped.id.toString());
},
firstDayOfWeek: DateTime.monday,
specialRegions: _buildSpecialTimeRegions(holidays),
timeSlotViewSettings: TimeSlotViewSettings(
startHour: 07.5,
endHour: 16.5,
timeInterval: const Duration(minutes: 30),
timeFormat: "HH:mm",
dayFormat: "EE",
timeIntervalHeight: elementScale,
),
timeRegionBuilder: (BuildContext context, TimeRegionDetails timeRegionDetails) => TimeRegionComponent(details: timeRegionDetails),
appointmentBuilder: (BuildContext context, CalendarAppointmentDetails details) => AppointmentComponent(details: details),
headerHeight: 0,
selectionDecoration: const BoxDecoration(),
allowAppointmentResize: false,
allowDragAndDrop: false,
allowViewNavigation: false,
),
);
}, },
), ),
); );
} }
List<TimeRegion> _buildSpecialTimeRegions(GetHolidaysResponse holidays, ) {
DateTime lastMonday = DateTime.now().subtract(Duration(days: DateTime.now().weekday - 1));
DateTime firstBreak = lastMonday.copyWith(hour: 10, minute: 15);
DateTime secondBreak = lastMonday.copyWith(hour: 13, minute: 50);
DateTime beforeSchool = lastMonday.copyWith(hour: 7, minute: 30);
return [
...holidays.result.map((e) {
return TimeRegion(
startTime: _parseWebuntisTimestamp(e.startDate, 755),
endTime: _parseWebuntisTimestamp(e.startDate, 1630),
text: 'holiday:${e.name}',
color: Theme.of(context).disabledColor.withAlpha(50),
iconData: Icons.holiday_village_outlined
);
}),
TimeRegion(
startTime: firstBreak,
endTime: firstBreak.add(const Duration(minutes: 20)),
recurrenceRule: 'FREQ=DAILY;INTERVAL=1;COUNT=5',
text: 'centerIcon',
color: Theme.of(context).primaryColor.withAlpha(50),
iconData: Icons.restaurant
),
TimeRegion(
startTime: secondBreak,
endTime: secondBreak.add(const Duration(minutes: 15)),
recurrenceRule: 'FREQ=DAILY;INTERVAL=1;COUNT=5',
text: 'centerIcon',
color: Theme.of(context).primaryColor.withAlpha(50),
iconData: Icons.restaurant
),
TimeRegion(
startTime: beforeSchool,
endTime: beforeSchool.add(const Duration(minutes: 25)),
recurrenceRule: 'FREQ=DAILY;INTERVAL=1;COUNT=5',
color: Theme.of(context).disabledColor.withAlpha(50),
text: "centerIcon",
),
];
}
TimetableEvents _buildTableEvents(TimetableProps data) {
List<Appointment> appointments = data.getTimetableResponse.result.map((element) {
GetRoomsResponse rooms = data.getRoomsResponse;
GetSubjectsResponse subjects = data.getSubjectsResponse;
try {
DateTime startTime = _parseWebuntisTimestamp(element.date, element.startTime);
DateTime endTime = _parseWebuntisTimestamp(element.date, element.endTime);
return Appointment(
id: element,
startTime: startTime,
endTime: endTime,
subject: subjects.result.firstWhere((subject) => subject.id == element.su[0]['id']).alternateName,
location: ""
"${rooms.result.firstWhere((room) => room.id == element.ro[0]['id']).name}"
"\n"
"${element.te.first['longname']}",
notes: element.activityType,
color: _getEventColor(element.code, startTime, endTime),
);
} on Error catch(e) {
log(e.toString());
return Appointment(
startTime: _parseWebuntisTimestamp(element.date, element.startTime),
endTime: _parseWebuntisTimestamp(element.date, element.endTime),
subject: "ERROR",
notes: element.info,
location: 'LOCATION',
color: Theme.of(context).primaryColor,
startTimeZone: '',
endTimeZone: '',
);
}
}).toList();
return TimetableEvents(appointments);
}
DateTime _parseWebuntisTimestamp(int date, int time) {
String timeString = time.toString().padLeft(4, '0');
return DateTime.parse('$date ${timeString.substring(0, 2)}:${timeString.substring(2, 4)}');
}
Color _getEventColor(String? code, DateTime startTime, DateTime endTime) {
int opacity = endTime.isBefore(DateTime.now()) ? 100 : 255;
if(code == "cancelled") return const Color(0xff000000).withAlpha(opacity);
if(code == "irregular") return const Color(0xff8F19B3).withAlpha(opacity);
if(endTime.isBefore(DateTime.now())) return Theme.of(context).primaryColor.withAlpha(opacity);
if(endTime.isAfter(DateTime.now()) && startTime.isBefore(DateTime.now())) return Theme.of(context).primaryColor.withRed(100);
return Theme.of(context).primaryColor;
}
} }

View File

@ -0,0 +1,8 @@
import 'package:syncfusion_flutter_calendar/calendar.dart';
class TimetableEvents extends CalendarDataSource {
TimetableEvents(List<Appointment> source) {
appointments = source;
}
}

View File

@ -1,272 +0,0 @@
import 'package:flutter/cupertino.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import 'package:marianum_mobile/api/webuntis/queries/getHolidays/getHolidaysResponse.dart';
import 'package:marianum_mobile/screen/pages/more/roomplan/roomplan.dart';
import 'package:marianum_mobile/screen/settings/debug/jsonViewer.dart';
import 'package:persistent_bottom_nav_bar/persistent_tab_view.dart';
import 'package:timetable_view/timetable_view.dart';
import '../../../api/webuntis/queries/getHolidays/getHolidays.dart';
import '../../../api/webuntis/queries/getRooms/getRoomsResponse.dart';
import '../../../api/webuntis/queries/getSubjects/getSubjectsResponse.dart';
import '../../../api/webuntis/queries/getTimetable/getTimetableResponse.dart';
import '../../../data/timetable/timetableProps.dart';
extension DateHelpers on DateTime {
bool isToday() {
final now = DateTime.now();
return now.day == day &&
now.month == month &&
now.year == year;
}
}
class WeekView extends StatefulWidget {
final TimetableProps value;
const WeekView(this.value, {Key? key}) : super(key: key);
@override
State<WeekView> createState() => _WeekViewState();
}
class _WeekViewState extends State<WeekView> {
@override
Widget build(BuildContext context) {
return TimetableView(
laneEventsList: _buildLaneEvents(widget.value),
onEventTap: (TableEvent event) {
try {
GetTimetableResponseObject timetableData = widget.value.getTimetableResponse.result.firstWhere((element) => element.id == event.eventId);
GetSubjectsResponseObject subject = widget.value.getSubjectsResponse.result.firstWhere((subject) => subject.id == timetableData.su[0]['id']);
GetRoomsResponseObject room = widget.value.getRoomsResponse.result.firstWhere((room) => room.id == timetableData.ro[0]['id']);
showModalBottomSheet(context: context, builder: (context) => Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 30),
child: Center(
child: Column(
children: [
Icon(Icons.info, color: event.backgroundColor),
const SizedBox(height: 10),
Text("${getEventPrefix(timetableData.code)}${subject.alternateName} - (${subject.longName})", style: const TextStyle(fontSize: 30)),
Text("${Jiffy(event.startTime).format("HH:mm")} - ${Jiffy(event.endTime).format("HH:mm")}", style: const TextStyle(fontSize: 15)),
],
),
),
),
Expanded(
child: ListView(
children: [
ListTile(
leading: const Icon(Icons.notifications_active),
title: Text("Status: ${timetableData.code != null ? "Geändert" : "Regulär"}"),
),
ListTile(
leading: const Icon(Icons.room),
title: Text("Raum: ${room.name}"),
trailing: IconButton(
icon: const Icon(Icons.house_outlined),
onPressed: () {
PersistentNavBarNavigator.pushNewScreen(context, withNavBar: false, screen: const Roomplan());
},
),
),
ListTile(
leading: const Icon(Icons.person),
title: Text("Lehrkraft: (${timetableData.te[0]['name']}) ${timetableData.te[0]['longname']}"),
trailing: IconButton(
icon: const Icon(Icons.textsms_outlined),
onPressed: () {
showDialog(context: context, builder: (context) => const AlertDialog(content: Text("Not implemented yet")));
},
),
),
ListTile(
leading: const Icon(Icons.abc),
title: Text("Typ: ${timetableData.activityType}"),
),
ListTile(
leading: const Icon(Icons.people),
title: Text("Klasse(n): ${timetableData.kl.map((e) => e['name']).join(", ")}"),
),
ListTile(
leading: const Icon(Icons.bug_report_outlined),
title: const Text("Webuntis Rohdaten zeigen"),
onTap: () => JsonViewer.asDialog(context, timetableData.toJson()),
)
],
),
)
],
));
} on StateError {
return;
}
},
timetableStyle: CustomTableStyle(context),
onEmptySlotTap: (int laneIndex, TableEventTime start, TableEventTime end) => {
},
);
}
List<LaneEvents> _buildLaneEvents(TimetableProps data) {
List<LaneEvents> laneEvents = List<LaneEvents>.empty(growable: true);
if(data.primaryLoading()) throw UnimplementedError();
GetTimetableResponse timetable = data.getTimetableResponse;
GetRoomsResponse rooms = data.getRoomsResponse;
GetSubjectsResponse subjects = data.getSubjectsResponse;
GetHolidaysResponse holidays = data.getHolidaysResponse;
List<int> dayList = timetable.result.map((e) => e.date).toSet().toList();
dayList.sort((a, b) => a-b);
for(int i = 0; i <= data.endDate.difference(data.startDate).inDays; i++) {
DateTime currentDay = data.startDate.copyWith().add(Duration(days: i));
// Check Holiday Information
GetHolidaysResponseObject? holidayInfo = GetHolidays.find(holidays, time: currentDay);
if(holidayInfo != null) {
laneEvents.add(
LaneEvents(
lane: getLane(currentDay),
events: List<TableEvent>.of([
TableEvent(
title: holidayInfo.name,
eventId: holidayInfo.id,
laneIndex: data.startDate.millisecondsSinceEpoch,
startTime: parseTime(0800),
endTime: parseTime(1500),
padding: const EdgeInsets.all(5),
backgroundColor: const Color(0xff3D62B3),
location: "\n${holidayInfo.longName}",
)
]),
)
);
}
}
for (var day in dayList) {
DateTime currentDay = DateTime.parse("$day");
Lane currentLane = getLane(currentDay);
//Every Day
List<TableEvent> events = List<TableEvent>.generate(
timetable.result.where((element) => element.date == day).length, (index) {
GetTimetableResponseObject tableEvent = timetable.result.where((element) => element.date == day).elementAt(index);
try {
GetSubjectsResponseObject subject = subjects.result.firstWhere((subject) => subject.id == tableEvent.su[0]['id']);
return TableEvent(
title: "${getEventPrefix(tableEvent.code)}${subject.alternateName} (${subject.longName})",
eventId: tableEvent.id,
laneIndex: day,
startTime: parseTime(tableEvent.startTime),
endTime: parseTime(tableEvent.endTime),
padding: const EdgeInsets.all(5),
backgroundColor: getEventColor(
tableEvent.code ?? "",
currentDay.add(Duration(hours: parseTime(tableEvent.startTime).hour, minutes: parseTime(tableEvent.startTime).minute)),
currentDay.add(Duration(hours: parseTime(tableEvent.endTime).hour, minutes: parseTime(tableEvent.endTime).minute)),
),
location: "\n${rooms.result.firstWhereOrNull((room) => room.id == tableEvent.ro[0]['id'])?.name ?? "?"} - ${tableEvent.te[0]['longname']} (${tableEvent.te[0]['name']})",
);
} on Error {
return TableEvent(title: "Unbekannt", eventId: index, laneIndex: day, startTime: parseTime(tableEvent.startTime), endTime: parseTime(tableEvent.endTime));
}
}
);
//Timepointer
if(currentDay.isToday()) {
events.add(TableEvent(
title: "",
eventId: 0,
laneIndex: day,
startTime: formatTime(DateTime.now()),
endTime: formatTime(DateTime.now().add(const Duration(minutes: 3))),
backgroundColor: Theme.of(context).disabledColor,
));
}
laneEvents.add(
LaneEvents(
lane: currentLane,
events: events
)
);
}
return laneEvents;
}
Lane getLane(DateTime currentDay) {
return Lane(
backgroundColor: currentDay.isToday() ? Theme.of(context).dividerColor : Colors.white,
laneIndex: currentDay.millisecondsSinceEpoch,
name: "${Jiffy(currentDay.toString()).format("dd MMM")}\n${Jiffy(currentDay.toString()).format("EE")}",
textStyle: TextStyle(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
fontSize: 14
)
);
}
TableEventTime parseTime(int input) {
String time = input.toString().length < 4 ? "0$input" : input.toString();
return TableEventTime(hour: int.parse(time.substring(0, 2)), minute: int.parse(time.substring(2, 4)));
}
TableEventTime formatTime(DateTime input) {
return TableEventTime(hour: input.hour, minute: input.minute);
}
String getEventPrefix(String? code) {
if(code == "cancelled") return "Entfällt: ";
if(code == "irregular") return "Änderung: ";
return code ?? "";
}
Color getEventColor(String? code, DateTime startTime, DateTime endTime) {
if(code == "cancelled") return const Color(0xff8F19B3);
if(code == "irregular") return const Color(0xff992B99);
if(endTime.isBefore(DateTime.now())) return Colors.grey;
if(startTime.isAfter(DateTime.now())) return Theme.of(context).primaryColor;
return const Color(0xff99563A);
}
}
class CustomTableStyle extends TimetableStyle {
dynamic context;
CustomTableStyle(this.context);
@override
int get startHour => 07;
@override
int get endHour => 17;
@override
Color get cornerColor => Theme.of(context).primaryColor;
@override
Color get timeItemTextColor => Theme.of(context).primaryColor;
@override
double get timeItemHeight => MediaQuery.of(context).size.width > 1000 ? 60 : 90;
@override
double get timeItemWidth => 40;
@override
double get laneHeight => 40;
@override
double get laneWidth => (MediaQuery.of(context).size.width - timeItemWidth) / 5;
}