folder restructuring

This commit is contained in:
2026-05-05 21:44:23 +02:00
parent db9c3386f1
commit 4f796dac2e
102 changed files with 1254 additions and 879 deletions
@@ -3,7 +3,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../widget/infoDialog.dart';
import '../../../../../widget/info_dialog.dart';
import '../bloc/loadable_state_bloc.dart';
class LoadableStateErrorBar extends StatelessWidget {
+12 -11
View File
@@ -2,23 +2,24 @@ import 'package:flutter/material.dart';
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:badges/badges.dart' as badges;
import '../../../api/mhsl/breaker/getBreakers/getBreakersResponse.dart';
import '../../../widget/breaker/breaker.dart';
import '../../../routing/app_routes.dart';
import '../../../view/pages/files/files.dart';
import '../../../view/pages/grade_averages/grade_averages_view.dart';
import '../../../view/pages/holidays/holidays_view.dart';
import '../../../view/pages/marianum_dates/marianum_dates_view.dart';
import '../../../view/pages/marianum_message/marianum_message_list_view.dart';
import '../../../view/pages/more/roomplan/roomplan.dart';
import '../../../view/pages/talk/chatList.dart';
import '../../../view/pages/talk/chat_list.dart';
import '../../../view/pages/timetable/timetable.dart';
import '../../../widget/centeredLeading.dart';
import '../../../widget/breaker/breaker.dart';
import '../../../widget/centered_leading.dart';
import '../infrastructure/loadableState/loadable_state.dart';
import 'chatList/bloc/chat_list_bloc.dart';
import 'chatList/bloc/chat_list_state.dart';
import 'settings/bloc/settings_cubit.dart';
import '../infrastructure/loadableState/loadable_state.dart';
import 'gradeAverages/view/grade_averages_view.dart';
import 'holidays/view/holidays_view.dart';
import 'marianumDates/view/marianum_dates_view.dart';
import 'marianumMessage/view/marianum_message_list_view.dart';
import 'package:badges/badges.dart' as badges;
class AppModule {
Modules module;
@@ -120,7 +121,7 @@ class AppModule {
key: key,
leading: CenteredLeading(icon()),
title: Text(name),
onTap: isReorder ? null : () => pushScreen(context, withNavBar: false, screen: create()),
onTap: isReorder ? null : () => AppRoutes.openModule(context, this),
trailing: isReorder
? Row(mainAxisSize: MainAxisSize.min, children: [
IconButton(onPressed: onVisibleChange, icon: Icon(isVisible ? Icons.visibility_outlined : Icons.visibility_off_outlined)),
@@ -1,70 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../bloc/grade_averages_bloc.dart';
import '../bloc/grade_averages_event.dart';
class GradeAveragesListView extends StatelessWidget {
const GradeAveragesListView({super.key});
@override
Widget build(BuildContext context) {
var bloc = context.watch<GradeAveragesBloc>();
String getGradeDisplay(int grade) {
if(bloc.isMiddleSchool()) {
return 'Note $grade';
} else {
return "$grade Punkt${grade > 1 ? "e" : ""}";
}
}
return ListView.builder(
itemCount: bloc.gradesInGradingSystem(),
itemBuilder: (context, index) {
var grade = bloc.getGradeFromIndex(index);
return Material(
child: ListTile(
tileColor: grade.isEven ? Colors.transparent : Colors.transparent.withAlpha(50),
title: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(getGradeDisplay(grade)),
const SizedBox(width: 30),
IconButton(
onPressed: () {
bloc.add(DecrementGrade(grade));
},
icon: const Icon(Icons.remove),
color: Theme.of(context).colorScheme.onSurface,
),
Text('${bloc.countOfGrade(grade)}', style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold)),
IconButton(
onPressed: () {
bloc.add(IncrementGrade(grade));
},
icon: const Icon(Icons.add),
color: Theme.of(context).colorScheme.onSurface,
),
],
),
),
trailing: Visibility(
maintainState: true,
maintainAnimation: true,
maintainSize: true,
visible: bloc.canDecrementOrDelete(grade),
child: IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
bloc.add(ResetGrade(grade));
},
),
),
),
);
},
);
}
}
@@ -1,106 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../widget/confirmDialog.dart';
import '../bloc/grade_averages_bloc.dart';
import '../bloc/grade_averages_event.dart';
import '../bloc/grade_averages_state.dart';
import 'grade_averages_list_view.dart';
class GradeAveragesView extends StatelessWidget {
const GradeAveragesView({super.key});
@override
Widget build(BuildContext context) => BlocProvider<GradeAveragesBloc>(
create: (context) => GradeAveragesBloc(),
child: BlocBuilder<GradeAveragesBloc, GradeAveragesState>(
builder: (context, state) {
var bloc = context.watch<GradeAveragesBloc>();
return Scaffold(
appBar: AppBar(
title: const Text('Notendurschnittsrechner'),
actions: [
Visibility(
visible: bloc.state.grades.isNotEmpty,
child: IconButton(
onPressed: () {
showDialog(
context: context,
builder: (context) => ConfirmDialog(
title: 'Zurücksetzen?',
content: 'Alle Einträge werden entfernt.',
confirmButton: 'Zurücksetzen',
onConfirm: () {
bloc.add(ResetAll());
},
),
);
},
icon: const Icon(Icons.delete_forever)),
),
PopupMenuButton<bool>(
initialValue: bloc.isMiddleSchool(),
icon: const Icon(Icons.more_horiz),
itemBuilder: (context) => [true, false].map((isMiddleSchool) => PopupMenuItem<bool>(
value: isMiddleSchool,
child: Row(
children: [
Icon(
isMiddleSchool ? Icons.calculate_outlined : Icons.school_outlined,
color: Theme.of(context).colorScheme.onSurface
),
const SizedBox(width: 15),
Text(isMiddleSchool ? 'Realschule' : 'Oberstufe'),
],
),
)).toList(),
onSelected: (isMiddleSchool) {
if (bloc.state.grades.isNotEmpty) {
showDialog(
context: context,
builder: (context) => ConfirmDialog(
title: 'Notensystem wechseln',
content:
'Beim Wechsel des Notensystems werden alle Einträge zurückgesetzt.',
confirmButton: 'Fortfahren',
onConfirm: () => bloc.add(GradingSystemChanged(isMiddleSchool)),
),
);
} else {
bloc.add(GradingSystemChanged(isMiddleSchool));
}
},
),
],
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 30),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Ø', style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold)),
SizedBox(width: 5),
Text(bloc.average().toStringAsFixed(2), style: const TextStyle(fontSize: 30, fontWeight: FontWeight.bold))
],
),
const SizedBox(height: 10),
const Divider(),
const SizedBox(height: 10),
Text(bloc.isMiddleSchool() ? 'Wähle die Anzahl deiner jeweiligen Noten aus' : 'Wähle die Anzahl deiner jeweiligen Punkte aus'),
const SizedBox(height: 10),
const Expanded(
child: GradeAveragesListView()
),
],
),
);
},
),
);
}
@@ -1,119 +0,0 @@
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import '../../../../../widget/animatedTime.dart';
import '../../../../../widget/list_view_util.dart';
import '../../../../../widget/centeredLeading.dart';
import '../../../../../widget/debug/debugTile.dart';
import '../../../../../widget/string_extensions.dart';
import '../../../infrastructure/loadableState/loadable_state.dart';
import '../../../infrastructure/loadableState/view/loadable_state_consumer.dart';
import '../../../infrastructure/utilityWidgets/bloc_module.dart';
import '../bloc/holidays_bloc.dart';
import '../bloc/holidays_event.dart';
import '../bloc/holidays_state.dart';
class HolidaysView extends StatelessWidget {
const HolidaysView({super.key});
@override
Widget build(BuildContext context) => BlocModule<HolidaysBloc, LoadableState<HolidaysState>>(
create: (context) => HolidaysBloc(),
autoRebuild: true,
child: (context, bloc, state) {
void showDisclaimer() {
showDialog(context: context, builder: (context) => AlertDialog(
title: const Text('Richtigkeit und Bereitstellung der Daten'),
content: const Text(''
'Sämtliche Datumsangaben sind ohne Gewähr.\n'
'Ich übernehme weder Verantwortung für die Richtigkeit der Daten noch hafte ich für wirtschaftliche Schäden die aus der Verwendung dieser Daten entstehen können.\n\n'
'Die Daten stammen von https://ferien-api.de/'),
actions: [
TextButton(child: const Text('Okay'), onPressed: () => Navigator.of(context).pop()),
],
));
}
return Scaffold(
appBar: AppBar(
title: const Text('Schulferien in Hessen'),
actions: [
IconButton(
icon: const Icon(Icons.info_outline),
onPressed: showDisclaimer,
),
PopupMenuButton<bool>(
initialValue: bloc.showPastHolidays(),
icon: const Icon(Icons.history),
itemBuilder: (context) => [true, false].map((e) => PopupMenuItem<bool>(
value: e,
enabled: e != bloc.showPastHolidays(),
child: Row(
children: [
Icon(e ? Icons.history_outlined : Icons.history_toggle_off_outlined, color: Theme.of(context).colorScheme.onSurface),
const SizedBox(width: 15),
Text(e ? 'Alle anzeigen' : 'Nur zukünftige anzeigen')
],
)
)).toList(),
onSelected: (e) => bloc.add(SetPastHolidaysVisible(e)),
),
],
),
body: LoadableStateConsumer<HolidaysBloc, HolidaysState>(
onLoad: (state) {
if(state.showDisclaimer) showDisclaimer();
bloc.add(DisclaimerDismissed());
},
child: (state, loading) => ListViewUtil.fromList<Holiday>(bloc.getHolidays(), (holiday) {
var holidayType = holiday.name.split(' ').first.capitalize();
String formatDate(String date) => Jiffy.parse(date).format(pattern: 'dd.MM.yyyy');
String getYear(String date, {String format = 'yyyy'}) => Jiffy.parse(date).format(pattern: format);
String getHolidayYear(String startDate, String endDate) => getYear(startDate) == getYear(endDate)
? getYear(startDate)
: '${getYear(startDate)}/${getYear(endDate, format: 'yy')}';
return ListTile(
leading: const CenteredLeading(Icon(Icons.calendar_month)),
title: Text('$holidayType ${getHolidayYear(holiday.start, holiday.end)}'),
subtitle: Text('${formatDate(holiday.start)} - ${formatDate(holiday.end)}'),
onTap: () => showDialog(context: context, builder: (context) => SimpleDialog(
title: Text('$holidayType ${holiday.year} in Hessen'),
children: [
ListTile(
leading: const CenteredLeading(Icon(Icons.signpost_outlined)),
title: Text(holiday.name.capitalize()),
subtitle: Text(holiday.slug.capitalize()),
),
ListTile(
leading: const Icon(Icons.date_range_outlined),
title: Text('vom ${formatDate(holiday.start)}'),
),
ListTile(
leading: const Icon(Icons.date_range_outlined),
title: Text('bis zum ${formatDate(holiday.end)}'),
),
Visibility(
visible: !DateTime.parse(holiday.start).difference(DateTime.now()).isNegative,
replacement: ListTile(
leading: const CenteredLeading(Icon(Icons.content_paste_search_outlined)),
title: Text(Jiffy.parse(holiday.start).fromNow()),
),
child: ListTile(
leading: const CenteredLeading(Icon(Icons.timer_outlined)),
title: AnimatedTime(callback: () => DateTime.parse(holiday.start).difference(DateTime.now())),
subtitle: Text(Jiffy.parse(holiday.start).fromNow()),
),
),
DebugTile(context).jsonData(holiday.toJson()),
],
)),
trailing: const Icon(Icons.arrow_right),
);
}),
),
);
},
);
}
@@ -1,135 +0,0 @@
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import '../../../../../view/pages/timetable/custom_events/custom_event_edit_dialog.dart';
import '../../../../../widget/animatedTime.dart';
import '../../../../../widget/centeredLeading.dart';
import '../../../../../widget/debug/debugTile.dart';
import '../../../../../widget/list_view_util.dart';
import '../../../infrastructure/loadableState/view/loadable_state_consumer.dart';
import '../../../infrastructure/utilityWidgets/bloc_module.dart';
import '../../../infrastructure/loadableState/loadable_state.dart';
import '../bloc/marianum_dates_bloc.dart';
import '../bloc/marianum_dates_event.dart';
import '../bloc/marianum_dates_state.dart';
class MarianumDatesView extends StatelessWidget {
const MarianumDatesView({super.key});
@override
Widget build(BuildContext context) => BlocModule<MarianumDatesBloc, LoadableState<MarianumDatesState>>(
create: (context) => MarianumDatesBloc(),
autoRebuild: true,
child: (context, bloc, state) => Scaffold(
appBar: AppBar(
title: const Text('Marianum Termine'),
actions: [
PopupMenuButton<bool>(
initialValue: bloc.showPastEvents(),
icon: const Icon(Icons.history),
itemBuilder: (context) => [true, false].map((e) => PopupMenuItem<bool>(
value: e,
enabled: e != bloc.showPastEvents(),
child: Row(
children: [
Icon(e ? Icons.history_outlined : Icons.history_toggle_off_outlined, color: Theme.of(context).colorScheme.onSurface),
const SizedBox(width: 15),
Text(e ? 'Alle anzeigen' : 'Nur zukünftige anzeigen'),
],
),
)).toList(),
onSelected: (e) => bloc.add(SetPastEventsVisible(e)),
),
],
),
body: LoadableStateConsumer<MarianumDatesBloc, MarianumDatesState>(
child: (state, loading) => ListViewUtil.fromList<MarianumDate>(bloc.getEvents(), (event) => _MarianumDateTile(event: event)),
),
),
);
}
class _MarianumDateTile extends StatelessWidget {
final MarianumDate event;
const _MarianumDateTile({required this.event});
String _formatSubtitle() {
final start = Jiffy.parseFromDateTime(event.start);
final end = Jiffy.parseFromDateTime(event.end);
if (event.isAllDay) {
// iCal end is exclusive for multi-day all-day events. The feed sets
// DTSTART == DTEND for single-day all-day events, so only subtract a
// day when end actually advances past start.
final inclusiveEnd = event.end.isAfter(event.start) ? end.subtract(days: 1) : end;
final sameAllDay = start.format(pattern: 'yyyy-MM-dd') == inclusiveEnd.format(pattern: 'yyyy-MM-dd');
return sameAllDay
? '${start.format(pattern: 'dd.MM.yyyy')} · Ganztägig'
: '${start.format(pattern: 'dd.MM.yyyy')} ${inclusiveEnd.format(pattern: 'dd.MM.yyyy')} · Ganztägig';
}
final sameDay = start.format(pattern: 'yyyy-MM-dd') == end.format(pattern: 'yyyy-MM-dd');
if (sameDay) {
if (event.start == event.end) {
return '${start.format(pattern: 'dd.MM.yyyy')} · ${start.format(pattern: 'HH:mm')}';
}
return '${start.format(pattern: 'dd.MM.yyyy')} · ${start.format(pattern: 'HH:mm')} ${end.format(pattern: 'HH:mm')}';
}
return '${start.format(pattern: 'dd.MM.yyyy HH:mm')} ${end.format(pattern: 'dd.MM.yyyy HH:mm')}';
}
@override
Widget build(BuildContext context) => ListTile(
leading: const CenteredLeading(Icon(Icons.event)),
title: Text(event.title.isEmpty ? '(ohne Titel)' : event.title),
subtitle: Text(_formatSubtitle()),
onTap: () => _showDetails(context),
trailing: IconButton(
icon: const Icon(Icons.add_circle_outline),
tooltip: 'In Stundenplan übernehmen',
onPressed: () => showDialog(
context: context,
builder: (_) => CustomEventEditDialog(
initialTitle: event.title,
initialDescription: event.description,
initialStart: event.start,
initialEnd: event.end,
),
barrierDismissible: false,
),
),
);
void _showDetails(BuildContext context) {
showDialog(
context: context,
builder: (context) => SimpleDialog(
title: Text(event.title.isEmpty ? '(ohne Titel)' : event.title),
children: [
ListTile(
leading: const CenteredLeading(Icon(Icons.date_range_outlined)),
title: Text(_formatSubtitle()),
),
if (event.description != null && event.description!.trim().isNotEmpty)
ListTile(
leading: const CenteredLeading(Icon(Icons.notes_outlined)),
title: Text(event.description!.trim()),
),
Visibility(
visible: !event.start.difference(DateTime.now()).isNegative,
replacement: ListTile(
leading: const CenteredLeading(Icon(Icons.content_paste_search_outlined)),
title: Text(Jiffy.parseFromDateTime(event.start).fromNow()),
),
child: ListTile(
leading: const CenteredLeading(Icon(Icons.timer_outlined)),
title: AnimatedTime(callback: () => event.start.difference(DateTime.now())),
subtitle: Text(Jiffy.parseFromDateTime(event.start).fromNow()),
),
),
DebugTile(context).jsonData(event.toJson()),
],
),
);
}
}
@@ -1,42 +0,0 @@
import 'package:flutter/material.dart';
import 'marianum_message_view.dart';
import '../../../infrastructure/loadableState/loadable_state.dart';
import '../../../infrastructure/loadableState/view/loadable_state_consumer.dart';
import '../../../infrastructure/utilityWidgets/bloc_module.dart';
import '../bloc/marianum_message_bloc.dart';
import '../bloc/marianum_message_state.dart';
class MarianumMessageListView extends StatelessWidget {
const MarianumMessageListView({super.key});
@override
Widget build(BuildContext context) => BlocModule<MarianumMessageBloc, LoadableState<MarianumMessageState>>(
create: (context) => MarianumMessageBloc(),
child: (context, bloc, state) => Scaffold(
appBar: AppBar(
title: const Text('Marianum Message'),
),
body: LoadableStateConsumer<MarianumMessageBloc, MarianumMessageState>(
child: (state, loading) => ListView.builder(
itemCount: state.messageList.messages.length,
itemBuilder: (context, index) {
var message = state.messageList.messages.toList()[index];
return ListTile(
leading: const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [Icon(Icons.newspaper)],
),
title: Text(message.name, overflow: TextOverflow.ellipsis),
subtitle: Text('vom ${message.date}'),
trailing: const Icon(Icons.arrow_right),
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => MessageView(basePath: state.messageList.base, message: message)));
},
);
}
),
),
)
);
}
@@ -1,52 +0,0 @@
import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart';
import 'package:url_launcher/url_launcher.dart';
import '../bloc/marianum_message_state.dart';
import '../../../../../widget/confirmDialog.dart';
class MessageView extends StatefulWidget {
final String basePath;
final MarianumMessage message;
const MessageView({super.key, required this.basePath, required this.message});
@override
State<MessageView> createState() => _MessageViewState();
}
class _MessageViewState extends State<MessageView> {
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: Text(widget.message.name),
),
body: SfPdfViewer.network(
widget.basePath + widget.message.url,
enableHyperlinkNavigation: true,
onDocumentLoadFailed: (PdfDocumentLoadFailedDetails e) {
Navigator.of(context).pop();
showDialog(context: context, builder: (context) => AlertDialog(
title: const Text('Fehler beim öffnen'),
content: Text("Dokument '${widget.message.name}' konnte nicht geladen werden:\n${e.description}"),
actions: [
TextButton(onPressed: () {
Navigator.of(context).pop();
}, child: const Text('Ok'))
],
));
},
onHyperlinkClicked: (PdfHyperlinkClickedDetails e) {
showDialog(
context: context,
builder: (context) => ConfirmDialog(
title: 'Link öffnen',
content: 'Möchtest du den folgenden Link öffnen?\n${e.uri}',
confirmButton: 'Öffnen',
onConfirm: () => launchUrl(Uri.parse(e.uri), mode: LaunchMode.externalApplication),
),
);
},
),
);
}
@@ -4,7 +4,7 @@ import 'package:easy_debounce/easy_debounce.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import '../../../../../storage/base/settings.dart';
import '../../../../../view/settings/defaultSettings.dart';
import '../../../../../view/pages/settings/data/default_settings.dart';
import '../../app_modules.dart';
class SettingsCubit extends HydratedCubit<Settings> {
@@ -20,7 +20,7 @@ import '../../../../../api/webuntis/queries/getTimegridUnits/getTimegridUnitsCac
import '../../../../../api/webuntis/queries/getTimegridUnits/getTimegridUnitsResponse.dart';
import '../../../../../api/webuntis/queries/getTimetable/getTimetableCache.dart';
import '../../../../../api/webuntis/queries/getTimetable/getTimetableResponse.dart';
import '../../../../../model/accountData.dart';
import '../../../../../model/account_data.dart';
class TimetableDataProvider {
static final DateFormat _dateFormat = DateFormat('yyyyMMdd');