From d833cdb733e5281af5903b357ea2eebe339c2839 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Elias=20M=C3=BCller?= <elias@elias-mueller.com>
Date: Sun, 9 Feb 2025 15:06:14 +0100
Subject: [PATCH] made app modules movable in their order

---
 lib/api/requestCache.dart                    |   3 +-
 lib/app.dart                                 |  29 +---
 lib/model/breakers/Breaker.dart              |   6 +-
 lib/notification/notificationController.dart |   2 +-
 lib/notification/notificationTasks.dart      |   7 +-
 lib/state/app/modules/app_modules.dart       | 118 +++++++++++++---
 lib/storage/base/settings.dart               |   3 +
 lib/storage/base/settings.g.dart             |   3 +
 lib/storage/general/modulesSettings.dart     |  19 +++
 lib/storage/general/modulesSettings.g.dart   |  35 +++++
 lib/view/pages/overhang.dart                 | 136 +++++++++++++------
 lib/view/settings/defaultSettings.dart       |  14 ++
 lib/widget/placeholderView.dart              |  43 +++---
 13 files changed, 300 insertions(+), 118 deletions(-)
 create mode 100644 lib/storage/general/modulesSettings.dart
 create mode 100644 lib/storage/general/modulesSettings.g.dart

diff --git a/lib/api/requestCache.dart b/lib/api/requestCache.dart
index 9bd9b5e..0420415 100644
--- a/lib/api/requestCache.dart
+++ b/lib/api/requestCache.dart
@@ -3,7 +3,6 @@ import 'dart:convert';
 import 'package:localstore/localstore.dart';
 
 import 'apiResponse.dart';
-import 'webuntis/webuntisError.dart';
 
 abstract class RequestCache<T extends ApiResponse?> {
   static const int cacheNothing = 0;
@@ -40,7 +39,7 @@ abstract class RequestCache<T extends ApiResponse?> {
         'json': jsonEncode(newValue),
         'lastupdate': DateTime.now().millisecondsSinceEpoch
       });
-    } on WebuntisError catch(e) {
+    } on Exception catch(e) {
       onError(e);
     }
   }
diff --git a/lib/app.dart b/lib/app.dart
index 72de706..0b4a03b 100644
--- a/lib/app.dart
+++ b/lib/app.dart
@@ -8,7 +8,6 @@ import 'package:flutter/material.dart';
 import 'state/app/modules/app_modules.dart';
 import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
 import 'package:provider/provider.dart';
-import 'package:badges/badges.dart' as badges;
 
 import 'api/mhsl/breaker/getBreakers/getBreakersResponse.dart';
 import 'api/mhsl/server/userIndex/update/updateUserindex.dart';
@@ -93,7 +92,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
   }
 
   @override
