From 85639c5c8f5d664190246a2ac61d503b840acad6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Beno=C3=AEt=20Harrault?= <benoit@harrault.fr>
Date: Thu, 2 May 2024 21:51:56 +0200
Subject: [PATCH] Add "settings" page with dart/light theme selector

---
 android/gradle.properties                     |   4 +-
 assets/translations/en.json                   |   8 +-
 assets/translations/fr.json                   |   8 +-
 .../metadata/android/en-US/changelogs/70.txt  |   1 +
 .../metadata/android/fr-FR/changelogs/70.txt  |   1 +
 lib/config/menu.dart                          |  48 +++++
 lib/cubit/nav_cubit.dart                      |  37 ++++
 lib/cubit/theme_cubit.dart                    |  31 +++
 lib/cubit/theme_state.dart                    |  15 ++
 lib/main.dart                                 |  33 ++--
 lib/ui/layout/board.dart                      |   2 +-
 lib/ui/screens/game_page.dart                 |  26 +++
 lib/ui/screens/screen_game.dart               |  34 ----
 lib/ui/screens/settings_page.dart             |  26 +++
 lib/ui/skeleton.dart                          |  25 +--
 lib/ui/widgets/cell_widget.dart               |   4 +-
 lib/ui/widgets/cell_widget_update.dart        |   2 +-
 lib/ui/widgets/game.dart                      |  37 ++++
 lib/ui/widgets/global_app_bar.dart            | 187 ++++++++++--------
 lib/ui/widgets/header_app.dart                |  24 +++
 .../parameters.dart}                          |   6 +-
 lib/ui/widgets/settings/settings_form.dart    |  63 ++++++
 lib/ui/widgets/settings/theme_card.dart       |  47 +++++
 pubspec.lock                                  |  24 +--
 pubspec.yaml                                  |   9 +-
 25 files changed, 528 insertions(+), 174 deletions(-)
 create mode 100644 fastlane/metadata/android/en-US/changelogs/70.txt
 create mode 100644 fastlane/metadata/android/fr-FR/changelogs/70.txt
 create mode 100644 lib/config/menu.dart
 create mode 100644 lib/cubit/nav_cubit.dart
 create mode 100644 lib/cubit/theme_cubit.dart
 create mode 100644 lib/cubit/theme_state.dart
 create mode 100644 lib/ui/screens/game_page.dart
 delete mode 100644 lib/ui/screens/screen_game.dart
 create mode 100644 lib/ui/screens/settings_page.dart
 create mode 100644 lib/ui/widgets/game.dart
 create mode 100644 lib/ui/widgets/header_app.dart
 rename lib/ui/{screens/screen_parameters.dart => widgets/parameters.dart} (96%)
 create mode 100644 lib/ui/widgets/settings/settings_form.dart
 create mode 100644 lib/ui/widgets/settings/theme_card.dart

diff --git a/android/gradle.properties b/android/gradle.properties
index 6da1d1a..4f6eb84 100644
--- a/android/gradle.properties
+++ b/android/gradle.properties
@@ -1,5 +1,5 @@
 org.gradle.jvmargs=-Xmx1536M
 android.useAndroidX=true
 android.enableJetifier=true
-app.versionName=0.1.20
-app.versionCode=69
+app.versionName=0.1.21
+app.versionCode=70
diff --git a/assets/translations/en.json b/assets/translations/en.json
index a815023..0047e08 100644
--- a/assets/translations/en.json
+++ b/assets/translations/en.json
@@ -1,3 +1,9 @@
 {
-  "app_name": "Sudoku"
+  "app_name": "Sudoku",
+
+  "bottom_nav_game": "Game",
+  "bottom_nav_settings": "Settings",
+
+  "settings_title": "Settings",
+  "settings_label_theme": "Theme mode"
 }
diff --git a/assets/translations/fr.json b/assets/translations/fr.json
index a815023..80f4637 100644
--- a/assets/translations/fr.json
+++ b/assets/translations/fr.json
@@ -1,3 +1,9 @@
 {
-  "app_name": "Sudoku"
+  "app_name": "Sudoku",
+
+  "bottom_nav_game": "Jeu",
+  "bottom_nav_settings": "Réglages",
+
+  "settings_title": "Réglages",
+  "settings_label_theme": "Thème de couleurs"
 }
