298 lines
9.7 KiB
Dart
298 lines
9.7 KiB
Dart
import 'package:badges/badges.dart' as badges;
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
|
|
|
|
import '../../../api/marianumconnect/queries/get_breakers/get_breakers_response.dart';
|
|
import '../../../routing/app_routes.dart';
|
|
import '../../../storage/modules_settings.dart';
|
|
import '../../../view/pages/absence_report/absence_report_view.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/chat_list.dart';
|
|
import '../../../view/pages/ticker/ticker_view.dart';
|
|
import '../../../view/pages/timetable/timetable.dart';
|
|
import '../../../widget/breaker/breaker.dart';
|
|
import '../../../widget/centered_leading.dart';
|
|
import '../infrastructure/loadable_state/loadable_state.dart';
|
|
import 'chat_list/bloc/chat_list_bloc.dart';
|
|
import 'chat_list/bloc/chat_list_state.dart';
|
|
import 'settings/bloc/settings_cubit.dart';
|
|
|
|
class AppModule {
|
|
Modules module;
|
|
String name;
|
|
Widget Function() icon;
|
|
BreakerArea breakerArea;
|
|
Widget Function() create;
|
|
|
|
AppModule(
|
|
this.module, {
|
|
required this.name,
|
|
required this.icon,
|
|
this.breakerArea = BreakerArea.global,
|
|
required this.create,
|
|
});
|
|
|
|
static Map<Modules, AppModule> modules(
|
|
BuildContext context, {
|
|
bool showFiltered = false,
|
|
}) {
|
|
final settings = context.read<SettingsCubit>();
|
|
var available = {
|
|
Modules.timetable: AppModule(
|
|
Modules.timetable,
|
|
name: 'Stundenplan',
|
|
icon: () => Icon(Icons.calendar_month),
|
|
breakerArea: BreakerArea.timetable,
|
|
create: Timetable.new,
|
|
),
|
|
Modules.ticker: AppModule(
|
|
Modules.ticker,
|
|
name: 'Ticker',
|
|
// Icons.newspaper is already taken by the "Marianum Message" module;
|
|
// use campaign to keep the two visually distinct.
|
|
icon: () => Icon(Icons.campaign),
|
|
breakerArea: BreakerArea.ticker,
|
|
create: TickerView.new,
|
|
),
|
|
Modules.talk: AppModule(
|
|
Modules.talk,
|
|
name: 'Talk',
|
|
icon: () => BlocBuilder<ChatListBloc, LoadableState<ChatListState>>(
|
|
builder: (context, state) {
|
|
final rooms = state.data?.rooms;
|
|
if (rooms == null || rooms.data.isEmpty) {
|
|
return const Icon(Icons.chat);
|
|
}
|
|
final messages = rooms.data
|
|
.map((e) => e.unreadMessages)
|
|
.reduce((a, b) => a + b);
|
|
return badges.Badge(
|
|
showBadge: messages > 0,
|
|
position: badges.BadgePosition.topEnd(top: -3, end: -3),
|
|
stackFit: StackFit.loose,
|
|
badgeStyle: badges.BadgeStyle(
|
|
padding: const EdgeInsets.all(3),
|
|
badgeColor: Theme.of(context).primaryColor,
|
|
elevation: 1,
|
|
),
|
|
badgeContent: Text(
|
|
'$messages',
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
child: const Icon(Icons.chat),
|
|
);
|
|
},
|
|
),
|
|
breakerArea: BreakerArea.talk,
|
|
create: ChatList.new,
|
|
),
|
|
Modules.files: AppModule(
|
|
Modules.files,
|
|
name: 'Dateien',
|
|
icon: () => Icon(Icons.folder),
|
|
breakerArea: BreakerArea.files,
|
|
create: Files.new,
|
|
),
|
|
Modules.marianumMessage: AppModule(
|
|
Modules.marianumMessage,
|
|
name: 'Marianum Message',
|
|
icon: () => Icon(Icons.newspaper),
|
|
breakerArea: BreakerArea.news,
|
|
create: MarianumMessageListView.new,
|
|
),
|
|
Modules.roomPlan: AppModule(
|
|
Modules.roomPlan,
|
|
name: 'Raumplan',
|
|
icon: () => Icon(Icons.location_pin),
|
|
breakerArea: BreakerArea.roomPlan,
|
|
create: Roomplan.new,
|
|
),
|
|
Modules.gradeAveragesCalculator: AppModule(
|
|
Modules.gradeAveragesCalculator,
|
|
name: 'Notendurchschnittsrechner',
|
|
icon: () => Icon(Icons.calculate),
|
|
breakerArea: BreakerArea.grades,
|
|
create: GradeAveragesView.new,
|
|
),
|
|
Modules.holidays: AppModule(
|
|
Modules.holidays,
|
|
name: 'Schulferien',
|
|
icon: () => Icon(Icons.beach_access_outlined),
|
|
breakerArea: BreakerArea.holidays,
|
|
create: HolidaysView.new,
|
|
),
|
|
Modules.marianumDates: AppModule(
|
|
Modules.marianumDates,
|
|
name: 'Marianum Termine',
|
|
icon: () => Icon(Icons.event_note),
|
|
breakerArea: BreakerArea.dates,
|
|
create: MarianumDatesView.new,
|
|
),
|
|
Modules.absenceReport: AppModule(
|
|
Modules.absenceReport,
|
|
name: 'Krankmeldung',
|
|
icon: () => Icon(Icons.sick_outlined),
|
|
create: AbsenceReportView.new,
|
|
),
|
|
};
|
|
|
|
if (!showFiltered) {
|
|
available.removeWhere(
|
|
(key, value) =>
|
|
settings.val().modulesSettings.hiddenModules.contains(key),
|
|
);
|
|
}
|
|
|
|
return {
|
|
for (var element in effectiveModuleOrder(
|
|
settings.val().modulesSettings,
|
|
).where((element) => available.containsKey(element)))
|
|
element: available[element]!,
|
|
};
|
|
}
|
|
|
|
// A persisted moduleOrder predates modules added in newer app versions.
|
|
// Deriving the effective order at read time (instead of relying on a heal
|
|
// during hydration) makes it impossible for a module to vanish from the
|
|
// bottom bar, the "Mehr" menu and the settings list at once. Missing
|
|
// modules are inserted right after their closest declared predecessor so
|
|
// they land at their default position (e.g. the ticker between Stundenplan
|
|
// and Talk) instead of at the end of the "Mehr" menu.
|
|
static List<Modules> effectiveModuleOrder(ModulesSettings settings) {
|
|
final seen = <Modules>{};
|
|
final order = [
|
|
for (final m in settings.moduleOrder)
|
|
if (seen.add(m)) m,
|
|
];
|
|
for (final missing in Modules.values) {
|
|
if (!seen.add(missing)) continue;
|
|
var insertAt = 0;
|
|
for (final predecessor in Modules.values.takeWhile(
|
|
(m) => m != missing,
|
|
)) {
|
|
final pos = order.indexOf(predecessor);
|
|
if (pos >= insertAt) insertAt = pos + 1;
|
|
}
|
|
order.insert(insertAt, missing);
|
|
}
|
|
return order;
|
|
}
|
|
|
|
// The settings list displays a capability-filtered subset, so reorder
|
|
// indices refer to that subset; the move is applied there and merged back
|
|
// into the full order (non-displayed modules keep their relative slots).
|
|
static List<Modules> reorderModuleOrder({
|
|
required List<Modules> displayed,
|
|
required List<Modules> effective,
|
|
required int oldIndex,
|
|
required int newIndex,
|
|
}) {
|
|
final moved = List<Modules>.from(displayed);
|
|
moved.insert(newIndex, moved.removeAt(oldIndex));
|
|
final displayedSet = displayed.toSet();
|
|
var moveIndex = 0;
|
|
return [
|
|
for (final m in effective)
|
|
displayedSet.contains(m) ? moved[moveIndex++] : m,
|
|
];
|
|
}
|
|
|
|
static const int minBottomBarSlots = 3;
|
|
static const int maxBottomBarSlots = 5;
|
|
|
|
static int resolveBottomBarSlotCount(BuildContext context) {
|
|
final settings = context.read<SettingsCubit>().val().modulesSettings;
|
|
final available = modules(context).length;
|
|
|
|
int desired;
|
|
if (settings.autoFillBottomBar) {
|
|
// 4 content tabs so the bar shows 5 entries including "Mehr" — enough
|
|
// for the Ticker module without pushing "Dateien" into the overflow.
|
|
desired = 4;
|
|
} else {
|
|
desired = settings.fixedBottomBarSlots;
|
|
}
|
|
|
|
desired = desired.clamp(minBottomBarSlots, maxBottomBarSlots);
|
|
return desired.clamp(0, available);
|
|
}
|
|
|
|
static List<AppModule> getBottomBarModules(BuildContext context) {
|
|
final all = modules(context).values.toList();
|
|
final slots = resolveBottomBarSlotCount(context);
|
|
return all.take(slots).toList();
|
|
}
|
|
|
|
static List<AppModule> getOverhangModules(BuildContext context) {
|
|
final all = modules(context).values.toList();
|
|
final slots = resolveBottomBarSlotCount(context);
|
|
return all.skip(slots).toList();
|
|
}
|
|
|
|
Widget toListTile(
|
|
BuildContext context, {
|
|
Key? key,
|
|
bool isReorder = false,
|
|
Function()? onVisibleChange,
|
|
bool isVisible = true,
|
|
}) => ListTile(
|
|
key: key,
|
|
leading: CenteredLeading(icon()),
|
|
title: Text(name),
|
|
onTap: isReorder ? null : () => AppRoutes.openModule(context, this),
|
|
trailing: isReorder
|
|
? Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
IconButton(
|
|
tooltip: isVisible ? 'Modul ausblenden' : 'Modul einblenden',
|
|
onPressed: onVisibleChange,
|
|
icon: Icon(
|
|
isVisible
|
|
? Icons.visibility_outlined
|
|
: Icons.visibility_off_outlined,
|
|
),
|
|
),
|
|
Icon(Icons.drag_handle_outlined),
|
|
],
|
|
)
|
|
: const Icon(Icons.arrow_right),
|
|
);
|
|
|
|
PersistentTabConfig toBottomTab(
|
|
BuildContext context, {
|
|
Widget Function(IconData icon)? iconBuilder,
|
|
}) => PersistentTabConfig(
|
|
screen: Breaker(breaker: breakerArea, child: create()),
|
|
item: ItemConfig(
|
|
activeForegroundColor: Theme.of(context).primaryColor,
|
|
inactiveForegroundColor: Theme.of(context).colorScheme.secondary,
|
|
icon: icon(),
|
|
title: name,
|
|
),
|
|
);
|
|
}
|
|
|
|
enum Modules {
|
|
timetable,
|
|
ticker,
|
|
talk,
|
|
files,
|
|
marianumMessage,
|
|
roomPlan,
|
|
gradeAveragesCalculator,
|
|
holidays,
|
|
marianumDates,
|
|
absenceReport,
|
|
}
|