fix: timetable monthly view design

This commit is contained in:
checkedear
2026-04-21 22:41:48 +02:00
parent 9ec164daac
commit b17509f9db

View File

@@ -63,9 +63,7 @@ class _HomeTimetableMonthlyScreen
forceCache: forceCache,
);
var testsResp = await widget.data.client.getTests(forceCache: forceCache);
var omissionsResp = await widget.data.client.getOmissions(
forceCache: forceCache,
);
var omissionsResp = await widget.data.client.getOmissions(forceCache: true);
List<DateTime> dates = List.empty(growable: true);
for (var i = 0; i < days; i++) {
@@ -119,496 +117,10 @@ class _HomeTimetableMonthlyScreen
}
Widget _buildContent(BuildContext context) {
if (lessons != null &&
omissions != null &&
tests != null &&
dates != null) {
List<Widget> ttDays = [];
final meow = dates![20];
final currentMonthStart = DateTime.utc(meow.year, meow.month, 1);
final currentMonthEnd = DateTime.utc(
meow.year,
meow.month + 1,
).subtract(Duration(days: 1));
// column-major -> row-major
for (var day = 0; day < 7; day++) {
for (var week = 0; week < 7; week++) {
final d = dates![week * 7 + day];
if (d.isBefore(currentMonthStart) || d.isAfter(currentMonthEnd)) {
ttDays.add(
Column(
children: [
Container(
width: 40,
height: 40,
clipBehavior: Clip.antiAlias,
decoration: ShapeDecoration(
color: appStyle.colors.cardTranslucent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
SizedBox(height: 4),
Text(
d.format(widget.data.l10n, FormatMode.d),
style: appStyle.fonts.B_16R.apply(
color:
(d.weekday == DateTime.saturday ||
d.weekday == DateTime.sunday)
? appStyle.colors.errorText
: appStyle.colors.textTertiary,
),
),
],
),
);
} else {
Widget body = SizedBox();
Color bodyBgColor = appStyle.colors.a15p;
var lessonsToday = lessons!.where(
(lesson) =>
lesson.start.isAfter(d.getMidnight()) &&
lesson.end.isBefore(
d.getMidnight().add(Duration(hours: 23, minutes: 59)),
),
);
var omissionType = lessonsToday.firstWhereOrNull(
(lesson) =>
lesson.studentPresence != null &&
lesson.studentPresence?.name != OmissionConsts.na &&
lesson.studentPresence?.name != OmissionConsts.present,
);
switch (activeFilter) {
case ActiveFilter.lessonNo:
if (lessonsToday.isNotEmpty) {
body = Center(
child: Text(
lessonsToday.length.toString(),
style: appStyle.fonts.H_16px.apply(
color:
omissionType != null &&
(omissionType.studentPresence!.name ==
OmissionConsts.absence ||
omissionType.studentPresence!.name ==
OmissionConsts.na)
? appStyle.colors.errorText
: timeNow().day == d.day &&
timeNow().month == d.month
? appStyle.colors.accent
: appStyle.colors.secondary,
),
),
);
if (omissionType != null &&
(omissionType.studentPresence!.name ==
OmissionConsts.absence ||
omissionType.studentPresence!.name ==
OmissionConsts.na)) {
bodyBgColor = appStyle.colors.error15p;
}
}
break;
case ActiveFilter.tests:
if (lessonsToday.firstWhereOrNull(
(lesson) => tests!.any(
(test) =>
test.lessonNumber == lesson.lessonNumber &&
lesson.start.isAfter(test.date.getMidnight()) &&
lesson.end.isBefore(
test.date.getMidnight().add(
Duration(hours: 23, minutes: 59),
),
),
),
) !=
null) {
body = Center(
child: FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.editPen4Solid,
size: 20.0,
color: appStyle.colors.accent,
),
);
}
break;
case ActiveFilter.omissions:
if (omissionType != null) {
switch (omissionType.studentPresence!.name) {
case OmissionConsts.absence:
final omission = omissions!.firstWhereOrNull((omission) {
return omission.date
.getMidnight()
.millisecondsSinceEpoch ==
omissionType.start
.getMidnight()
.millisecondsSinceEpoch &&
omission.subject.uid == omissionType.subject?.uid;
});
if (omission != null) {
switch (omission.state) {
case "Igazolando":
body = Center(
child: FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.restrictedSolid,
size: 20.0,
color: appStyle.colors.warningAccent,
),
);
bodyBgColor = appStyle.colors.warning15p;
break;
default:
body = Center(
child: FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.multiplySolid,
size: 20.0,
color: appStyle.colors.accent,
),
);
}
} else {
body = Center(
child: FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.multiplySolid,
size: 20.0,
color: appStyle.colors.accent,
),
);
}
break;
default:
logger.fine(
"omission: ${omissionType.studentPresence!.name}",
);
body = Center(
child: FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.timerLine,
size: 20.0,
color: appStyle.colors.accent,
),
);
}
}
break;
}
ttDays.add(
Column(
children: [
Container(
width: 40,
height: 40,
clipBehavior: Clip.antiAlias,
decoration: ShapeDecoration(
color: bodyBgColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: body,
),
SizedBox(height: 4),
Text(
d.format(widget.data.l10n, FormatMode.d),
style: appStyle.fonts.B_16R.apply(
color:
(d.weekday == DateTime.saturday ||
d.weekday == DateTime.sunday) &&
lessonsToday.isEmpty
? appStyle.colors.errorText
: appStyle.colors.textSecondary,
),
),
SizedBox(height: 12),
],
),
);
if (timeNow().getMidnight().millisecondsSinceEpoch ==
d.toLocal().getMidnight().millisecondsSinceEpoch) {
bodyBgColor = appStyle.colors.buttonSecondaryFill;
}
}
}
}
return Scaffold(
backgroundColor: appStyle.colors.background,
body: Stack(
children: [
SizedBox(
width: MediaQuery.of(context).size.width,
height: 74 + 16,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
widget.data.l10n.timetable,
style: appStyle.fonts.H_H2.apply(
color: appStyle.colors.textPrimary,
),
),
Row(
children: [
GestureDetector(
child: Card(
color: appStyle.colors.buttonSecondaryFill,
child: Padding(
padding: const EdgeInsets.all(8),
child: FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.tableSolid,
size: 26.0,
color: appStyle.colors.accent,
),
),
),
onTap: () {
context.pop();
},
),
// Nincs elkészítve jelenleg: Dolgozat stb hozzáadása(?)
// Card(
// color: appStyle.colors.buttonSecondaryFill,
// child: Padding(
// padding: const EdgeInsets.all(4),
// child: FirkaIconWidget(
// FirkaIconType.majesticons,
// Majesticon.plusLine,
// size: 32.0,
// color: appStyle.colors.accent,
// ),
// ),
// ),
GestureDetector(
child: Card(
color: appStyle.colors.buttonSecondaryFill,
child: Padding(
padding: const EdgeInsets.all(8),
child: FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.settingsCogSolid,
size: 26.0,
color: appStyle.colors.accent,
),
),
),
onTap: () {
showSettingsSheet(
context,
MediaQuery.of(context).size.height * 0.4,
widget.data,
widget.data.settings
.group("settings")
.subGroup("timetable_toast"),
);
},
),
],
),
],
),
SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
GestureDetector(
behavior: HitTestBehavior.translucent,
child: SizedBox(
width: 24,
height: 24,
child: FirkaIconWidget(
FirkaIconType.icons,
"dropdownLeft",
size: 24,
color: appStyle.colors.accent,
),
),
onTap: () async {
var newNow = DateTime(now!.year, now!.month - 1);
setState(() {
now = newNow;
lessons = null;
dates = null;
});
await initForMonth(newNow);
setState(() {
now = newNow;
});
},
),
Text(
now!
.format(widget.data.l10n, FormatMode.yyyymmmm)
.toLowerCase(),
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
),
GestureDetector(
child: FirkaIconWidget(
FirkaIconType.icons,
"dropdownRight",
size: 24,
color: appStyle.colors.accent,
),
onTap: () async {
var newNow = DateTime(now!.year, now!.month + 1);
setState(() {
now = newNow;
lessons = null;
dates = null;
});
await initForMonth(newNow);
setState(() {
now = newNow;
});
},
),
],
),
],
),
),
),
TransparentPointer(
child: Padding(
padding: const EdgeInsets.only(top: 74 + 16 + 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
StaggeredGrid.count(
crossAxisCount: 7,
mainAxisSpacing: 8,
children: ttDays,
),
],
),
),
),
TransparentPointer(
child: Column(
children: [
SizedBox(height: MediaQuery.of(context).size.height / 1.3),
SizedBox(
width: MediaQuery.of(context).size.width,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SizedBox(),
Row(
children: [
_StatusToast(
FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.clockSolid,
color: appStyle.colors.accent,
size: 16,
),
lessons!
.where(
(lesson) =>
lesson.start.isAfter(
currentMonthStart,
) &&
lesson.end.isBefore(currentMonthEnd),
)
.length,
activeFilter == ActiveFilter.lessonNo,
() {
setState(() {
activeFilter = ActiveFilter.lessonNo;
});
},
),
_StatusToast(
FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.editPen4Solid,
color: appStyle.colors.accent,
size: 16,
),
lessons!
.where(
(lesson) => tests!.any(
(test) =>
test.lessonNumber ==
lesson.lessonNumber &&
lesson.start.isAfter(
test.date.getMidnight(),
) &&
lesson.end.isBefore(
test.date.getMidnight().add(
Duration(hours: 23, minutes: 59),
),
),
),
)
.length,
activeFilter == ActiveFilter.tests,
() {
setState(() {
activeFilter = ActiveFilter.tests;
});
},
),
_StatusToast(
FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.timerLine,
color: appStyle.colors.accent,
size: 16,
),
lessons!
.where(
(lesson) =>
lesson.start.isAfter(
currentMonthStart,
) &&
lesson.end.isBefore(currentMonthEnd) &&
lesson.studentPresence != null &&
lesson.studentPresence?.name !=
OmissionConsts.na &&
lesson.studentPresence?.name !=
OmissionConsts.present,
)
.length,
activeFilter == ActiveFilter.omissions,
() {
setState(() {
activeFilter = ActiveFilter.omissions;
});
},
),
],
),
SizedBox(),
],
),
),
],
),
),
],
),
);
} else {
if (lessons == null ||
omissions == null ||
tests == null ||
dates == null) {
return Scaffold(
backgroundColor: appStyle.colors.background,
body: Column(
@@ -622,6 +134,437 @@ class _HomeTimetableMonthlyScreen
),
);
}
List<Widget> ttDays = [];
final meow = dates![20];
final currentMonthStart = DateTime.utc(meow.year, meow.month, 1);
final currentMonthEnd = DateTime.utc(
meow.year,
meow.month + 1,
).subtract(Duration(days: 1));
// column-major -> row-major
for (var week = 0; week < 7; week++) {
for (var day = 0; day < 7; day++) {
final d = dates![week * 7 + day];
bool outOfRange =
d.isBefore(currentMonthStart) || d.isAfter(currentMonthEnd);
bool isToday =
!outOfRange && timeNow().day == d.day && timeNow().month == d.month;
Widget body = SizedBox();
Color? todayAccent = isToday ? appStyle.colors.textPrimaryLight : null;
Color bodyBgColor = appStyle.colors.a15p;
var lessonsToday = lessons!.where(
(lesson) =>
lesson.start.isAfter(d.getMidnight()) &&
lesson.end.isBefore(
d.getMidnight().add(Duration(hours: 23, minutes: 59)),
),
);
var omittedLesson = lessonsToday.firstWhereOrNull(
(lesson) =>
lesson.studentPresence != null &&
lesson.studentPresence?.name != OmissionConsts.na &&
lesson.studentPresence?.name != OmissionConsts.present,
);
if (lessonsToday.isNotEmpty) {
switch (activeFilter) {
case ActiveFilter.lessonNo:
body = Center(
child: Text(
lessonsToday.length.toString(),
style: appStyle.fonts.H_16px.apply(
color:
todayAccent ??
(omittedLesson != null
? appStyle.colors.errorText
: appStyle.colors.secondary),
),
),
);
if (omittedLesson != null) {
bodyBgColor = appStyle.colors.error15p;
}
break;
case ActiveFilter.tests:
if (lessonsToday.firstWhereOrNull(
(lesson) => tests!.any(
(test) =>
test.lessonNumber == lesson.lessonNumber &&
lesson.start.isAfter(test.date.getMidnight()) &&
lesson.end.isBefore(
test.date.getMidnight().add(
Duration(hours: 23, minutes: 59),
),
),
),
) !=
null) {
body = Center(
child: FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.editPen4Solid,
size: 20.0,
color: todayAccent ?? appStyle.colors.accent,
),
);
}
break;
case ActiveFilter.omissions:
if (omittedLesson == null) {
break;
}
// TODO: Nézze meg a többi órát is
final omission = omissions!.firstWhereOrNull((omission) {
return omission.date.getMidnight().millisecondsSinceEpoch ==
omittedLesson.start
.getMidnight()
.millisecondsSinceEpoch &&
omission.subject.uid == omittedLesson.subject?.uid;
});
if (omission?.state == "Igazolt") {
body = Center(
child: FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.multiplySolid,
size: 20.0,
color: todayAccent ?? appStyle.colors.accent,
),
);
break;
}
if (omission?.lateForMin != null) {
body = Center(
child: FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.timerLine,
size: 20.0,
color: todayAccent ?? appStyle.colors.warningAccent,
),
);
bodyBgColor = appStyle.colors.warning15p;
break;
}
body = Center(
child: FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.restrictedSolid,
size: 20.0,
color: todayAccent ?? appStyle.colors.errorAccent,
),
);
bodyBgColor = appStyle.colors.error15p;
break;
}
}
if (isToday) {
bodyBgColor = appStyle.colors.accent;
}
if (outOfRange) {
bodyBgColor = appStyle.colors.cardTranslucent;
}
bool isWeekend = d.weekday > 5 && lessonsToday.isEmpty;
Color textColor = (isWeekend
? appStyle.colors.errorText
: isToday
? appStyle.colors.textPrimary
: appStyle.colors.textTertiary);
if (outOfRange) {
textColor = textColor.withAlpha(77);
} else if (isWeekend) {
textColor = textColor.withAlpha(128);
}
ttDays.add(
Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40,
height: 40,
clipBehavior: Clip.antiAlias,
decoration: ShapeDecoration(
color: bodyBgColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: body,
),
SizedBox(height: 4),
Text(
d.format(widget.data.l10n, FormatMode.d),
style: (isToday ? appStyle.fonts.B_14SB : appStyle.fonts.B_14R)
.apply(color: textColor),
),
],
),
);
}
}
return Scaffold(
backgroundColor: appStyle.colors.background,
body: Stack(
children: [
SizedBox(
width: MediaQuery.of(context).size.width,
height: 74 + 16,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
widget.data.l10n.timetable,
style: appStyle.fonts.H_H2.apply(
color: appStyle.colors.textPrimary,
),
),
Row(
children: [
GestureDetector(
child: Card(
color: appStyle.colors.buttonSecondaryFill,
child: Padding(
padding: const EdgeInsets.all(8),
child: FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.tableSolid,
size: 26.0,
color: appStyle.colors.accent,
),
),
),
onTap: () {
context.pop();
},
),
// Nincs elkészítve jelenleg: Dolgozat stb hozzáadása(?)
// Card(
// color: appStyle.colors.buttonSecondaryFill,
// child: Padding(
// padding: const EdgeInsets.all(4),
// child: FirkaIconWidget(
// FirkaIconType.majesticons,
// Majesticon.plusLine,
// size: 32.0,
// color: appStyle.colors.accent,
// ),
// ),
// ),
GestureDetector(
child: Card(
color: appStyle.colors.buttonSecondaryFill,
child: Padding(
padding: const EdgeInsets.all(8),
child: FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.settingsCogSolid,
size: 26.0,
color: appStyle.colors.accent,
),
),
),
onTap: () {
showSettingsSheet(
context,
MediaQuery.of(context).size.height * 0.4,
widget.data,
widget.data.settings
.group("settings")
.subGroup("timetable_toast"),
);
},
),
],
),
],
),
SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
GestureDetector(
behavior: HitTestBehavior.translucent,
child: SizedBox(
width: 24,
height: 24,
child: FirkaIconWidget(
FirkaIconType.icons,
"dropdownLeft",
size: 24,
color: appStyle.colors.accent,
),
),
onTap: () async {
var newNow = DateTime(now!.year, now!.month - 1);
setState(() {
now = newNow;
lessons = null;
dates = null;
});
await initForMonth(newNow);
setState(() {
now = newNow;
});
},
),
Text(
now!
.format(widget.data.l10n, FormatMode.yyyymmmm)
.toLowerCase(),
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
),
GestureDetector(
child: FirkaIconWidget(
FirkaIconType.icons,
"dropdownRight",
size: 24,
color: appStyle.colors.accent,
),
onTap: () async {
var newNow = DateTime(now!.year, now!.month + 1);
setState(() {
now = newNow;
lessons = null;
dates = null;
});
await initForMonth(newNow);
setState(() {
now = newNow;
});
},
),
],
),
],
),
),
),
TransparentPointer(
child: Padding(
padding: const EdgeInsets.only(
top: 74 + 16 + 12,
left: 20,
right: 20,
),
child: GridView.count(
crossAxisCount: 7,
mainAxisSpacing: 16,
mainAxisExtent: 62,
children: ttDays,
),
),
),
TransparentPointer(
child: Align(
alignment: Alignment.bottomCenter,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 8,
children: [
_StatusToast(
FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.clockSolid,
color: appStyle.colors.accent,
size: 16,
),
lessons!
.where(
(lesson) =>
lesson.start.isAfter(currentMonthStart) &&
lesson.end.isBefore(currentMonthEnd),
)
.length,
activeFilter == ActiveFilter.lessonNo,
() {
setState(() {
activeFilter = ActiveFilter.lessonNo;
});
},
),
_StatusToast(
FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.editPen4Solid,
color: appStyle.colors.accent,
size: 16,
),
lessons!
.where(
(lesson) => tests!.any(
(test) =>
test.lessonNumber == lesson.lessonNumber &&
lesson.start.isAfter(test.date.getMidnight()) &&
lesson.end.isBefore(
test.date.getMidnight().add(
Duration(hours: 23, minutes: 59),
),
),
),
)
.length,
activeFilter == ActiveFilter.tests,
() {
setState(() {
activeFilter = ActiveFilter.tests;
});
},
),
_StatusToast(
FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.timerLine,
color: appStyle.colors.accent,
size: 16,
),
lessons!
.where(
(lesson) =>
lesson.start.isAfter(currentMonthStart) &&
lesson.end.isBefore(currentMonthEnd) &&
lesson.studentPresence != null &&
lesson.studentPresence?.name !=
OmissionConsts.na &&
lesson.studentPresence?.name !=
OmissionConsts.present,
)
.length,
activeFilter == ActiveFilter.omissions,
() {
setState(() {
activeFilter = ActiveFilter.omissions;
});
},
),
],
),
),
),
],
),
);
}
}
@@ -638,29 +581,29 @@ class _StatusToast extends StatelessWidget {
return GestureDetector(
onTap: _onTap,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 6),
clipBehavior: Clip.antiAlias,
decoration: ShapeDecoration(
color: _active
? appStyle.colors.buttonSecondaryFill
: appStyle.colors.cardTranslucent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(16),
),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
child: Row(
children: [
_icon,
SizedBox(width: 6),
Text(
_count.toString(),
style: appStyle.fonts.H_16px.apply(
color: appStyle.colors.textPrimary,
),
child: Row(
spacing: 6,
children: [
_icon,
Text(
_count.toString(),
style: appStyle.fonts.H_16px.apply(
color: _active
? appStyle.colors.textPrimary
: appStyle.colors.textSecondary,
),
],
),
),
],
),
),
);