diff --git a/fastlane/metadata/android/en-US/changelogs/70.txt b/fastlane/metadata/android/en-US/changelogs/70.txt
new file mode 100644
index 0000000..1a4f9ce
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/70.txt
@@ -0,0 +1 @@
+Add "settings" page with dark/light theme selector.
diff --git a/fastlane/metadata/android/fr-FR/changelogs/70.txt b/fastlane/metadata/android/fr-FR/changelogs/70.txt
new file mode 100644
index 0000000..269e6de
--- /dev/null
+++ b/fastlane/metadata/android/fr-FR/changelogs/70.txt
@@ -0,0 +1 @@
+Ajout d'une page de réglages avec sélection du thème clair ou sombre.
diff --git a/lib/config/menu.dart b/lib/config/menu.dart
new file mode 100644
index 0000000..a21c528
--- /dev/null
+++ b/lib/config/menu.dart
@@ -0,0 +1,48 @@
+import 'package:flutter/material.dart';
+import 'package:unicons/unicons.dart';
+
+import 'package:sudoku/ui/screens/game_page.dart';
+import 'package:sudoku/ui/screens/settings_page.dart';
+
+class MenuItem {
+  final String code;
+  final Icon icon;
+  final Widget page;
+
+  const MenuItem({
+    required this.code,
+    required this.icon,
+    required this.page,
+  });
+}
+
+class Menu {
+  static const indexGame = 0;
+  static const menuItemGame = MenuItem(
+    code: 'bottom_nav_game',
+    icon: Icon(UniconsLine.home),
+    page: GamePage(),
+  );
+
+  static const indexSettings = 1;
+  static const menuItemSettings = MenuItem(
+    code: 'bottom_nav_settings',
+    icon: Icon(UniconsLine.setting),
+    page: SettingsPage(),
+  );
+
+  static Map<int, MenuItem> items = {
+    indexGame: menuItemGame,
+    indexSettings: menuItemSettings,
+  };
+
+  static bool isIndexAllowed(int pageIndex) {
+    return items.keys.contains(pageIndex);
+  }
+
+  static Widget getPageWidget(int pageIndex) {
+    return items[pageIndex]?.page ?? menuItemGame.page;
+  }
+
+  static int itemsCount = Menu.items.length;
+}
diff --git a/lib/cubit/nav_cubit.dart b/lib/cubit/nav_cubit.dart
new file mode 100644
index 0000000..845ecc2
--- /dev/null
+++ b/lib/cubit/nav_cubit.dart
@@ -0,0 +1,37 @@
+import 'package:hydrated_bloc/hydrated_bloc.dart';
+
+import 'package:sudoku/config/menu.dart';
+
+class NavCubit extends HydratedCubit<int> {
+  NavCubit() : super(0);
+
+  void updateIndex(int index) {
+    if (Menu.isIndexAllowed(index)) {
+      emit(index);
+    } else {
+      goToGamePage();
+    }
+  }
+
+  void goToGamePage() {
+    emit(Menu.indexGame);
+  }
+
+  void switchToSettingsPage() {
+    if (state != Menu.indexSettings) {
+      emit(Menu.indexSettings);
+    } else {
+      goToGamePage();
+    }
+  }
+
+  @override
+  int fromJson(Map<String, dynamic> json) {
+    return Menu.indexGame;
+  }
+
+  @override
+  Map<String, dynamic>? toJson(int state) {
+    return <String, int>{'pageIndex': state};
+  }
+}
diff --git a/lib/cubit/theme_cubit.dart b/lib/cubit/theme_cubit.dart
new file mode 100644
index 0000000..b793e89
--- /dev/null
+++ b/lib/cubit/theme_cubit.dart
@@ -0,0 +1,31 @@
+import 'package:equatable/equatable.dart';
+import 'package:flutter/material.dart';
+import 'package:hydrated_bloc/hydrated_bloc.dart';
+
+part 'theme_state.dart';
+
+class ThemeCubit extends HydratedCubit<ThemeModeState> {
+  ThemeCubit() : super(const ThemeModeState());
+
+  void getTheme(ThemeModeState state) {
+    emit(state);
+  }
+
+  @override
+  ThemeModeState? fromJson(Map<String, dynamic> json) {
+    switch (json['themeMode']) {
+      case 'ThemeMode.dark':
+        return const ThemeModeState(themeMode: ThemeMode.dark);
+      case 'ThemeMode.light':
+        return const ThemeModeState(themeMode: ThemeMode.light);
+      case 'ThemeMode.system':
+      default:
+        return const ThemeModeState(themeMode: ThemeMode.system);
+    }
+  }
+
+  @override
+  Map<String, String>? toJson(ThemeModeState state) {
+    return <String, String>{'themeMode': state.themeMode.toString()};
+  }
+}
diff --git a/lib/cubit/theme_state.dart b/lib/cubit/theme_state.dart
new file mode 100644
index 0000000..e479a50
--- /dev/null
+++ b/lib/cubit/theme_state.dart
@@ -0,0 +1,15 @@
+part of 'theme_cubit.dart';
+
+@immutable
+class ThemeModeState extends Equatable {
+  const ThemeModeState({
+    this.themeMode,
+  });
+
+  final ThemeMode? themeMode;
+
+  @override
+  List<Object?> get props => <Object?>[
+        themeMode,
+      ];
+}
diff --git a/lib/main.dart b/lib/main.dart
index a5c7e89..a69d933 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -6,13 +6,14 @@ import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:hive/hive.dart';
 import 'package:hydrated_bloc/hydrated_bloc.dart';
 import 'package:path_provider/path_provider.dart';
