updated timetable UI with event status and enhanced appointment tile rendering

This commit is contained in:
2026-05-06 22:53:24 +02:00
parent 95ef29fb09
commit b8cac73e74
5 changed files with 163 additions and 23 deletions
@@ -1,10 +1,15 @@
import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_calendar/calendar.dart';
import '../data/arbitrary_appointment.dart';
import 'cross_painter.dart';
class AppointmentTile extends StatelessWidget {
static const _radius = BorderRadius.all(Radius.circular(7));
static const _titleFontSize = 15.0;
static const _titleMinFontSize = 11.0;
static const _bodyFontSize = 10.0;
static const _bodyLineHeight = 1.15;
final Appointment appointment;
final bool crossedOut;
@@ -15,12 +20,8 @@ class AppointmentTile extends StatelessWidget {
Widget build(BuildContext context) {
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);
final isCustom = appointment.id is CustomAppointment;
final description = appointment.location ?? '';
return Padding(
padding: const EdgeInsets.all(1),
@@ -37,15 +38,33 @@ class AppointmentTile extends StatelessWidget {
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.max,
children: [
_ScaledLine(
_AdaptiveTitle(
text: appointment.subject,
fontSize: 15,
fontSize: _titleFontSize,
minFontSize: _titleMinFontSize,
fontWeight: FontWeight.w500,
),
for (final line in locationLines)
_ScaledLine(text: line, fontSize: 10),
if (isCustom) ...[
if (description.isNotEmpty)
Expanded(
child: Padding(
padding: const EdgeInsets.only(top: 1),
child: _WrappingBody(
text: description,
fontSize: _bodyFontSize,
lineHeight: _bodyLineHeight,
),
),
),
] else ...[
for (final line in description
.split('\n')
.where((p) => p.isNotEmpty)
.take(2))
_ScaledLine(text: line, fontSize: _bodyFontSize),
],
],
),
),
@@ -69,18 +88,111 @@ class AppointmentTile extends StatelessWidget {
}
}
/// Renders the appointment title. Scales down to fit the available width via
/// [FittedBox], but never below [minFontSize] — when even the minimum size
/// overflows, the text is rendered at [minFontSize] with an ellipsis.
class _AdaptiveTitle extends StatelessWidget {
final String text;
final double fontSize;
final double minFontSize;
final FontWeight? fontWeight;
const _AdaptiveTitle({
required this.text,
required this.fontSize,
required this.minFontSize,
this.fontWeight,
});
@override
Widget build(BuildContext context) {
final baseStyle = TextStyle(
color: Colors.white,
fontSize: fontSize,
fontWeight: fontWeight,
height: 1.1,
);
final textScaler = MediaQuery.textScalerOf(context);
return LayoutBuilder(
builder: (context, constraints) {
// Probe at the minimum size: if even that overflows, we have to ellipsize.
final probe = TextPainter(
text: TextSpan(text: text, style: baseStyle.copyWith(fontSize: minFontSize)),
textDirection: TextDirection.ltr,
maxLines: 1,
textScaler: textScaler,
)..layout();
if (probe.width > constraints.maxWidth) {
return Text(
text,
style: baseStyle.copyWith(fontSize: minFontSize),
maxLines: 1,
softWrap: false,
overflow: TextOverflow.ellipsis,
);
}
return FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
text,
style: baseStyle,
maxLines: 1,
softWrap: false,
),
);
},
);
}
}
/// Body text for custom events. Wraps to fill the available height and clips
/// trailing content with an ellipsis if there is more than fits.
class _WrappingBody extends StatelessWidget {
final String text;
final double fontSize;
final double lineHeight;
const _WrappingBody({
required this.text,
required this.fontSize,
required this.lineHeight,
});
@override
Widget build(BuildContext context) {
final style = TextStyle(
color: Colors.white,
fontSize: fontSize,
height: lineHeight,
);
final textScaler = MediaQuery.textScalerOf(context);
return LayoutBuilder(
builder: (context, constraints) {
final lineBox = textScaler.scale(fontSize) * lineHeight;
final maxLines = (constraints.maxHeight / lineBox).floor().clamp(1, 99);
return Text(
text,
style: style,
maxLines: maxLines,
overflow: TextOverflow.ellipsis,
softWrap: true,
);
},
);
}
}
/// 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
@@ -92,7 +204,6 @@ class _ScaledLine extends StatelessWidget {
style: TextStyle(
color: Colors.white,
fontSize: fontSize,
fontWeight: fontWeight,
height: 1.1,
),
maxLines: 1,