refactored lesson details, centralized logout logic, and added resume re-fetch
This commit is contained in:
@@ -10,7 +10,7 @@ class Roomplan extends StatelessWidget {
|
||||
title: const Text('Raumplan'),
|
||||
),
|
||||
body: PhotoView(
|
||||
imageProvider: Image.asset('assets/img/raumplan.jpg').image,
|
||||
imageProvider: Image.asset('assets/img/raumplan.png').image,
|
||||
minScale: 0.5,
|
||||
maxScale: 2.0,
|
||||
backgroundDecoration: BoxDecoration(color: Theme.of(context).colorScheme.surface),
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../../../model/account_data.dart';
|
||||
import '../../../../state/app/modules/settings/bloc/settings_cubit.dart';
|
||||
import '../../../../widget/centered_leading.dart';
|
||||
import '../../../../widget/confirm_dialog.dart';
|
||||
import '../../../../widget/debug/cache_view.dart';
|
||||
|
||||
class AccountSection extends StatelessWidget {
|
||||
const AccountSection({super.key});
|
||||
@@ -26,16 +22,13 @@ class AccountSection extends StatelessWidget {
|
||||
title: 'Abmelden?',
|
||||
content: 'Möchtest du dich wirklich abmelden?',
|
||||
confirmButton: 'Abmelden',
|
||||
onConfirmAsync: () async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.clear();
|
||||
PaintingBinding.instance.imageCache.clear();
|
||||
if (!context.mounted) return;
|
||||
await context.read<SettingsCubit>().reset();
|
||||
await const CacheView().clear();
|
||||
if (!context.mounted) return;
|
||||
await AccountData().removeData(context: context);
|
||||
},
|
||||
// Cleanup of caches, hydrated bloc storage and bloc in-memory state is
|
||||
// handled by the AccountBloc listener in main.dart on the loggedOut
|
||||
// transition. Doing the cleanup *before* setting loggedOut caused
|
||||
// rebuilds in the still-mounted App tree (TimetableBloc/ChatListBloc
|
||||
// emitting empty states) which raced with the home-route swap and
|
||||
// produced a black screen.
|
||||
onConfirmAsync: () => AccountData().removeData(context: context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ class ChatBubbleStyles {
|
||||
|
||||
BubbleStyle getSystemStyle() => BubbleStyle(
|
||||
color: AppTheme.isDarkMode(context) ? const Color(0xff182229) : Colors.white,
|
||||
borderWidth: 1,
|
||||
elevation: 2,
|
||||
margin: const BubbleEdges.only(bottom: 20, top: 10),
|
||||
alignment: Alignment.center,
|
||||
@@ -35,7 +34,6 @@ class ChatBubbleStyles {
|
||||
return BubbleStyle(
|
||||
nip: BubbleNip.leftTop,
|
||||
color: seamless ? Colors.transparent : color,
|
||||
borderWidth: seamless ? 0 : 1,
|
||||
elevation: seamless ? 0 : 1,
|
||||
margin: const BubbleEdges.only(bottom: 10, left: 10, right: 50),
|
||||
alignment: Alignment.topLeft,
|
||||
@@ -47,7 +45,6 @@ class ChatBubbleStyles {
|
||||
return BubbleStyle(
|
||||
nip: BubbleNip.rightBottom,
|
||||
color: seamless ? Colors.transparent : color,
|
||||
borderWidth: seamless ? 0 : 1,
|
||||
elevation: seamless ? 0 : 1,
|
||||
margin: const BubbleEdges.only(bottom: 10, right: 10, left: 50),
|
||||
alignment: Alignment.topRight,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
@@ -18,8 +19,9 @@ class WebuntisLessonSheet {
|
||||
final state = bloc.state.data;
|
||||
if (state == null) return;
|
||||
|
||||
final subject = _resolveSubject(state, lesson);
|
||||
final room = _resolveRoom(state, lesson);
|
||||
final headerSubject = _resolveSubject(state, lesson.su.firstOrNull?.id);
|
||||
final headerTitle = _firstNonEmpty([headerSubject.alternateName, headerSubject.name, headerSubject.longName, '?']);
|
||||
final headerLongName = headerSubject.longName.isNotEmpty && headerSubject.longName != headerTitle ? headerSubject.longName : '';
|
||||
|
||||
showAppointmentBottomSheet(
|
||||
context,
|
||||
@@ -28,12 +30,12 @@ class WebuntisLessonSheet {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'${_codePrefix(lesson.code)}${subject.alternateName}',
|
||||
'${_codePrefix(lesson.code)}$headerTitle',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 25),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(subject.longName),
|
||||
if (headerLongName.isNotEmpty) Text(headerLongName),
|
||||
Text(
|
||||
'${Jiffy.parseFromDateTime(appointment.startTime).format(pattern: 'HH:mm')} - '
|
||||
'${Jiffy.parseFromDateTime(appointment.endTime).format(pattern: 'HH:mm')}',
|
||||
@@ -42,68 +44,210 @@ class WebuntisLessonSheet {
|
||||
],
|
||||
),
|
||||
),
|
||||
body: (_) => SliverChildListDelegate([
|
||||
body: (_) => SliverChildListDelegate(<Widget>[
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.notifications_active),
|
||||
title: Text('Status: ${lesson.code != null ? "Geändert" : "Regulär"}'),
|
||||
title: Text('Status: ${_statusLabel(lesson.code)}'),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.room),
|
||||
title: Text('Raum: ${room.name} (${room.longName})'),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.house_outlined),
|
||||
onPressed: () => AppRoutes.openRoomplan(context),
|
||||
if (lesson.su.length > 1)
|
||||
_listTile(
|
||||
icon: Icons.book_outlined,
|
||||
label: 'Fächer',
|
||||
entries: lesson.su.map((s) {
|
||||
final resolved = _resolveSubject(state, s.id);
|
||||
return _formatLine(
|
||||
_firstNonEmpty([resolved.name, s.name, '?']),
|
||||
longname: _firstNonEmpty([resolved.longName, s.longname, '']),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.person),
|
||||
title: lesson.te.isNotEmpty
|
||||
? Text(
|
||||
'Lehrkraft: ${lesson.te[0].name}'
|
||||
'${lesson.te[0].longname.isNotEmpty ? " (${lesson.te[0].longname})" : ""}',
|
||||
)
|
||||
: const Text('?'),
|
||||
trailing: Visibility(
|
||||
visible: !kReleaseMode,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.textsms_outlined),
|
||||
onPressed: () => UnimplementedDialog.show(context),
|
||||
),
|
||||
_roomTile(context, state, lesson),
|
||||
_teacherTile(context, lesson),
|
||||
if ((lesson.activityType ?? '').trim().isNotEmpty)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.abc),
|
||||
title: Text('Typ: ${lesson.activityType}'),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.abc),
|
||||
title: Text('Typ: ${lesson.activityType}'),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.people),
|
||||
title: Text('Klasse(n): ${lesson.kl.map((e) => e.name).join(", ")}'),
|
||||
),
|
||||
if (lesson.kl.isNotEmpty)
|
||||
_listTile(
|
||||
icon: Icons.people,
|
||||
label: lesson.kl.length == 1 ? 'Klasse' : 'Klassen',
|
||||
entries: lesson.kl
|
||||
.map((k) => _formatLine(
|
||||
k.name.isNotEmpty ? k.name : '?',
|
||||
longname: k.longname,
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
..._optionalTextTiles(lesson),
|
||||
DebugTile(context).jsonData(lesson.toJson()),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
static Widget _roomTile(BuildContext context, TimetableState state, GetTimetableResponseObject lesson) {
|
||||
final trailing = IconButton(
|
||||
icon: const Icon(Icons.house_outlined),
|
||||
onPressed: () => AppRoutes.openRoomplan(context),
|
||||
);
|
||||
|
||||
if (lesson.ro.isEmpty) {
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.room),
|
||||
title: const Text('Raum: ?'),
|
||||
trailing: trailing,
|
||||
);
|
||||
}
|
||||
|
||||
final entries = lesson.ro.map((r) {
|
||||
final resolved = _resolveRoom(state, r.id);
|
||||
final name = _firstNonEmpty([resolved.name, r.name, '?']);
|
||||
final longname = _firstNonEmpty([resolved.longName, r.longname, '']);
|
||||
final building = resolved.building.trim();
|
||||
return _formatLine(
|
||||
name,
|
||||
longname: longname,
|
||||
extra: (building.isNotEmpty && building != '?') ? building : null,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
return _listTile(
|
||||
icon: Icons.room,
|
||||
label: lesson.ro.length == 1 ? 'Raum' : 'Räume',
|
||||
entries: entries,
|
||||
trailing: trailing,
|
||||
);
|
||||
}
|
||||
|
||||
static Widget _teacherTile(BuildContext context, GetTimetableResponseObject lesson) {
|
||||
final trailing = Visibility(
|
||||
visible: !kReleaseMode,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.textsms_outlined),
|
||||
onPressed: () => UnimplementedDialog.show(context),
|
||||
),
|
||||
);
|
||||
|
||||
if (lesson.te.isEmpty) {
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.person),
|
||||
title: const Text('Lehrkraft: ?'),
|
||||
trailing: trailing,
|
||||
);
|
||||
}
|
||||
|
||||
final entries = lesson.te.map((t) {
|
||||
final base = _formatLine(
|
||||
t.name.isNotEmpty ? t.name : '?',
|
||||
longname: t.longname,
|
||||
);
|
||||
final orgname = (t.orgname ?? '').trim();
|
||||
return orgname.isEmpty ? base : '$base · ehemals $orgname';
|
||||
}).toList();
|
||||
|
||||
return _listTile(
|
||||
icon: Icons.person,
|
||||
label: lesson.te.length == 1 ? 'Lehrkraft' : 'Lehrkräfte',
|
||||
entries: entries,
|
||||
trailing: trailing,
|
||||
);
|
||||
}
|
||||
|
||||
static Widget _listTile({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required List<String> entries,
|
||||
Widget? trailing,
|
||||
}) {
|
||||
if (entries.length == 1) {
|
||||
return ListTile(
|
||||
leading: Icon(icon),
|
||||
title: Text('$label: ${entries.first}'),
|
||||
trailing: trailing,
|
||||
);
|
||||
}
|
||||
return ListTile(
|
||||
leading: Icon(icon),
|
||||
title: Text(label),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: entries.map<Widget>(Text.new).toList(),
|
||||
),
|
||||
trailing: trailing,
|
||||
);
|
||||
}
|
||||
|
||||
static List<Widget> _optionalTextTiles(GetTimetableResponseObject lesson) {
|
||||
return <Widget?>[
|
||||
_textTile(Icons.info_outline, 'Info', lesson.info),
|
||||
_textTile(Icons.swap_horiz, 'Vertretungstext', lesson.substText),
|
||||
_textTile(Icons.subject, 'Stundentext', lesson.lstext),
|
||||
_textTile(Icons.category_outlined, 'Stundentyp', lesson.lstype),
|
||||
_textTile(Icons.flag_outlined, 'Statusmerkmale', lesson.statflags),
|
||||
_textTile(Icons.school_outlined, 'Lerngruppe', lesson.sg),
|
||||
_textTile(Icons.bookmark_outline, 'Buchungshinweis', lesson.bkRemark),
|
||||
_textTile(Icons.notes, 'Buchungstext', lesson.bkText),
|
||||
].whereType<Widget>().toList();
|
||||
}
|
||||
|
||||
static Widget? _textTile(IconData icon, String label, String? value) {
|
||||
final text = (value ?? '').trim();
|
||||
if (text.isEmpty) return null;
|
||||
return ListTile(
|
||||
leading: Icon(icon),
|
||||
title: Text(label),
|
||||
subtitle: Text(text),
|
||||
);
|
||||
}
|
||||
|
||||
static String _formatLine(String name, {String? longname, String? extra}) {
|
||||
final parts = <String>[
|
||||
if (name.isNotEmpty) name else '?',
|
||||
];
|
||||
final ln = (longname ?? '').trim();
|
||||
if (ln.isNotEmpty && ln != name) parts.add('($ln)');
|
||||
final ex = (extra ?? '').trim();
|
||||
if (ex.isNotEmpty) parts.add('· $ex');
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
static String _firstNonEmpty(List<String> values) {
|
||||
for (final v in values) {
|
||||
if (v.trim().isNotEmpty) return v;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
static String _statusLabel(String? code) {
|
||||
switch (code) {
|
||||
case null:
|
||||
case '':
|
||||
return 'Regulär';
|
||||
case 'cancelled':
|
||||
return 'Entfällt';
|
||||
case 'irregular':
|
||||
return 'Geändert';
|
||||
default:
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
static String _codePrefix(String? code) {
|
||||
if (code == 'cancelled') return 'Entfällt: ';
|
||||
if (code == 'irregular') return 'Änderung: ';
|
||||
return code ?? '';
|
||||
}
|
||||
|
||||
static GetSubjectsResponseObject _resolveSubject(TimetableState state, GetTimetableResponseObject lesson) {
|
||||
try {
|
||||
return state.subjects!.result.firstWhere((s) => s.id == lesson.su[0].id);
|
||||
} catch (_) {
|
||||
return GetSubjectsResponseObject(0, '?', 'Unbekannt', '?', true);
|
||||
}
|
||||
static GetSubjectsResponseObject _resolveSubject(TimetableState state, int? id) {
|
||||
final fallback = GetSubjectsResponseObject(0, '?', 'Unbekannt', '?', true);
|
||||
if (id == null) return fallback;
|
||||
return state.subjects?.result.firstWhereOrNull((s) => s.id == id) ?? fallback;
|
||||
}
|
||||
|
||||
static GetRoomsResponseObject _resolveRoom(TimetableState state, GetTimetableResponseObject lesson) {
|
||||
try {
|
||||
return state.rooms!.result.firstWhere((r) => r.id == lesson.ro[0].id);
|
||||
} catch (_) {
|
||||
return GetRoomsResponseObject(0, '?', 'Unbekannt', true, '?');
|
||||
}
|
||||
static GetRoomsResponseObject _resolveRoom(TimetableState state, int? id) {
|
||||
final fallback = GetRoomsResponseObject(0, '?', 'Unbekannt', true, '');
|
||||
if (id == null) return fallback;
|
||||
return state.rooms?.result.firstWhereOrNull((r) => r.id == id) ?? fallback;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user