-import 'package:overlay_support/overlay_support.dart';
 
 import 'package:sudoku/config/default_global_settings.dart';
 import 'package:sudoku/config/theme.dart';
 import 'package:sudoku/cubit/game_cubit.dart';
+import 'package:sudoku/cubit/nav_cubit.dart';
 import 'package:sudoku/cubit/settings_game_cubit.dart';
 import 'package:sudoku/cubit/settings_global_cubit.dart';
+import 'package:sudoku/cubit/theme_cubit.dart';
 import 'package:sudoku/ui/skeleton.dart';
 
 void main() async {
@@ -51,22 +52,30 @@ class MyApp extends StatelessWidget {
 
     return MultiBlocProvider(
       providers: [
+        BlocProvider<NavCubit>(create: (context) => NavCubit()),
+        BlocProvider<ThemeCubit>(create: (context) => ThemeCubit()),
         BlocProvider<GameCubit>(create: (context) => GameCubit()),
         BlocProvider<GlobalSettingsCubit>(create: (context) => GlobalSettingsCubit()),
         BlocProvider<GameSettingsCubit>(create: (context) => GameSettingsCubit()),
       ],
-      child: OverlaySupport(
-        child: MaterialApp(
-          title: 'Sudoku',
-          theme: appTheme,
-          home: const SkeletonScreen(),
+      child: BlocBuilder<ThemeCubit, ThemeModeState>(
+        builder: (BuildContext context, ThemeModeState state) {
+          return MaterialApp(
+            title: 'Sudoku',
+            home: const SkeletonScreen(),
 
-          // Localization stuff
-          localizationsDelegates: context.localizationDelegates,
-          supportedLocales: context.supportedLocales,
-          locale: context.locale,
-          debugShowCheckedModeBanner: false,
-        ),
+            // Theme stuff
+            theme: lightTheme,
+            darkTheme: darkTheme,
+            themeMode: state.themeMode,
+
+            // Localization stuff
+            localizationsDelegates: context.localizationDelegates,
+            supportedLocales: context.supportedLocales,
+            locale: context.locale,
+            debugShowCheckedModeBanner: false,
+          );
+        },
       ),
     );
   }
diff --git a/lib/ui/layout/board.dart b/lib/ui/layout/board.dart
index 3d44197..188d51c 100644
--- a/lib/ui/layout/board.dart
+++ b/lib/ui/layout/board.dart
@@ -15,7 +15,7 @@ class BoardLayout extends StatelessWidget {
       builder: (BuildContext context, GameState gameState) {
         final Game game = gameState.game;
 
-        const Color borderColor = Colors.black;
+        final Color borderColor = Theme.of(context).colorScheme.onBackground;
 
         return Container(
           margin: const EdgeInsets.all(2),
diff --git a/lib/ui/screens/game_page.dart b/lib/ui/screens/game_page.dart
new file mode 100644
index 0000000..9a8173f
--- /dev/null
+++ b/lib/ui/screens/game_page.dart
@@ -0,0 +1,26 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:sudoku/cubit/game_cubit.dart';
+import 'package:sudoku/ui/widgets/game.dart';
+import 'package:sudoku/ui/widgets/parameters.dart';
+
+class GamePage extends StatelessWidget {
+  const GamePage({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return Material(
+      color: Theme.of(context).colorScheme.background,
+      child: BlocBuilder<GameCubit, GameState>(
+        builder: (BuildContext context, GameState gameState) {
+          return gameState.game.isRunning
+              ? const GameWidget()
+              : Parameters(
+                  canResume: gameState.game.isStarted && !gameState.game.isFinished,
+                );
+        },
+      ),
+    );
+  }
+}
diff --git a/lib/ui/screens/screen_game.dart b/lib/ui/screens/screen_game.dart
deleted file mode 100644
index 0992122..0000000
--- a/lib/ui/screens/screen_game.dart
+++ /dev/null
@@ -1,34 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_bloc/flutter_bloc.dart';
-
-import 'package:sudoku/cubit/game_cubit.dart';
-import 'package:sudoku/models/game.dart';
-import 'package:sudoku/ui/layout/board.dart';
-import 'package:sudoku/ui/widgets/message_game_end.dart';
-import 'package:sudoku/ui/widgets/bar_select_cell_value.dart';
-
-class ScreenGame extends StatelessWidget {
-  const ScreenGame({super.key});
-
-  @override
-  Widget build(BuildContext context) {
-    return BlocBuilder<GameCubit, GameState>(
-      builder: (BuildContext context, GameState gameState) {
-        final Game game = gameState.game;
-
-        return Column(
-          mainAxisAlignment: MainAxisAlignment.start,
-          crossAxisAlignment: CrossAxisAlignment.center,
-          children: [
-            const SizedBox(height: 8),
-            const BoardLayout(),
-            const SizedBox(height: 8),
-            game.isFinished ? const SizedBox.shrink() : const SelectCellValueBar(),
-            const Expanded(child: SizedBox.shrink()),
-            game.isFinished ? const EndGameMessage() : const SizedBox.shrink(),
-          ],
-        );
-      },
-    );
-  }
-}
diff --git a/lib/ui/screens/settings_page.dart b/lib/ui/screens/settings_page.dart
new file mode 100644
index 0000000..aea7bd5
--- /dev/null
+++ b/lib/ui/screens/settings_page.dart
@@ -0,0 +1,26 @@
+import 'package:flutter/material.dart';
+
+import 'package:sudoku/ui/widgets/header_app.dart';
+import 'package:sudoku/ui/widgets/settings/settings_form.dart';
+
+class SettingsPage extends StatelessWidget {
+  const SettingsPage({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return const Padding(
+      padding: EdgeInsets.symmetric(horizontal: 8),
+      child: Column(
+        mainAxisAlignment: MainAxisAlignment.start,
+        crossAxisAlignment: CrossAxisAlignment.start,
+        mainAxisSize: MainAxisSize.max,
+        children: <Widget>[
+          SizedBox(height: 8),
+          AppHeader(text: 'settings_title'),
+          SizedBox(height: 8),
+          SettingsForm(),
+        ],
+      ),
+    );
+  }
+}
diff --git a/lib/ui/skeleton.dart b/lib/ui/skeleton.dart
index 2bfdbfe..0923b76 100644
--- a/lib/ui/skeleton.dart
+++ b/lib/ui/skeleton.dart
@@ -1,9 +1,8 @@
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
-import 'package:sudoku/cubit/game_cubit.dart';
-import 'package:sudoku/ui/screens/screen_game.dart';
-import 'package:sudoku/ui/screens/screen_parameters.dart';
+import 'package:sudoku/config/menu.dart';
+import 'package:sudoku/cubit/nav_cubit.dart';
 import 'package:sudoku/ui/widgets/global_app_bar.dart';
 
 class SkeletonScreen extends StatelessWidget {
@@ -14,22 +13,10 @@ class SkeletonScreen extends StatelessWidget {
     return Scaffold(
       appBar: const GlobalAppBar(),
       extendBodyBehindAppBar: false,
-      body: Material(
-        color: Theme.of(context).colorScheme.background,
-        child: Container(
-          margin: const EdgeInsets.only(
-            top: 8.0,
-          ),
-          child: BlocBuilder<GameCubit, GameState>(
-            builder: (BuildContext context, GameState gameState) {
-              return gameState.game.isRunning
-                  ? const ScreenGame()
-                  : ScreenParameters(
-                      canResume: gameState.game.isStarted && !gameState.game.isFinished,
-                    );
-            },
-          ),
-        ),
+      body: BlocBuilder<NavCubit, int>(
+        builder: (BuildContext context, int pageIndex) {
+          return Menu.getPageWidget(pageIndex);
+        },
       ),
       backgroundColor: Theme.of(context).colorScheme.background,
     );
diff --git a/lib/ui/widgets/cell_widget.dart b/lib/ui/widgets/cell_widget.dart
index eb697a0..6bc0200 100644
--- a/lib/ui/widgets/cell_widget.dart
+++ b/lib/ui/widgets/cell_widget.dart
@@ -115,8 +115,8 @@ class CellWidget extends StatelessWidget {
 
   // Compute cell borders, from board size and cell state
   Border getCellBorders(Game game) {
-    const Color cellBorderDarkColor = Colors.black;
-    const Color cellBorderLightColor = Colors.grey;
+    final Color cellBorderDarkColor = Colors.grey.shade800;
+    final Color cellBorderLightColor = Colors.grey.shade600;
     const Color cellBorderSelectedColor = Colors.red;
 
     Color cellBorderColor = cellBorderSelectedColor;
diff --git a/lib/ui/widgets/cell_widget_update.dart b/lib/ui/widgets/cell_widget_update.dart
index a8bb005..37371a5 100644
--- a/lib/ui/widgets/cell_widget_update.dart
+++ b/lib/ui/widgets/cell_widget_update.dart
@@ -36,7 +36,7 @@ class CellWidgetUpdate extends StatelessWidget {
           decoration: BoxDecoration(
             color: backgroundColor,
             border: Border.all(
-              color: Colors.black,
+              color: Colors.grey.shade700,
               width: 2,
             ),
           ),
diff --git a/lib/ui/widgets/game.dart b/lib/ui/widgets/game.dart
new file mode 100644
index 0000000..b9160d1
--- /dev/null
+++ b/lib/ui/widgets/game.dart
@@ -0,0 +1,37 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:sudoku/cubit/game_cubit.dart';
+import 'package:sudoku/models/game.dart';
+import 'package:sudoku/ui/layout/board.dart';
+import 'package:sudoku/ui/widgets/message_game_end.dart';
+import 'package:sudoku/ui/widgets/bar_select_cell_value.dart';
+
+class GameWidget extends StatelessWidget {
+  const GameWidget({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<GameCubit, GameState>(
+      builder: (BuildContext context, GameState gameState) {
+        final Game game = gameState.game;
+
+        return Container(
+          padding: const EdgeInsets.all(4),
+          child: Column(
+            mainAxisAlignment: MainAxisAlignment.start,
+            crossAxisAlignment: CrossAxisAlignment.center,
+            children: [
+              const SizedBox(height: 8),
+              const BoardLayout(),
+              const SizedBox(height: 8),
+              game.isFinished ? const SizedBox.shrink() : const SelectCellValueBar(),
+              const Expanded(child: SizedBox.shrink()),
+              game.isFinished ? const EndGameMessage() : const SizedBox.shrink(),
+            ],
+          ),
+        );
+      },
+    );
+  }
+}
diff --git a/lib/ui/widgets/global_app_bar.dart b/lib/ui/widgets/global_app_bar.dart
index 5583405..22cccc1 100644
--- a/lib/ui/widgets/global_app_bar.dart
+++ b/lib/ui/widgets/global_app_bar.dart
@@ -1,10 +1,11 @@
 import 'package:badges/badges.dart' as badges;
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
-import 'package:overlay_support/overlay_support.dart';
 
 import 'package:sudoku/config/default_global_settings.dart';
+import 'package:sudoku/config/menu.dart';
 import 'package:sudoku/cubit/game_cubit.dart';
+import 'package:sudoku/cubit/nav_cubit.dart';
 import 'package:sudoku/models/game.dart';
 
 class GlobalAppBar extends StatelessWidget implements PreferredSizeWidget {
@@ -14,95 +15,123 @@ class GlobalAppBar extends StatelessWidget implements PreferredSizeWidget {
   Widget build(BuildContext context) {
     return BlocBuilder<GameCubit, GameState>(
       builder: (BuildContext context, GameState gameState) {
-        final Game game = gameState.game;
+        return BlocBuilder<NavCubit, int>(
+          builder: (BuildContext context, int pageIndex) {
+            final Game game = gameState.game;
 
-        final List<Widget> menuActions = [];
+            final List<Widget> menuActions = [];
 
-        if (game.isRunning && !game.isFinished) {
-          final GameCubit gameCubit = BlocProvider.of<GameCubit>(context);
+            if (game.isRunning && !game.isFinished) {
+              final GameCubit gameCubit = BlocProvider.of<GameCubit>(context);
 
-          menuActions.add(TextButton(
-            child: Container(
-              decoration: BoxDecoration(
-                borderRadius: BorderRadius.circular(4),
-                border: Border.all(
-                  color: Colors.white,
-                  width: 3,
-                ),
-              ),
-              child: const Image(
-                image: AssetImage('assets/icons/button_back.png'),
-                fit: BoxFit.fill,
-              ),
-            ),
-            onPressed: () => toast('Long press to quit game...'),
-            onLongPress: () {
-              gameCubit.quitGame();
-            },
-          ));
-          menuActions.add(const Spacer(flex: 6));
-          menuActions.add(TextButton(
-            child: Container(
-              decoration: BoxDecoration(
-                borderRadius: BorderRadius.circular(4),
-                border: Border.all(
-                  color: Colors.white,
-                  width: 3,
-                ),
-              ),
-              child: badges.Badge(
-                showBadge: game.givenTipsCount == 0 ? false : true,
-                badgeStyle: badges.BadgeStyle(
-                  badgeColor: game.givenTipsCount < 10
-                      ? Colors.green
-                      : game.givenTipsCount < 20
-                          ? Colors.orange
-                          : Colors.red,
+              menuActions.add(TextButton(
+                onPressed: null,
+                onLongPress: () {
+                  gameCubit.quitGame();
+                },
+                child: Container(
+                  decoration: BoxDecoration(
+                    borderRadius: BorderRadius.circular(4),
+                    border: Border.all(
+                      color: Colors.white,
+                      width: 3,
+                    ),
+                  ),
+                  child: const Image(
+                    image: AssetImage('assets/icons/button_back.png'),
+                    fit: BoxFit.fill,
+                  ),
                 ),
-                badgeContent: Text(
-                  game.givenTipsCount == 0 ? '' : game.givenTipsCount.toString(),
-                  style: const TextStyle(color: Colors.white),
+              ));
+              menuActions.add(const Spacer(flex: 6));
+              menuActions.add(TextButton(
+                child: Container(
+                  decoration: BoxDecoration(
+                    borderRadius: BorderRadius.circular(4),
+                    border: Border.all(
+                      color: Colors.white,
+                      width: 3,
+                    ),
+                  ),
+                  child: badges.Badge(
+                    showBadge: game.givenTipsCount == 0 ? false : true,
+                    badgeStyle: badges.BadgeStyle(
+                      badgeColor: game.givenTipsCount < 10
+                          ? Colors.green
+                          : game.givenTipsCount < 20
+                              ? Colors.orange
+                              : Colors.red,
+                    ),
+                    badgeContent: Text(
+                      game.givenTipsCount == 0 ? '' : game.givenTipsCount.toString(),
+                      style: const TextStyle(color: Colors.white),
+                    ),
+                    child: Container(
+                      padding: EdgeInsets.all(15 *
+                          game.buttonTipsCountdown /
+                          DefaultGlobalSettings.defaultTipCountDownValueInSeconds),
+                      child: const Image(
+                        image: AssetImage('assets/icons/button_help.png'),
+                        fit: BoxFit.fill,
+                      ),
+                    ),
+                  ),
                 ),
+                onPressed: () {
+                  final GameCubit gameCubit = BlocProvider.of<GameCubit>(context);
+                  game.canGiveTip() ? game.showTip(gameCubit) : null;
+                },
+              ));
+              menuActions.add(const Spacer());
+              menuActions.add(TextButton(
                 child: Container(
-                  padding: EdgeInsets.all(15 *
-                      game.buttonTipsCountdown /
-                      DefaultGlobalSettings.defaultTipCountDownValueInSeconds),
+                  decoration: BoxDecoration(
+                    borderRadius: BorderRadius.circular(4),
+                    border: Border.all(
+                      color: game.showConflicts == true ? Colors.blue : Colors.white,
+                      width: 3,
+                    ),
+                  ),
                   child: const Image(
-                    image: AssetImage('assets/icons/button_help.png'),
+                    image: AssetImage('assets/icons/button_show_conflicts.png'),
                     fit: BoxFit.fill,
                   ),
                 ),
-              ),
-            ),
-            onPressed: () {
-              final GameCubit gameCubit = BlocProvider.of<GameCubit>(context);
-              game.canGiveTip() ? game.showTip(gameCubit) : null;
-            },
-          ));
-          menuActions.add(const Spacer());
-          menuActions.add(TextButton(
-            child: Container(
-              decoration: BoxDecoration(
-                borderRadius: BorderRadius.circular(4),
-                border: Border.all(
-                  color: game.showConflicts == true ? Colors.blue : Colors.white,
-                  width: 3,
-                ),
-              ),
-              child: const Image(
-                image: AssetImage('assets/icons/button_show_conflicts.png'),
-                fit: BoxFit.fill,
-              ),
-            ),
-            onPressed: () {
-              gameCubit.toggleShowConflicts();
-            },
-          ));
-        }
+                onPressed: () {
+                  gameCubit.toggleShowConflicts();
+                },
+              ));
+            } else {
+              if (pageIndex == Menu.indexGame) {
+                // go to Settings page
+                menuActions.add(ElevatedButton(
+                  onPressed: () {
+                    context.read<NavCubit>().switchToSettingsPage();
+                  },
+                  style: ElevatedButton.styleFrom(
+                    shape: const CircleBorder(),
+                  ),
+                  child: Menu.menuItemSettings.icon,
+                ));
+              } else {
+                // back to Home page
+                menuActions.add(ElevatedButton(
+                  onPressed: () {
+                    context.read<NavCubit>().goToGamePage();
+                  },
+                  style: ElevatedButton.styleFrom(
+                    shape: const CircleBorder(),
+                  ),
+                  child: Menu.menuItemGame.icon,
+                ));
+              }
+            }
 
-        return AppBar(
-          title: const SizedBox.shrink(),
-          actions: menuActions,
+            return AppBar(
+              title: const SizedBox.shrink(),
+              actions: menuActions,
+            );
+          },
         );
       },
     );
diff --git a/lib/ui/widgets/header_app.dart b/lib/ui/widgets/header_app.dart
new file mode 100644
index 0000000..b5c5be0
--- /dev/null
+++ b/lib/ui/widgets/header_app.dart
@@ -0,0 +1,24 @@
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+
+class AppHeader extends StatelessWidget {
+  const AppHeader({super.key, required this.text});
+
+  final String text;
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      mainAxisAlignment: MainAxisAlignment.start,
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        Text(
+          tr(text),
+          textAlign: TextAlign.start,
+          style: Theme.of(context).textTheme.headlineSmall!.apply(fontWeightDelta: 2),
+        ),
+        const SizedBox(height: 8),
+      ],
+    );
+  }
+}
diff --git a/lib/ui/screens/screen_parameters.dart b/lib/ui/widgets/parameters.dart
similarity index 96%
rename from lib/ui/screens/screen_parameters.dart
rename to lib/ui/widgets/parameters.dart
index ff6e2ab..4018b05 100644
--- a/lib/ui/screens/screen_parameters.dart
+++ b/lib/ui/widgets/parameters.dart
@@ -11,8 +11,8 @@ import 'package:sudoku/ui/widgets/button_game_start_new.dart';
 import 'package:sudoku/ui/widgets/button_resume_saved_game.dart';
 import 'package:sudoku/ui/widgets/parameter_image.dart';
 
-class ScreenParameters extends StatelessWidget {
-  const ScreenParameters({super.key, required this.canResume});
+class Parameters extends StatelessWidget {
+  const Parameters({super.key, required this.canResume});
 
   final bool canResume;
 
@@ -22,6 +22,8 @@ class ScreenParameters extends StatelessWidget {
   Widget build(BuildContext context) {
     final List<Widget> lines = [];
 
+    lines.add(SizedBox(height: separatorHeight));
+
     // Game settings
     for (String code in DefaultGameSettings.availableParameters) {
       lines.add(Row(
diff --git a/lib/ui/widgets/settings/settings_form.dart b/lib/ui/widgets/settings/settings_form.dart
new file mode 100644
index 0000000..53576c1
--- /dev/null
+++ b/lib/ui/widgets/settings/settings_form.dart
@@ -0,0 +1,63 @@
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:unicons/unicons.dart';
+
+import 'package:sudoku/ui/widgets/settings/theme_card.dart';
+
+class SettingsForm extends StatefulWidget {
+  const SettingsForm({super.key});
+
+  @override
+  State<SettingsForm> createState() => _SettingsFormState();
+}
+
+class _SettingsFormState extends State<SettingsForm> {
+  @override
+  void dispose() {
+    super.dispose();
+  }
+
+  @override
+  void didChangeDependencies() {
+    super.didChangeDependencies();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      mainAxisAlignment: MainAxisAlignment.start,
+      crossAxisAlignment: CrossAxisAlignment.start,
+      mainAxisSize: MainAxisSize.max,
+      children: <Widget>[
+        // Light/dark theme
+        Row(
+          mainAxisAlignment: MainAxisAlignment.spaceBetween,
+          crossAxisAlignment: CrossAxisAlignment.center,
+          children: <Widget>[
+            const Text('settings_label_theme').tr(),
+            const Row(
+              mainAxisAlignment: MainAxisAlignment.end,
+              crossAxisAlignment: CrossAxisAlignment.center,
+              children: [
+                ThemeCard(
+                  mode: ThemeMode.system,
+                  icon: UniconsLine.cog,
+                ),
+                ThemeCard(
+                  mode: ThemeMode.light,
+                  icon: UniconsLine.sun,
+                ),
+                ThemeCard(
+                  mode: ThemeMode.dark,
+                  icon: UniconsLine.moon,
+                )
+              ],
+            ),
+          ],
+        ),
+
+        const SizedBox(height: 16),
+      ],
+    );
+  }
+}
diff --git a/lib/ui/widgets/settings/theme_card.dart b/lib/ui/widgets/settings/theme_card.dart
new file mode 100644
index 0000000..39b3d6d
--- /dev/null
+++ b/lib/ui/widgets/settings/theme_card.dart
@@ -0,0 +1,47 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:sudoku/cubit/theme_cubit.dart';
+
+class ThemeCard extends StatelessWidget {
+  const ThemeCard({
+    super.key,
+    required this.mode,
+    required this.icon,
+  });
+
+  final IconData icon;
+  final ThemeMode mode;
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<ThemeCubit, ThemeModeState>(
+      builder: (BuildContext context, ThemeModeState state) {
+        return Card(
+          elevation: 2,
+          shadowColor: Theme.of(context).colorScheme.shadow,
+          color: state.themeMode == mode
+              ? Theme.of(context).colorScheme.primary
+              : Theme.of(context).colorScheme.surface,
+          shape: const RoundedRectangleBorder(
+            borderRadius: BorderRadius.all(Radius.circular(12)),
+          ),
+          margin: const EdgeInsets.all(5),
+          child: InkWell(
+            onTap: () => BlocProvider.of<ThemeCubit>(context).getTheme(
+              ThemeModeState(themeMode: mode),
+            ),
+            borderRadius: const BorderRadius.all(Radius.circular(12)),
+            child: Icon(
+              icon,
+              size: 32,
+              color: state.themeMode != mode
+                  ? Theme.of(context).colorScheme.primary
+                  : Colors.white,
+            ),
+          ),
+        );
+      },
+    );
+  }
+}
diff --git a/pubspec.lock b/pubspec.lock
index 4991f4d..b35bc75 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -9,14 +9,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.5.0"
-  async:
-    dependency: transitive
-    description:
-      name: async
-      sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
-      url: "https://pub.dev"
-    source: hosted
-    version: "2.11.0"
   badges:
     dependency: "direct main"
     description:
@@ -192,14 +184,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.0.0"
-  overlay_support:
-    dependency: "direct main"
-    description:
-      name: overlay_support
-      sha256: fc39389bfd94e6985e1e13b2a88a125fc4027608485d2d4e2847afe1b2bb339c
-      url: "https://pub.dev"
-    source: hosted
-    version: "2.1.0"
   path:
     dependency: transitive
     description:
@@ -357,6 +341,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.3.2"
+  unicons:
+    dependency: "direct main"
+    description:
+      name: unicons
+      sha256: dbfcf93ff4d4ea19b324113857e358e4882115ab85db04417a4ba1c72b17a670
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.1"
   vector_math:
     dependency: transitive
     description:
diff --git a/pubspec.yaml b/pubspec.yaml
index fca46c7..d71e600 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,24 +1,25 @@
 name: sudoku
 description: A sudoku game application.
 
-publish_to: 'none'
+publish_to: "none"
 
-version: 0.1.20+69
+version: 0.1.21+70
 
 environment:
-  sdk: '^3.0.0'
+  sdk: "^3.0.0"
 
 dependencies:
   flutter:
     sdk: flutter
+
   badges: ^3.1.2
   easy_localization: ^3.0.1
   equatable: ^2.0.5
   flutter_bloc: ^8.1.1
   hive: ^2.2.3
   hydrated_bloc: ^9.0.0
-  overlay_support: ^2.1.0
   path_provider: ^2.0.11
+  unicons: ^2.1.1
 
 dev_dependencies:
   flutter_lints: ^3.0.1
-- 
GitLab