-  Widget build(BuildContext context) => PersistentTabView(
+  Widget build(BuildContext context) => Consumer<SettingsProvider>(builder: (context, settings, child) => PersistentTabView(
       controller: Main.bottomNavigator,
       navBarOverlap: const NavBarOverlap.none(),
       backgroundColor: Theme.of(context).colorScheme.primary,
@@ -101,29 +100,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
 
       screenTransitionAnimation: const ScreenTransitionAnimation(curve: Curves.easeOutQuad, duration: Duration(milliseconds: 200)),
       tabs: [
-        AppModule.getModule(Modules.timetable).toBottomTab(context),
-        AppModule.getModule(Modules.talk).toBottomTab(
-          context,
-          itemBuilder: (icon) => Consumer<ChatListProps>(
-            builder: (context, value, child) {
-              if(value.primaryLoading()) return Icon(icon);
-              var messages = value.getRoomsResponse.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: Icon(icon),
-              );
-            },
-          ),
-        ),
-        AppModule.getModule(Modules.files).toBottomTab(context),
+        ...AppModule.getBottomBarModules(context).map((e) => e.toBottomTab(context)),
 
         PersistentTabConfig(
           screen: const Breaker(breaker: BreakerArea.more, child: Overhang()),
@@ -142,7 +119,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
           color: Theme.of(context).colorScheme.surface,
         ),
       ),
-    );
+    ));
 
   @override
   void dispose() {
diff --git a/lib/model/breakers/Breaker.dart b/lib/model/breakers/Breaker.dart
index 1545296..d9ad93e 100644
--- a/lib/model/breakers/Breaker.dart
+++ b/lib/model/breakers/Breaker.dart
@@ -23,7 +23,11 @@ class _BreakerState extends State<Breaker> {
       builder: (context, value, child) {
         var blocked = value.isBlocked(widget.breaker);
         if(blocked != null) {
-          return PlaceholderView(icon: Icons.security_outlined, text: "Die App/ Dieser Bereich wurde als Schutzmaßnahme deaktiviert!\n\n${blocked.isEmpty ? "Es wurde vom Server kein Grund übermittelt." : blocked}");
+          return PlaceholderView(
+            icon: Icons.app_blocking_outlined,
+            text: 'Die App / Dieser Bereich ist zurzeit nicht verfügbar!\n\n'
+                "${blocked.isEmpty ? "Es wurde vom Server kein Grund übermittelt.\nAktualisiere die App und versuche es später erneut" : blocked}"
+          );
         }
 
         return widget.child;
diff --git a/lib/notification/notificationController.dart b/lib/notification/notificationController.dart
index af35c22..babe229 100644
--- a/lib/notification/notificationController.dart
+++ b/lib/notification/notificationController.dart
@@ -44,7 +44,7 @@ class NotificationController {
   }
 
   static Future<void> onAppOpenedByNotification(RemoteMessage message, BuildContext context) async {
-    NotificationTasks.navigateToTalk();
+    NotificationTasks.navigateToTalk(context);
     NotificationTasks.updateProviders(context);
 
     DebugTile(context).run(() {
diff --git a/lib/notification/notificationTasks.dart b/lib/notification/notificationTasks.dart
index 9ab82ec..7fb59df 100644
--- a/lib/notification/notificationTasks.dart
+++ b/lib/notification/notificationTasks.dart
@@ -6,6 +6,7 @@ import 'package:provider/provider.dart';
 import '../main.dart';
 import '../model/chatList/chatListProps.dart';
 import '../model/chatList/chatProps.dart';
+import '../state/app/modules/app_modules.dart';
 
 class NotificationTasks {
   static void updateBadgeCount(RemoteMessage notification) {
@@ -17,7 +18,9 @@ class NotificationTasks {
     Provider.of<ChatProps>(context, listen: false).run();
   }
 
-  static void navigateToTalk() {
-    Main.bottomNavigator.jumpToTab(1);
+  static void navigateToTalk(BuildContext context) {
+    var talkTab = AppModule.getBottomBarModules(context).map((e) => e.module).toList().indexOf(Modules.talk);
+    if(talkTab == -1) return;
+    Main.bottomNavigator.jumpToTab(talkTab);
   }
 }
diff --git a/lib/state/app/modules/app_modules.dart b/lib/state/app/modules/app_modules.dart
index 796cfe6..2af4ebc 100644
--- a/lib/state/app/modules/app_modules.dart
+++ b/lib/state/app/modules/app_modules.dart
@@ -1,8 +1,11 @@
 import 'package:flutter/material.dart';
 import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
+import 'package:provider/provider.dart';
 
 import '../../../api/mhsl/breaker/getBreakers/getBreakersResponse.dart';
 import '../../../model/breakers/Breaker.dart';
+import '../../../model/chatList/chatListProps.dart';
+import '../../../storage/base/settingsProvider.dart';
 import '../../../view/pages/files/files.dart';
 import '../../../view/pages/more/roomplan/roomplan.dart';
 import '../../../view/pages/talk/chatList.dart';
@@ -12,38 +15,115 @@ import 'gradeAverages/view/grade_averages_view.dart';
 import 'holidays/view/holidays_view.dart';
 import 'marianumMessage/view/marianum_message_list_view.dart';
 
+import 'package:badges/badges.dart' as badges;
+
 class AppModule {
+  Modules module;
   String name;
-  IconData icon;
+  Widget Function() icon;
+  BreakerArea breakerArea;
   Widget Function() create;
 
-  AppModule(this.name, this.icon, this.create);
+  AppModule(this.module, {required this.name, required this.icon, this.breakerArea = BreakerArea.global, required this.create});
 
-  static Map<Modules, AppModule> modules() => {
-    Modules.timetable: AppModule('Vertretung', Icons.calendar_month, Timetable.new),
-    Modules.talk: AppModule('Talk', Icons.chat, ChatList.new),
-    Modules.files: AppModule('Files', Icons.folder, Files.new),
-    Modules.marianumMessage: AppModule('Marianum Message', Icons.newspaper, MarianumMessageListView.new),
-    Modules.roomPlan: AppModule('Raumplan', Icons.location_pin, Roomplan.new),
-    Modules.gradeAveragesCalculator: AppModule('Notendurschnittsrechner', Icons.calculate, GradeAveragesView.new),
-    Modules.holidays: AppModule('Schulferien', Icons.flight, HolidaysView.new),
-  };
+  static Map<Modules, AppModule> modules(BuildContext context, { showFiltered = false }) {
+    var settings = Provider.of<SettingsProvider>(context, listen: false);
+    var available = {
+      Modules.timetable: AppModule(
+        Modules.timetable,
+        name: 'Vertretung',
+        icon: () => Icon(Icons.calendar_month),
+        breakerArea: BreakerArea.timetable,
+        create: Timetable.new,
+      ),
+      Modules.talk: AppModule(
+        Modules.talk,
+        name: 'Talk',
+        icon: () => Consumer<ChatListProps>(
+          builder: (context, value, child) {
+            if(value.primaryLoading()) return Icon(Icons.chat);
+            var messages = value.getRoomsResponse.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: Icon(Icons.chat),
+            );
+          },
+        ),
+        breakerArea: BreakerArea.talk,
+        create: ChatList.new,
+      ),
+      Modules.files: AppModule(
+        Modules.files,
+        name: 'Files',
+        icon: () => Icon(Icons.folder),
+        breakerArea: BreakerArea.files,
+        create: Files.new,
+      ),
+      Modules.marianumMessage: AppModule(
+        Modules.marianumMessage,
+        name: 'Marianum Message',
+        icon: () => Icon(Icons.newspaper),
+        breakerArea: BreakerArea.more,
+        create: MarianumMessageListView.new,
+      ),
+      Modules.roomPlan: AppModule(
+        Modules.roomPlan,
+        name: 'Raumplan',
+        icon: () => Icon(Icons.location_pin),
+        breakerArea: BreakerArea.more,
+        create: Roomplan.new,
+      ),
+      Modules.gradeAveragesCalculator: AppModule(
+        Modules.gradeAveragesCalculator,
+        name: 'Notendurschnittsrechner',
+        icon: () => Icon(Icons.calculate),
+        breakerArea: BreakerArea.more,
+        create: GradeAveragesView.new,
+      ),
+      Modules.holidays: AppModule(
+        Modules.holidays,
+        name: 'Schulferien',
+        icon: () => Icon(Icons.flight),
+        breakerArea: BreakerArea.more,
+        create: HolidaysView.new,
+      ),
+    };
 
-  static AppModule getModule(Modules module) => modules()[module]!;
+    if(!showFiltered) available.removeWhere((key, value) => settings.val().modulesSettings.hiddenModules.contains(key));
 
-  Widget toListTile(BuildContext context) => ListTile(
-    leading: CenteredLeading(Icon(icon)),
+    return { for (var element in settings.val().modulesSettings.moduleOrder.where((element) => available.containsKey(element))) element : available[element]! };
+  }
+
+  static List<AppModule> getBottomBarModules(BuildContext context) => modules(context).values.toList().getRange(0, 3).toList();
+  static List<AppModule> getOverhangModules(BuildContext context) => modules(context).values.skip(3).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: () => pushScreen(context, withNavBar: false, screen: create()),
-    trailing: const Icon(Icons.arrow_right),
+    onTap: isReorder ? null : () => pushScreen(context, withNavBar: false, screen: create()),
+    trailing: isReorder
+        ? Row(mainAxisSize: MainAxisSize.min, children: [
+            IconButton(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)? itemBuilder}) => PersistentTabConfig(
-    screen: Breaker(breaker: BreakerArea.global, child: create()),
+  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: itemBuilder == null ? Icon(icon) : itemBuilder(icon),
+        icon: icon(),
         title: name
     ),
   );
diff --git a/lib/storage/base/settings.dart b/lib/storage/base/settings.dart
index c0448d7..2914e1a 100644
--- a/lib/storage/base/settings.dart
+++ b/lib/storage/base/settings.dart
@@ -4,6 +4,7 @@ import 'package:json_annotation/json_annotation.dart';
 import '../devTools/devToolsSettings.dart';
 import '../file/fileSettings.dart';
 import '../fileView/fileViewSettings.dart';
+import '../general/modulesSettings.dart';
 import '../holidays/holidaysSettings.dart';
 import '../notification/notificationSettings.dart';
 import '../talk/talkSettings.dart';
@@ -20,6 +21,7 @@ class Settings {
   ThemeMode appTheme;
   bool devToolsEnabled;
 
+  ModulesSettings modulesSettings;
   TimetableSettings timetableSettings;
   TalkSettings talkSettings;
   FileSettings fileSettings;
@@ -31,6 +33,7 @@ class Settings {
   Settings({
     required this.appTheme,
     required this.devToolsEnabled,
+    required this.modulesSettings,
     required this.timetableSettings,
     required this.talkSettings,
     required this.fileSettings,
diff --git a/lib/storage/base/settings.g.dart b/lib/storage/base/settings.g.dart
index b8390f5..c4569e1 100644
--- a/lib/storage/base/settings.g.dart
+++ b/lib/storage/base/settings.g.dart
@@ -9,6 +9,8 @@ part of 'settings.dart';
 Settings _$SettingsFromJson(Map<String, dynamic> json) => Settings(
       appTheme: Settings._themeFromJson(json['appTheme'] as String),
       devToolsEnabled: json['devToolsEnabled'] as bool,
+      modulesSettings: ModulesSettings.fromJson(
+          json['modulesSettings'] as Map<String, dynamic>),
       timetableSettings: TimetableSettings.fromJson(
           json['timetableSettings'] as Map<String, dynamic>),
       talkSettings:
@@ -28,6 +30,7 @@ Settings _$SettingsFromJson(Map<String, dynamic> json) => Settings(
 Map<String, dynamic> _$SettingsToJson(Settings instance) => <String, dynamic>{
       'appTheme': Settings._themeToJson(instance.appTheme),
       'devToolsEnabled': instance.devToolsEnabled,
+      'modulesSettings': instance.modulesSettings.toJson(),
       'timetableSettings': instance.timetableSettings.toJson(),
       'talkSettings': instance.talkSettings.toJson(),
       'fileSettings': instance.fileSettings.toJson(),
diff --git a/lib/storage/general/modulesSettings.dart b/lib/storage/general/modulesSettings.dart
new file mode 100644
index 0000000..387982f
--- /dev/null
+++ b/lib/storage/general/modulesSettings.dart
@@ -0,0 +1,19 @@
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+import '../../state/app/modules/app_modules.dart';
+
+part 'modulesSettings.g.dart';
+
+@JsonSerializable()
+class ModulesSettings {
+  List<Modules> moduleOrder;
+  List<Modules> hiddenModules;
+
+  ModulesSettings({
+    required this.moduleOrder,
+    required this.hiddenModules
+  });
+
+  factory ModulesSettings.fromJson(Map<String, dynamic> json) => _$ModulesSettingsFromJson(json);
+  Map<String, dynamic> toJson() => _$ModulesSettingsToJson(this);
+}
diff --git a/lib/storage/general/modulesSettings.g.dart b/lib/storage/general/modulesSettings.g.dart
new file mode 100644
index 0000000..0f9f9b6
--- /dev/null
+++ b/lib/storage/general/modulesSettings.g.dart
@@ -0,0 +1,35 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'modulesSettings.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+ModulesSettings _$ModulesSettingsFromJson(Map<String, dynamic> json) =>
+    ModulesSettings(
+      moduleOrder: (json['moduleOrder'] as List<dynamic>)
+          .map((e) => $enumDecode(_$ModulesEnumMap, e))
+          .toList(),
+      hiddenModules: (json['hiddenModules'] as List<dynamic>)
+          .map((e) => $enumDecode(_$ModulesEnumMap, e))
+          .toList(),
+    );
+
+Map<String, dynamic> _$ModulesSettingsToJson(ModulesSettings instance) =>
+    <String, dynamic>{
+      'moduleOrder':
+          instance.moduleOrder.map((e) => _$ModulesEnumMap[e]!).toList(),
+      'hiddenModules':
+          instance.hiddenModules.map((e) => _$ModulesEnumMap[e]!).toList(),
+    };
+
+const _$ModulesEnumMap = {
+  Modules.timetable: 'timetable',
+  Modules.talk: 'talk',
+  Modules.files: 'files',
+  Modules.marianumMessage: 'marianumMessage',
+  Modules.roomPlan: 'roomPlan',
+  Modules.gradeAveragesCalculator: 'gradeAveragesCalculator',
+  Modules.holidays: 'holidays',
+};
diff --git a/lib/view/pages/overhang.dart b/lib/view/pages/overhang.dart
index 6269307..7b3db61 100644
--- a/lib/view/pages/overhang.dart
+++ b/lib/view/pages/overhang.dart
@@ -3,76 +3,124 @@ import 'dart:io';
 
 import 'package:flutter/material.dart';
 import 'package:in_app_review/in_app_review.dart';
+import 'package:provider/provider.dart';
 import '../../extensions/renderNotNull.dart';
 import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
 
 import '../../state/app/modules/app_modules.dart';
+import '../../storage/base/settingsProvider.dart';
 import '../../widget/centeredLeading.dart';
 import '../../widget/infoDialog.dart';
+import '../settings/defaultSettings.dart';
 import '../settings/settings.dart';
 import 'more/feedback/feedbackDialog.dart';
 import 'more/share/selectShareTypeDialog.dart';
 
-class Overhang extends StatelessWidget {
+class Overhang extends StatefulWidget {
   const Overhang({super.key});
 
   @override
-  Widget build(BuildContext context) => Scaffold(
+  State<Overhang> createState() => _OverhangState();
+}
+
+class _OverhangState extends State<Overhang> {
+  bool editMode = false;
+
+  @override
+  Widget build(BuildContext context) => Consumer<SettingsProvider>(builder: (context, settings, child) => Scaffold(
       appBar: AppBar(
         title: const Text('Mehr'),
         actions: [
-          IconButton(onPressed: () => pushScreen(context, screen: const Settings(), withNavBar: false), icon: const Icon(Icons.settings))
+          if(editMode) IconButton(
+            onPressed: settings.val().modulesSettings.toJson().toString() != DefaultSettings.get().modulesSettings.toJson().toString()
+                ? () => settings.val(write: true).modulesSettings = DefaultSettings.get().modulesSettings
+                : null,
+            icon: Icon(Icons.undo_outlined)
+          ),
+          IconButton(onPressed: () => setState(() => editMode = !editMode), icon: Icon(Icons.edit_note_outlined), color: editMode ? Theme.of(context).primaryColor : null),
+          IconButton(onPressed: editMode ? null : () => pushScreen(context, screen: const Settings(), withNavBar: false), icon: const Icon(Icons.settings)),
         ],
       ),
-      body: ListView(
-        children: [
-          AppModule.getModule(Modules.marianumMessage).toListTile(context),
-          AppModule.getModule(Modules.roomPlan).toListTile(context),
-          AppModule.getModule(Modules.gradeAveragesCalculator).toListTile(context),
-          AppModule.getModule(Modules.holidays).toListTile(context),
+      body: editMode ? _sorting()  : _overhang(),
+    ));
 
-          const Divider(),
+  Widget _sorting() => Consumer<SettingsProvider>(builder: (context, settings, child) {
+    void changeVisibility(Modules module) {
+      var hidden = settings.val(write: true).modulesSettings.hiddenModules;
+      hidden.contains(module) ? hidden.remove(module) : (hidden.length < 3 ? hidden.add(module) : null);
+    }
 
-          ListTile(
+    return ReorderableListView(
+      header: const Center(
+        heightFactor: 2,
+        child: Text('Halte und ziehe einen Eintrag, um ihn zu verschieben.\nEs können 3 Bereiche ausgeblendet werden.', textAlign: TextAlign.center)
+      ),
+      children: AppModule.modules(context, showFiltered: true)
+        .map((key, value) => MapEntry(key, value.toListTile(
+          context,
+          key: Key(key.name),
+          isReorder: true,
+          onVisibleChange: () => changeVisibility(key),
+          isVisible: !settings.val().modulesSettings.hiddenModules.contains(key)
+        )))
+        .values
+        .toList(),
+      onReorder: (oldIndex, newIndex) {
+        if (newIndex > oldIndex) newIndex -= 1;
+
+        var order = settings.val().modulesSettings.moduleOrder.toList();
+        final movedModule = order.removeAt(oldIndex);
+        order.insert(newIndex, movedModule);
+        settings.val(write: true).modulesSettings.moduleOrder = order;
+      }
+    );
+  });
+
+  Widget _overhang() => ListView(
+      children: [
+        ...AppModule.getOverhangModules(context).map((e) => e.toListTile(context)),
+
+        const Divider(),
+
+        ListTile(
             leading: const Icon(Icons.share_outlined),
             title: const Text('Teile die App'),
             subtitle: const Text('Mit Freunden und deiner Klasse teilen'),
             trailing: const Icon(Icons.arrow_right),
             onTap: () => showDialog(context: context, builder: (context) => const SelectShareTypeDialog())
-          ),
-          FutureBuilder(
-            future: InAppReview.instance.isAvailable(),
-            builder: (context, snapshot) {
-              if(!snapshot.hasData) return const SizedBox.shrink();
+        ),
+        FutureBuilder(
+          future: InAppReview.instance.isAvailable(),
+          builder: (context, snapshot) {
+            if(!snapshot.hasData) return const SizedBox.shrink();
 
-              String? getPlatformStoreName() {
-                if(Platform.isAndroid) return 'Play store';
-                if(Platform.isIOS) return 'App store';
-                return null;
-              }
+            String? getPlatformStoreName() {
+              if(Platform.isAndroid) return 'Play store';
+              if(Platform.isIOS) return 'App store';
+              return null;
+            }
 
-              return ListTile(
-                leading: const CenteredLeading(Icon(Icons.star_rate_outlined)),
-                title: const Text('App bewerten'),
-                subtitle: getPlatformStoreName().wrapNullable((data) => Text('Im $data')),
-                trailing: const Icon(Icons.arrow_right),
-                onTap: () {
-                  InAppReview.instance.openStoreListing(appStoreId: '6458789560').then(
-                          (value) => InfoDialog.show(context, 'Vielen Dank!'),
-                      onError: (error) => InfoDialog.show(context, error.toString())
-                  );
-                },
-              );
-            },
-          ),
-          ListTile(
-            leading: const CenteredLeading(Icon(Icons.feedback_outlined)),
-            title: const Text('Du hast eine Idee?'),
-            subtitle: const Text('Fehler und Verbessungsvorschläge'),
-            trailing: const Icon(Icons.arrow_right),
-            onTap: () => pushScreen(context, withNavBar: false, screen: const FeedbackDialog()),
-          ),
-        ],
-      ),
+            return ListTile(
+              leading: const CenteredLeading(Icon(Icons.star_rate_outlined)),
+              title: const Text('App bewerten'),
+              subtitle: getPlatformStoreName().wrapNullable((data) => Text('Im $data')),
+              trailing: const Icon(Icons.arrow_right),
+              onTap: () {
+                InAppReview.instance.openStoreListing(appStoreId: '6458789560').then(
+                        (value) => InfoDialog.show(context, 'Vielen Dank!'),
+                    onError: (error) => InfoDialog.show(context, error.toString())
+                );
+              },
+            );
+          },
+        ),
+        ListTile(
+          leading: const CenteredLeading(Icon(Icons.feedback_outlined)),
+          title: const Text('Du hast eine Idee?'),
+          subtitle: const Text('Fehler und Verbessungsvorschläge'),
+          trailing: const Icon(Icons.arrow_right),
+          onTap: () => pushScreen(context, withNavBar: false, screen: const FeedbackDialog()),
+        ),
+      ],
     );
 }
diff --git a/lib/view/settings/defaultSettings.dart b/lib/view/settings/defaultSettings.dart
index d4c43df..36b2289 100644
--- a/lib/view/settings/defaultSettings.dart
+++ b/lib/view/settings/defaultSettings.dart
@@ -2,10 +2,12 @@ import 'dart:io';
 
 import 'package:flutter/material.dart';
 
+import '../../state/app/modules/app_modules.dart';
 import '../../storage/base/settings.dart';
 import '../../storage/devTools/devToolsSettings.dart';
 import '../../storage/file/fileSettings.dart';
 import '../../storage/fileView/fileViewSettings.dart';
+import '../../storage/general/modulesSettings.dart';
 import '../../storage/holidays/holidaysSettings.dart';
 import '../../storage/notification/notificationSettings.dart';
 import '../../storage/talk/talkSettings.dart';
@@ -17,6 +19,18 @@ class DefaultSettings {
   static Settings get() => Settings(
       appTheme: ThemeMode.system,
       devToolsEnabled: false,
+      modulesSettings: ModulesSettings(
+        moduleOrder: [
+          Modules.timetable,
+          Modules.talk,
+          Modules.files,
+          Modules.marianumMessage,
+          Modules.roomPlan,
+          Modules.gradeAveragesCalculator,
+          Modules.holidays
+        ],
+        hiddenModules: [],
+      ),
       timetableSettings: TimetableSettings(
         connectDoubleLessons: false,
         timetableNameMode: TimetableNameMode.name
diff --git a/lib/widget/placeholderView.dart b/lib/widget/placeholderView.dart
index 8896b94..e890332 100644
--- a/lib/widget/placeholderView.dart
+++ b/lib/widget/placeholderView.dart
@@ -7,29 +7,26 @@ class PlaceholderView extends StatelessWidget {
   const PlaceholderView({super.key, required this.icon, required this.text, this.button});
 
   @override
-  Widget build(BuildContext context) => DefaultTextStyle(
-      style: const TextStyle(),
-      child: Center(
-        child: Container(
-          margin: const EdgeInsets.only(top: 100, left: 20, right: 20),
-          child: Column(
-            children: [
-              Container(
-                margin: const EdgeInsets.all(30),
-                child: Icon(icon, color: Colors.grey, size: 60),
-              ),
-              Text(text,
-                style: const TextStyle(
-                  fontSize: 20,
-                  color: Colors.grey,
-                ),
-                textAlign: TextAlign.center,
-              ),
-              const SizedBox(height: 30),
-              if(button != null) button!,
-            ],
-          ),
+  Widget build(BuildContext context) => Scaffold(
+    body: Center(
+      child: Container(
+        margin: const EdgeInsets.only(top: 100, left: 20, right: 20),
+        child: Column(
+          children: [
+            Container(
+              margin: const EdgeInsets.all(30),
+              child: Icon(icon, size: 60),
+            ),
+            Text(
+              text,
+              style: const TextStyle(fontSize: 20,),
+              textAlign: TextAlign.center,
+            ),
+            const SizedBox(height: 30),
+            if(button != null) button!,
+          ],
         ),
       ),
-    );
+    ),
+  );
 }