custom login implementation, period-based timetable layout with overlap handling, enhanced error dialogs, and unified bottom sheets
This commit is contained in:
@@ -1,51 +1,32 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shows a modal bottom sheet for an appointment, matching the design of the
|
||||
/// other sheets in the app (file details, file actions, overflow lessons):
|
||||
/// drag handle on top, default theme background, ListTile-style header
|
||||
/// followed by a divider, scrollable body below.
|
||||
void showAppointmentBottomSheet(
|
||||
BuildContext context, {
|
||||
required Widget Function(BuildContext context) header,
|
||||
required SliverChildListDelegate Function(BuildContext context) body,
|
||||
required Widget header,
|
||||
required List<Widget> Function(BuildContext sheetContext) children,
|
||||
}) {
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
showDragHandle: true,
|
||||
useSafeArea: true,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
builder: (sheetContext) => DraggableScrollableSheet(
|
||||
expand: false,
|
||||
initialChildSize: 0.4,
|
||||
minChildSize: 0.2,
|
||||
maxChildSize: 0.7,
|
||||
snap: true,
|
||||
snapSizes: const [0.4],
|
||||
builder: (_, scrollController) => CustomScrollView(
|
||||
controller: scrollController,
|
||||
slivers: [
|
||||
SliverPersistentHeader(
|
||||
pinned: true,
|
||||
delegate: _StickyHeader(child: header(sheetContext)),
|
||||
),
|
||||
SliverList(delegate: body(sheetContext)),
|
||||
],
|
||||
builder: (sheetContext) => SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
header,
|
||||
const Divider(height: 1),
|
||||
...children(sheetContext),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _StickyHeader extends SliverPersistentHeaderDelegate {
|
||||
_StickyHeader({required this.child});
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
double get minExtent => 100;
|
||||
@override
|
||||
double get maxExtent => 100;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) => Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: SizedBox.expand(child: child),
|
||||
);
|
||||
|
||||
@override
|
||||
bool shouldRebuild(covariant _StickyHeader oldDelegate) => oldDelegate.child != child;
|
||||
}
|
||||
|
||||
@@ -11,51 +11,49 @@ import 'delete_custom_event.dart';
|
||||
|
||||
class CustomEventSheet {
|
||||
static void show(BuildContext context, CustomTimetableEvent event) {
|
||||
final timeRange =
|
||||
'${Jiffy.parseFromDateTime(event.startDate).format(pattern: 'HH:mm')} - '
|
||||
'${Jiffy.parseFromDateTime(event.endDate).format(pattern: 'HH:mm')}';
|
||||
|
||||
showAppointmentBottomSheet(
|
||||
context,
|
||||
header: (_) => Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(event.title, style: const TextStyle(fontSize: 25, overflow: TextOverflow.ellipsis)),
|
||||
Text(
|
||||
'${Jiffy.parseFromDateTime(event.startDate).format(pattern: 'HH:mm')} - '
|
||||
'${Jiffy.parseFromDateTime(event.endDate).format(pattern: 'HH:mm')}',
|
||||
style: const TextStyle(fontSize: 15),
|
||||
),
|
||||
],
|
||||
),
|
||||
header: ListTile(
|
||||
leading: const Icon(Icons.event_outlined, size: 32),
|
||||
title: Text(event.title, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
subtitle: Text(timeRange),
|
||||
),
|
||||
body: (sheetCtx) => SliverChildListDelegate([
|
||||
const Divider(),
|
||||
Center(
|
||||
child: Wrap(
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.of(sheetCtx).pop();
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => CustomEventEditDialog(existingEvent: event),
|
||||
);
|
||||
},
|
||||
label: const Text('Bearbeiten'),
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
showDeleteCustomEventDialog(context, event).future.then((_) {
|
||||
if (!sheetCtx.mounted) return;
|
||||
children: (sheetCtx) => [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Center(
|
||||
child: Wrap(
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.of(sheetCtx).pop();
|
||||
});
|
||||
},
|
||||
label: const Text('Löschen'),
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
),
|
||||
],
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => CustomEventEditDialog(existingEvent: event),
|
||||
);
|
||||
},
|
||||
label: const Text('Bearbeiten'),
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
showDeleteCustomEventDialog(context, event).future.then((_) {
|
||||
if (!sheetCtx.mounted) return;
|
||||
Navigator.of(sheetCtx).pop();
|
||||
});
|
||||
},
|
||||
label: const Text('Löschen'),
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.info_outline),
|
||||
title: Text(event.description.isEmpty ? 'Keine Beschreibung' : event.description),
|
||||
@@ -82,7 +80,7 @@ class CustomEventSheet {
|
||||
),
|
||||
),
|
||||
DebugTile(sheetCtx).jsonData(event.toJson()),
|
||||
]),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,29 +23,24 @@ class WebuntisLessonSheet {
|
||||
final headerTitle = _firstNonEmpty([headerSubject.alternateName, headerSubject.name, headerSubject.longName, '?']);
|
||||
final headerLongName = headerSubject.longName.isNotEmpty && headerSubject.longName != headerTitle ? headerSubject.longName : '';
|
||||
|
||||
final timeRange =
|
||||
'${Jiffy.parseFromDateTime(appointment.startTime).format(pattern: 'HH:mm')} - '
|
||||
'${Jiffy.parseFromDateTime(appointment.endTime).format(pattern: 'HH:mm')}';
|
||||
|
||||
showAppointmentBottomSheet(
|
||||
context,
|
||||
header: (_) => Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'${_codePrefix(lesson.code)}$headerTitle',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 25),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (headerLongName.isNotEmpty) Text(headerLongName),
|
||||
Text(
|
||||
'${Jiffy.parseFromDateTime(appointment.startTime).format(pattern: 'HH:mm')} - '
|
||||
'${Jiffy.parseFromDateTime(appointment.endTime).format(pattern: 'HH:mm')}',
|
||||
style: const TextStyle(fontSize: 15),
|
||||
),
|
||||
],
|
||||
header: ListTile(
|
||||
leading: Icon(_iconForCode(lesson.code), size: 32),
|
||||
title: Text(
|
||||
'${_codePrefix(lesson.code)}$headerTitle',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
subtitle: Text(headerLongName.isNotEmpty
|
||||
? '$timeRange\n$headerLongName'
|
||||
: timeRange),
|
||||
isThreeLine: headerLongName.isNotEmpty,
|
||||
),
|
||||
body: (_) => SliverChildListDelegate(<Widget>[
|
||||
const Divider(),
|
||||
children: (_) => <Widget>[
|
||||
ListTile(
|
||||
leading: const Icon(Icons.notifications_active),
|
||||
title: Text('Status: ${_statusLabel(lesson.code)}'),
|
||||
@@ -82,10 +77,21 @@ class WebuntisLessonSheet {
|
||||
),
|
||||
..._optionalTextTiles(lesson),
|
||||
DebugTile(context).jsonData(lesson.toJson()),
|
||||
]),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
static IconData _iconForCode(String? code) {
|
||||
switch (code) {
|
||||
case 'cancelled':
|
||||
return Icons.event_busy_outlined;
|
||||
case 'irregular':
|
||||
return Icons.swap_horiz;
|
||||
default:
|
||||
return Icons.school_outlined;
|
||||
}
|
||||
}
|
||||
|
||||
static Widget _roomTile(BuildContext context, TimetableState state, GetTimetableResponseObject lesson) {
|
||||
final trailing = IconButton(
|
||||
icon: const Icon(Icons.house_outlined),
|
||||
@@ -193,7 +199,7 @@ class WebuntisLessonSheet {
|
||||
|
||||
static Widget? _textTile(IconData icon, String label, String? value) {
|
||||
final text = (value ?? '').trim();
|
||||
if (text.isEmpty) return null;
|
||||
if (text.isEmpty || text == '-') return null;
|
||||
return ListTile(
|
||||
leading: Icon(icon),
|
||||
title: Text(label),
|
||||
|
||||
Reference in New Issue
Block a user