custom login implementation, period-based timetable layout with overlap handling, enhanced error dialogs, and unified bottom sheets

This commit is contained in:
2026-05-06 20:42:09 +02:00
parent 50d2941e52
commit 86d12884fc
32 changed files with 1038 additions and 377 deletions
@@ -4,6 +4,8 @@ import 'package:syncfusion_flutter_calendar/calendar.dart';
import 'cross_painter.dart';
class AppointmentTile extends StatelessWidget {
static const _radius = BorderRadius.all(Radius.circular(7));
final Appointment appointment;
final bool crossedOut;
@@ -14,54 +16,51 @@ class AppointmentTile extends StatelessWidget {
final isPast = appointment.endTime.isBefore(DateTime.now());
final color = appointment.color.withAlpha(isPast ? 160 : 255);
final locationLines = (appointment.location ?? '')
.split('\n')
.where((p) => p.isNotEmpty)
.take(2)
.toList(growable: false);
return Padding(
padding: const EdgeInsets.all(1),
child: Stack(
children: [
Positioned.fill(
child: Container(
padding: const EdgeInsets.all(4),
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
alignment: Alignment.topLeft,
decoration: BoxDecoration(
shape: BoxShape.rectangle,
borderRadius: const BorderRadius.all(Radius.circular(7)),
borderRadius: _radius,
color: color,
),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FittedBox(
fit: BoxFit.fitWidth,
child: Text(
appointment.subject,
style: const TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w500),
maxLines: 1,
softWrap: false,
),
),
FittedBox(
fit: BoxFit.fitWidth,
child: Text(
appointment.location?.isNotEmpty == true ? appointment.location! : ' ',
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: const TextStyle(color: Colors.white, fontSize: 10),
),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
_ScaledLine(
text: appointment.subject,
fontSize: 15,
fontWeight: FontWeight.w500,
),
for (final line in locationLines)
_ScaledLine(text: line, fontSize: 10),
],
),
),
),
if (crossedOut)
Positioned.fill(
child: DecoratedBox(
decoration: BoxDecoration(
border: Border.all(width: 2, color: Colors.red.withAlpha(200)),
borderRadius: const BorderRadius.all(Radius.circular(7)),
child: ClipRRect(
borderRadius: _radius,
child: DecoratedBox(
decoration: BoxDecoration(
border: Border.all(width: 2, color: Colors.red.withAlpha(200)),
borderRadius: _radius,
),
child: CustomPaint(painter: CrossPainter()),
),
child: CustomPaint(painter: CrossPainter()),
),
),
],
@@ -69,3 +68,35 @@ class AppointmentTile extends StatelessWidget {
);
}
}
/// One row of appointment text. The FittedBox scales **only this line** down
/// when the text is wider than the tile, so a long teacher name does not
/// shrink the room number above it.
class _ScaledLine extends StatelessWidget {
final String text;
final double fontSize;
final FontWeight? fontWeight;
const _ScaledLine({
required this.text,
required this.fontSize,
this.fontWeight,
});
@override
Widget build(BuildContext context) => FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
text,
style: TextStyle(
color: Colors.white,
fontSize: fontSize,
fontWeight: fontWeight,
height: 1.1,
),
maxLines: 1,
softWrap: false,
),
);
}