diff --git a/android/app/build.gradle b/android/app/build.gradle
index 205d4b68c1e6b433e0475d1ae26e334dd0314f57..c837737204ea3688c7128f45e28ae9851b79bd66 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -37,7 +37,7 @@ if (keystorePropertiesFile.exists()) {
 }
 
 android {
-    compileSdkVersion 33
+    compileSdkVersion 34
     namespace "org.benoitharrault.tetrisdual"
 
     defaultConfig {
diff --git a/android/gradle.properties b/android/gradle.properties
index d9abd55731010fe508f39321892e8002f10e79ef..6e2124a9b333ebbcb6e52e0ae6e7080553f5887a 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.0.12
-app.versionCode=12
+app.versionName=0.1.0
+app.versionCode=13
diff --git a/assets/fonts/Nunito-Bold.ttf b/assets/fonts/Nunito-Bold.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..6519feb781449ebe0015cbc74dfd9e13110fbba9
Binary files /dev/null and b/assets/fonts/Nunito-Bold.ttf differ
diff --git a/assets/fonts/Nunito-Light.ttf b/assets/fonts/Nunito-Light.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..8a0736c41cd6c2a1225d356bf274de1d0afc3497
Binary files /dev/null and b/assets/fonts/Nunito-Light.ttf differ
diff --git a/assets/fonts/Nunito-Medium.ttf b/assets/fonts/Nunito-Medium.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..88fccdc0638b6f5d6ac49d9d269dc3d518618ad1
Binary files /dev/null and b/assets/fonts/Nunito-Medium.ttf differ
diff --git a/assets/fonts/Nunito-Regular.ttf b/assets/fonts/Nunito-Regular.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..e7b8375a896ef0cd8e06730a78c84532b377e784
Binary files /dev/null and b/assets/fonts/Nunito-Regular.ttf differ
diff --git a/assets/translations/en.json b/assets/translations/en.json
new file mode 100644
index 0000000000000000000000000000000000000000..83738f6c34524dd01113e8f896084aeab7f00612
--- /dev/null
+++ b/assets/translations/en.json
@@ -0,0 +1,12 @@
+{
+  "app_name": "Tetris dual",
+
+  "settings_title": "Settings",
+  "settings_label_theme": "Theme mode",
+
+  "about_title": "Informations",
+  "about_content": "Tetris dual",
+  "about_version": "Version: {version}",
+
+  "": ""
+}
diff --git a/assets/translations/fr.json b/assets/translations/fr.json
new file mode 100644
index 0000000000000000000000000000000000000000..94b86c938fe2285a50df060d5869596324da81a9
--- /dev/null
+++ b/assets/translations/fr.json
@@ -0,0 +1,12 @@
+{
+  "app_name": "Tetris duel",
+
+  "settings_title": "Réglages",
+  "settings_label_theme": "Thème de couleurs",
+
+  "about_title": "Informations",
+  "about_content": "Tetris duel.",
+  "about_version": "Version : {version}",
+
+  "": ""
+}
diff --git a/assets/icons/button_back.png b/assets/ui/button_back.png
similarity index 100%
rename from assets/icons/button_back.png
rename to assets/ui/button_back.png
diff --git a/assets/ui/button_delete_saved_game.png b/assets/ui/button_delete_saved_game.png
new file mode 100644
index 0000000000000000000000000000000000000000..5e4f217689b11e444b7163557d7e5d68f3bbfe7d
Binary files /dev/null and b/assets/ui/button_delete_saved_game.png differ
diff --git a/assets/ui/button_resume_game.png b/assets/ui/button_resume_game.png
new file mode 100644
index 0000000000000000000000000000000000000000..b2ea0a02d05e42377eb551a4b51428b511a32f5d
Binary files /dev/null and b/assets/ui/button_resume_game.png differ
diff --git a/assets/icons/button_start.png b/assets/ui/button_start.png
similarity index 100%
rename from assets/icons/button_start.png
rename to assets/ui/button_start.png
diff --git a/assets/icons/placeholder.png b/assets/ui/placeholder.png
similarity index 100%
rename from assets/icons/placeholder.png
rename to assets/ui/placeholder.png
diff --git a/fastlane/metadata/android/en-US/changelogs/13.txt b/fastlane/metadata/android/en-US/changelogs/13.txt
new file mode 100644
index 0000000000000000000000000000000000000000..d4afd512e55b3fd8ffbfd795adb9b00832e5aaef
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/13.txt
@@ -0,0 +1 @@
+Improve/normalize game architecture.
diff --git a/fastlane/metadata/android/fr-FR/changelogs/13.txt b/fastlane/metadata/android/fr-FR/changelogs/13.txt
new file mode 100644
index 0000000000000000000000000000000000000000..6a9871a5eb8eb3c6e9106520f1cbf1f39f9e5ef7
--- /dev/null
+++ b/fastlane/metadata/android/fr-FR/changelogs/13.txt
@@ -0,0 +1 @@
+Amélioration/normalisation de l'architecture du jeu.
diff --git a/icons/build_application_icons.sh b/icons/build_application_icons.sh
deleted file mode 100755
index 2ae92914464ceb9984af27c4b0ad1a4ff920bd55..0000000000000000000000000000000000000000
--- a/icons/build_application_icons.sh
+++ /dev/null
@@ -1,82 +0,0 @@
-#! /bin/bash
-
-# Check dependencies
-command -v inkscape >/dev/null 2>&1 || { echo >&2 "I require inkscape but it's not installed. Aborting."; exit 1; }
-command -v scour >/dev/null 2>&1 || { echo >&2 "I require scour but it's not installed. Aborting."; exit 1; }
-command -v optipng >/dev/null 2>&1 || { echo >&2 "I require optipng but it's not installed. Aborting."; exit 1; }
-
-CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
-BASE_DIR="$(dirname "${CURRENT_DIR}")"
-ASSETS_DIR="${BASE_DIR}/assets"
-
-OPTIPNG_OPTIONS="-preserve -quiet -o7"
-ICON_SIZE=192
-
-#######################################################
-
-# Game images
-AVAILABLE_GAME_IMAGES="
-  button_back
-  button_start
-  placeholder
-"
-
-# Settings images
-AVAILABLES_GAME_SETTINGS="
-"
-
-#######################################################
-
-# optimize svg
-function optimize_svg() {
-  SOURCE="$1"
-
-  cp ${SOURCE} ${SOURCE}.tmp
-  scour \
-      --remove-descriptive-elements \
-      --enable-id-stripping \
-      --enable-viewboxing \
-      --enable-comment-stripping \
-      --nindent=4 \
-      --quiet \
-      -i ${SOURCE}.tmp \
-      -o ${SOURCE}
-  rm ${SOURCE}.tmp
-}
-
-# build icons
-function build_icon() {
-  SOURCE="$1"
-  TARGET="$2"
-
-  echo "Building ${TARGET}"
-
-  if [ ! -f "${SOURCE}" ]; then
-    echo "Missing file: ${SOURCE}"
-    exit 1
-  fi
-
-  optimize_svg "${SOURCE}"
-
-  inkscape \
-      --export-width=${ICON_SIZE} \
-      --export-height=${ICON_SIZE} \
-      --export-filename=${TARGET} \
-      ${SOURCE}
-
-  optipng ${OPTIPNG_OPTIONS} ${TARGET}
-}
-
-#######################################################
-
-# Create output folders
-mkdir -p ${ASSETS_DIR}/icons
-
-# Delete existing generated images
-find ${ASSETS_DIR}/icons -type f -name "*.png" -delete
-
-# build game images
-for GAME_IMAGE in ${AVAILABLE_GAME_IMAGES}
-do
-  build_icon ${CURRENT_DIR}/${GAME_IMAGE}.svg ${ASSETS_DIR}/icons/${GAME_IMAGE}.png
-done
diff --git a/lib/config/default_game_settings.dart b/lib/config/default_game_settings.dart
new file mode 100644
index 0000000000000000000000000000000000000000..7341459a4da26baa921b0b5586be6728fdbcd047
--- /dev/null
+++ b/lib/config/default_game_settings.dart
@@ -0,0 +1,33 @@
+import 'package:tetrisdual/utils/tools.dart';      
+
+class DefaultGameSettings {
+  // available game parameters codes
+  static const String parameterCodeGameType = 'gameType';
+  static const List<String> availableParameters = [
+    parameterCodeGameType,
+  ];
+
+  // game mode: available values
+  static const String gameTypeValueDefault = 'default';
+  static const List<String> allowedGameTypeValues = [
+    gameTypeValueDefault,
+  ];
+  // game mode: default value
+  static const String defaultGameTypeValue = gameTypeValueDefault;
+
+  // available values from parameter code
+  static List<String> getAvailableValues(String parameterCode) {
+    switch (parameterCode) {
+      case parameterCodeGameType:
+        return DefaultGameSettings.allowedGameTypeValues;
+    }
+
+    printlog('Did not find any available value for game parameter "$parameterCode".');
+    return [];
+  }
+
+  // parameters displayed with assets (instead of painter)
+  static List<String> displayedWithAssets = [
+    //
+  ];
+}
diff --git a/lib/config/default_global_settings.dart b/lib/config/default_global_settings.dart
new file mode 100644
index 0000000000000000000000000000000000000000..3ab4eb7dc9f1803c925174cfdde182839e403842
--- /dev/null
+++ b/lib/config/default_global_settings.dart
@@ -0,0 +1,33 @@
+import 'package:tetrisdual/utils/tools.dart';      
+
+class DefaultGlobalSettings {
+  // available global parameters codes
+  static const String parameterCodeSkin = 'skin';
+  static const List<String> availableParameters = [
+    parameterCodeSkin,
+  ];
+
+  // skin: available values
+  static const String skinValueDefault = 'default';
+  static const List<String> allowedSkinValues = [
+    skinValueDefault,
+  ];
+  // skin: default value
+  static const String defaultSkinValue = skinValueDefault;
+
+  // available values from parameter code
+  static List<String> getAvailableValues(String parameterCode) {
+    switch (parameterCode) {
+      case parameterCodeSkin:
+        return DefaultGlobalSettings.allowedSkinValues;
+    }
+
+    printlog('Did not find any available value for global parameter "$parameterCode".');
+    return [];
+  }
+
+  // parameters displayed with assets (instead of painter)
+  static List<String> displayedWithAssets = [
+    //
+  ];
+}
diff --git a/lib/config/menu.dart b/lib/config/menu.dart
new file mode 100644
index 0000000000000000000000000000000000000000..78c099415746da7c1c87b7e104c31fe49759f6e4
--- /dev/null
+++ b/lib/config/menu.dart
@@ -0,0 +1,52 @@
+import 'package:flutter/material.dart';
+import 'package:unicons/unicons.dart';
+
+import 'package:tetrisdual/ui/screens/page_about.dart';
+import 'package:tetrisdual/ui/screens/page_game.dart';
+import 'package:tetrisdual/ui/screens/page_settings.dart';
+
+class MenuItem {
+  final Icon icon;
+  final Widget page;
+
+  const MenuItem({
+    required this.icon,
+    required this.page,
+  });
+}
+
+class Menu {
+  static const indexGame = 0;
+  static const menuItemGame = MenuItem(
+    icon: Icon(UniconsLine.home),
+    page: PageGame(),
+  );
+
+  static const indexSettings = 1;
+  static const menuItemSettings = MenuItem(
+    icon: Icon(UniconsLine.setting),
+    page: PageSettings(),
+  );
+
+  static const indexAbout = 2;
+  static const menuItemAbout = MenuItem(
+    icon: Icon(UniconsLine.info_circle),
+    page: PageAbout(),
+  );
+
+  static Map<int, MenuItem> items = {
+    indexGame: menuItemGame,
+    indexSettings: menuItemSettings,
+    indexAbout: menuItemAbout,
+  };
+
+  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/config/theme.dart b/lib/config/theme.dart
new file mode 100644
index 0000000000000000000000000000000000000000..74f532fd5abf693979118609564d29167e902009
--- /dev/null
+++ b/lib/config/theme.dart
@@ -0,0 +1,190 @@
+import 'package:flutter/material.dart';
+
+/// Colors from Tailwind CSS (v3.0) - June 2022
+///
+/// https://tailwindcss.com/docs/customizing-colors
+
+const int _primaryColor = 0xFF6366F1;
+const MaterialColor primarySwatch = MaterialColor(_primaryColor, <int, Color>{
+  50: Color(0xFFEEF2FF), // indigo-50
+  100: Color(0xFFE0E7FF), // indigo-100
+  200: Color(0xFFC7D2FE), // indigo-200
+  300: Color(0xFFA5B4FC), // indigo-300
+  400: Color(0xFF818CF8), // indigo-400
+  500: Color(_primaryColor), // indigo-500
+  600: Color(0xFF4F46E5), // indigo-600
+  700: Color(0xFF4338CA), // indigo-700
+  800: Color(0xFF3730A3), // indigo-800
+  900: Color(0xFF312E81), // indigo-900
+});
+
+const int _textColor = 0xFF64748B;
+const MaterialColor textSwatch = MaterialColor(_textColor, <int, Color>{
+  50: Color(0xFFF8FAFC), // slate-50
+  100: Color(0xFFF1F5F9), // slate-100
+  200: Color(0xFFE2E8F0), // slate-200
+  300: Color(0xFFCBD5E1), // slate-300
+  400: Color(0xFF94A3B8), // slate-400
+  500: Color(_textColor), // slate-500
+  600: Color(0xFF475569), // slate-600
+  700: Color(0xFF334155), // slate-700
+  800: Color(0xFF1E293B), // slate-800
+  900: Color(0xFF0F172A), // slate-900
+});
+
+const Color errorColor = Color(0xFFDC2626); // red-600
+
+final ColorScheme lightColorScheme = ColorScheme.light(
+  primary: primarySwatch.shade500,
+  secondary: primarySwatch.shade500,
+  onSecondary: Colors.white,
+  error: errorColor,
+  onSurface: textSwatch.shade500,
+  surface: textSwatch.shade50,
+  surfaceContainerHighest: Colors.white,
+  shadow: textSwatch.shade900.withOpacity(.1),
+);
+
+final ColorScheme darkColorScheme = ColorScheme.dark(
+  primary: primarySwatch.shade500,
+  secondary: primarySwatch.shade500,
+  onSecondary: Colors.white,
+  error: errorColor,
+  onSurface: textSwatch.shade300,
+  surface: const Color(0xFF262630),
+  surfaceContainerHighest: const Color(0xFF282832),
+  shadow: textSwatch.shade900.withOpacity(.2),
+);
+
+final ThemeData lightTheme = ThemeData(
+  colorScheme: lightColorScheme,
+  fontFamily: 'Nunito',
+  textTheme: TextTheme(
+    displayLarge: TextStyle(
+      color: textSwatch.shade700,
+      fontFamily: 'Nunito',
+    ),
+    displayMedium: TextStyle(
+      color: textSwatch.shade600,
+      fontFamily: 'Nunito',
+    ),
+    displaySmall: TextStyle(
+      color: textSwatch.shade500,
+      fontFamily: 'Nunito',
+    ),
+    headlineLarge: TextStyle(
+      color: textSwatch.shade700,
+      fontFamily: 'Nunito',
+    ),
+    headlineMedium: TextStyle(
+      color: textSwatch.shade600,
+      fontFamily: 'Nunito',
+    ),
+    headlineSmall: TextStyle(
+      color: textSwatch.shade500,
+      fontFamily: 'Nunito',
+    ),
+    titleLarge: TextStyle(
+      color: textSwatch.shade700,
+      fontFamily: 'Nunito',
+    ),
+    titleMedium: TextStyle(
+      color: textSwatch.shade600,
+      fontFamily: 'Nunito',
+    ),
+    titleSmall: TextStyle(
+      color: textSwatch.shade500,
+      fontFamily: 'Nunito',
+    ),
+    bodyLarge: TextStyle(
+      color: textSwatch.shade700,
+      fontFamily: 'Nunito',
+    ),
+    bodyMedium: TextStyle(
+      color: textSwatch.shade600,
+      fontFamily: 'Nunito',
+    ),
+    bodySmall: TextStyle(
+      color: textSwatch.shade500,
+      fontFamily: 'Nunito',
+    ),
+    labelLarge: TextStyle(
+      color: textSwatch.shade700,
+      fontFamily: 'Nunito',
+    ),
+    labelMedium: TextStyle(
+      color: textSwatch.shade600,
+      fontFamily: 'Nunito',
+    ),
+    labelSmall: TextStyle(
+      color: textSwatch.shade500,
+      fontFamily: 'Nunito',
+    ),
+  ),
+);
+
+final ThemeData darkTheme = lightTheme.copyWith(
+  colorScheme: darkColorScheme,
+  textTheme: TextTheme(
+    displayLarge: TextStyle(
+      color: textSwatch.shade200,
+      fontFamily: 'Nunito',
+    ),
+    displayMedium: TextStyle(
+      color: textSwatch.shade300,
+      fontFamily: 'Nunito',
+    ),
+    displaySmall: TextStyle(
+      color: textSwatch.shade400,
+      fontFamily: 'Nunito',
+    ),
+    headlineLarge: TextStyle(
+      color: textSwatch.shade200,
+      fontFamily: 'Nunito',
+    ),
+    headlineMedium: TextStyle(
+      color: textSwatch.shade300,
+      fontFamily: 'Nunito',
+    ),
+    headlineSmall: TextStyle(
+      color: textSwatch.shade400,
+      fontFamily: 'Nunito',
+    ),
+    titleLarge: TextStyle(
+      color: textSwatch.shade200,
+      fontFamily: 'Nunito',
+    ),
+    titleMedium: TextStyle(
+      color: textSwatch.shade300,
+      fontFamily: 'Nunito',
+    ),
+    titleSmall: TextStyle(
+      color: textSwatch.shade400,
+      fontFamily: 'Nunito',
+    ),
+    bodyLarge: TextStyle(
+      color: textSwatch.shade200,
+      fontFamily: 'Nunito',
+    ),
+    bodyMedium: TextStyle(
+      color: textSwatch.shade300,
+      fontFamily: 'Nunito',
+    ),
+    bodySmall: TextStyle(
+      color: textSwatch.shade400,
+      fontFamily: 'Nunito',
+    ),
+    labelLarge: TextStyle(
+      color: textSwatch.shade200,
+      fontFamily: 'Nunito',
+    ),
+    labelMedium: TextStyle(
+      color: textSwatch.shade300,
+      fontFamily: 'Nunito',
+    ),
+    labelSmall: TextStyle(
+      color: textSwatch.shade400,
+      fontFamily: 'Nunito',
+    ),
+  ),
+);
diff --git a/lib/cubit/game_cubit.dart b/lib/cubit/game_cubit.dart
new file mode 100644
index 0000000000000000000000000000000000000000..c8dd323c9b9e16e470454b917349ae09d3c54733
--- /dev/null
+++ b/lib/cubit/game_cubit.dart
@@ -0,0 +1,127 @@
+import 'dart:math';
+
+import 'package:equatable/equatable.dart';
+import 'package:flutter/material.dart';
+import 'package:hydrated_bloc/hydrated_bloc.dart';
+
+import 'package:tetrisdual/models/game/game.dart';
+import 'package:tetrisdual/models/game/player.dart';
+import 'package:tetrisdual/models/settings/settings_game.dart';
+import 'package:tetrisdual/models/settings/settings_global.dart';
+
+part 'game_state.dart';
+
+class GameCubit extends HydratedCubit<GameState> {
+  GameCubit()
+      : super(GameState(
+          currentGame: Game.createEmpty(),
+        ));
+
+  void updateState(Game game) {
+    emit(GameState(
+      currentGame: game,
+    ));
+  }
+
+  void refresh() {
+    final Game game = Game(
+      // Settings
+      gameSettings: state.currentGame.gameSettings,
+      globalSettings: state.currentGame.globalSettings,
+      // State
+      isRunning: state.currentGame.isRunning,
+      isStarted: state.currentGame.isStarted,
+      isFinished: state.currentGame.isFinished,
+      animationInProgress: state.currentGame.animationInProgress,
+      // Base data
+      players: state.currentGame.players,
+      // Game data
+      currentPlayer: state.currentGame.currentPlayer,
+    );
+    // game.dump();
+
+    updateState(game);
+  }
+
+  void startNewGame({
+    required GameSettings gameSettings,
+    required GlobalSettings globalSettings,
+  }) {
+    final Game newGame = Game.createNew(
+      // Settings
+      gameSettings: gameSettings,
+      globalSettings: globalSettings,
+    );
+
+    newGame.dump();
+
+    updateState(newGame);
+    enableRandomPlayer();
+    refresh();
+  }
+
+  void quitGame() {
+    state.currentGame.isRunning = false;
+    refresh();
+  }
+
+  void resumeSavedGame() {
+    state.currentGame.isRunning = true;
+    refresh();
+  }
+
+  void deleteSavedGame() {
+    state.currentGame.isRunning = false;
+    state.currentGame.isFinished = true;
+    refresh();
+  }
+
+  void toggleCurrentPlayer() {
+    state.currentGame.isStarted = true;
+
+    // brand new game
+    if (state.currentGame.currentPlayer == 0) {
+      state.currentGame.currentPlayer = 1;
+    } else {
+      // Reset current player tetrimino
+      getCurrentPlayer().resetTetrimino();
+
+      // toggle: 1 -> 2 ; 2 -> 1
+      state.currentGame.currentPlayer = 3 - state.currentGame.currentPlayer;
+    }
+
+    // Pick new tetrimino
+    getCurrentPlayer().pickRandomTetrimino();
+
+    refresh();
+  }
+
+  void enableRandomPlayer() {
+    state.currentGame.currentPlayer = Random().nextInt(2) + 1;
+    toggleCurrentPlayer();
+  }
+
+  Player getPlayer(int playerId) {
+    return state.currentGame.players[playerId - 1];
+  }
+
+  Player getCurrentPlayer() {
+    return getPlayer(state.currentGame.currentPlayer);
+  }
+
+  @override
+  GameState? fromJson(Map<String, dynamic> json) {
+    final Game currentGame = json['currentGame'] as Game;
+
+    return GameState(
+      currentGame: currentGame,
+    );
+  }
+
+  @override
+  Map<String, dynamic>? toJson(GameState state) {
+    return <String, dynamic>{
+      'currentGame': state.currentGame.toJson(),
+    };
+  }
+}
diff --git a/lib/cubit/game_state.dart b/lib/cubit/game_state.dart
new file mode 100644
index 0000000000000000000000000000000000000000..00e211668c3269255926939324355792abd61c41
--- /dev/null
+++ b/lib/cubit/game_state.dart
@@ -0,0 +1,15 @@
+part of 'game_cubit.dart';
+
+@immutable
+class GameState extends Equatable {
+  const GameState({
+    required this.currentGame,
+  });
+
+  final Game currentGame;
+
+  @override
+  List<dynamic> get props => <dynamic>[
+        currentGame,
+      ];
+}
diff --git a/lib/cubit/nav_cubit.dart b/lib/cubit/nav_cubit.dart
new file mode 100644
index 0000000000000000000000000000000000000000..e65528cb3ae94660e1b6f4d9bec221619c5c596b
--- /dev/null
+++ b/lib/cubit/nav_cubit.dart
@@ -0,0 +1,37 @@
+import 'package:hydrated_bloc/hydrated_bloc.dart';
+
+import 'package:tetrisdual/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 goToSettingsPage() {
+    emit(Menu.indexSettings);
+  }
+
+  void goToAboutPage() {
+    emit(Menu.indexAbout);
+  }
+
+  @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/settings_game_cubit.dart b/lib/cubit/settings_game_cubit.dart
new file mode 100644
index 0000000000000000000000000000000000000000..406db47d441bccc6974d8d5e351e1e609a56bc3d
--- /dev/null
+++ b/lib/cubit/settings_game_cubit.dart
@@ -0,0 +1,61 @@
+import 'package:equatable/equatable.dart';
+import 'package:flutter/material.dart';
+import 'package:hydrated_bloc/hydrated_bloc.dart';
+
+import 'package:tetrisdual/config/default_game_settings.dart';
+import 'package:tetrisdual/models/settings/settings_game.dart';
+
+part 'settings_game_state.dart';
+
+class GameSettingsCubit extends HydratedCubit<GameSettingsState> {
+  GameSettingsCubit() : super(GameSettingsState(settings: GameSettings.createDefault()));
+
+  void setValues({
+    String? gameType,
+  }) {
+    emit(
+      GameSettingsState(
+        settings: GameSettings(
+          gameType: gameType ?? state.settings.gameType,
+        ),
+      ),
+    );
+  }
+
+  String getParameterValue(String code) {
+    switch (code) {
+      case DefaultGameSettings.parameterCodeGameType:
+        return GameSettings.getGameTypeValueFromUnsafe(state.settings.gameType);
+    }
+
+    return '';
+  }
+
+  void setParameterValue(String code, String value) {
+    final String gameType = code == DefaultGameSettings.parameterCodeGameType
+        ? value
+        : getParameterValue(DefaultGameSettings.parameterCodeGameType);
+
+    setValues(
+      gameType: gameType,
+    );
+  }
+
+  @override
+  GameSettingsState? fromJson(Map<String, dynamic> json) {
+    final String gameType = json[DefaultGameSettings.parameterCodeGameType] as String;
+
+    return GameSettingsState(
+      settings: GameSettings(
+        gameType: gameType,
+      ),
+    );
+  }
+
+  @override
+  Map<String, dynamic>? toJson(GameSettingsState state) {
+    return <String, dynamic>{
+      DefaultGameSettings.parameterCodeGameType: state.settings.gameType,
+    };
+  }
+}
diff --git a/lib/cubit/settings_game_state.dart b/lib/cubit/settings_game_state.dart
new file mode 100644
index 0000000000000000000000000000000000000000..5acd85b44ba541e1c5e9c26af1c4be26a385b9ed
--- /dev/null
+++ b/lib/cubit/settings_game_state.dart
@@ -0,0 +1,15 @@
+part of 'settings_game_cubit.dart';
+
+@immutable
+class GameSettingsState extends Equatable {
+  const GameSettingsState({
+    required this.settings,
+  });
+
+  final GameSettings settings;
+
+  @override
+  List<dynamic> get props => <dynamic>[
+        settings,
+      ];
+}
diff --git a/lib/cubit/settings_global_cubit.dart b/lib/cubit/settings_global_cubit.dart
new file mode 100644
index 0000000000000000000000000000000000000000..19b8d182f69c1b63c4f3567ffd1d952b4e8a4011
--- /dev/null
+++ b/lib/cubit/settings_global_cubit.dart
@@ -0,0 +1,60 @@
+import 'package:equatable/equatable.dart';
+import 'package:flutter/material.dart';
+import 'package:hydrated_bloc/hydrated_bloc.dart';
+
+import 'package:tetrisdual/config/default_global_settings.dart';
+import 'package:tetrisdual/models/settings/settings_global.dart';
+
+part 'settings_global_state.dart';
+
+class GlobalSettingsCubit extends HydratedCubit<GlobalSettingsState> {
+  GlobalSettingsCubit() : super(GlobalSettingsState(settings: GlobalSettings.createDefault()));
+
+  void setValues({
+    String? skin,
+  }) {
+    emit(
+      GlobalSettingsState(
+        settings: GlobalSettings(
+          skin: skin ?? state.settings.skin,
+        ),
+      ),
+    );
+  }
+
+  String getParameterValue(String code) {
+    switch (code) {
+      case DefaultGlobalSettings.parameterCodeSkin:
+        return GlobalSettings.getSkinValueFromUnsafe(state.settings.skin);
+    }
+    return '';
+  }
+
+  void setParameterValue(String code, String value) {
+    final String skin = (code == DefaultGlobalSettings.parameterCodeSkin)
+        ? value
+        : getParameterValue(DefaultGlobalSettings.parameterCodeSkin);
+
+    setValues(
+      skin: skin,
+    );
+  }
+
+  @override
+  GlobalSettingsState? fromJson(Map<String, dynamic> json) {
+    final String skin = json[DefaultGlobalSettings.parameterCodeSkin] as String;
+
+    return GlobalSettingsState(
+      settings: GlobalSettings(
+        skin: skin,
+      ),
+    );
+  }
+
+  @override
+  Map<String, dynamic>? toJson(GlobalSettingsState state) {
+    return <String, dynamic>{
+      DefaultGlobalSettings.parameterCodeSkin: state.settings.skin,
+    };
+  }
+}
diff --git a/lib/cubit/settings_global_state.dart b/lib/cubit/settings_global_state.dart
new file mode 100644
index 0000000000000000000000000000000000000000..ebcddd700f252257223ca8e16c85202b04f3ff24
--- /dev/null
+++ b/lib/cubit/settings_global_state.dart
@@ -0,0 +1,15 @@
+part of 'settings_global_cubit.dart';
+
+@immutable
+class GlobalSettingsState extends Equatable {
+  const GlobalSettingsState({
+    required this.settings,
+  });
+
+  final GlobalSettings settings;
+
+  @override
+  List<dynamic> get props => <dynamic>[
+        settings,
+      ];
+}
diff --git a/lib/cubit/theme_cubit.dart b/lib/cubit/theme_cubit.dart
new file mode 100644
index 0000000000000000000000000000000000000000..b793e895dbb0c672d451cd403e0036c3d9ac9b42
--- /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 0000000000000000000000000000000000000000..e479a50f12fe72a35a1fd1722ff72afbb692a136
--- /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/entity/player.dart b/lib/entity/player.dart
deleted file mode 100644
index cef7f92cdc452487d61b7e6acbfda837020e75cf..0000000000000000000000000000000000000000
--- a/lib/entity/player.dart
+++ /dev/null
@@ -1,176 +0,0 @@
-import 'dart:math';
-
-import 'package:flutter/material.dart';
-
-import 'package:tetrisdual/entity/counter.dart';
-import 'package:tetrisdual/layout/board_painter.dart';
-import 'package:tetrisdual/provider/data.dart';
-
-class Player {
-  Player(this.playerId);
-  int playerId;
-
-  int _score = 0;
-  int _currentTetrimino = 0;
-  final Counter _counter = Counter();
-
-  Widget buildTetriminoWidget(Data myProvider, double width) {
-    return GestureDetector(
-      onTapUp: (details) {
-        if (playerId == myProvider.getCurrentPlayer().playerId) {
-          pickRandomTetrimino();
-          myProvider.redraw();
-        }
-      },
-      child: CustomPaint(
-        size: Size(width, width),
-        willChange: false,
-        painter: BoardPainter(_currentTetrimino),
-        isComplex: true,
-        key: Key(_currentTetrimino.toString()),
-      ),
-    );
-  }
-
-  Widget buildManagerWidget(Data myProvider) {
-    return Expanded(
-      child: Container(
-        margin: const EdgeInsets.all(5),
-        child: Column(
-          mainAxisAlignment: MainAxisAlignment.start,
-          crossAxisAlignment: CrossAxisAlignment.center,
-          children: myProvider.currentPlayer == playerId
-              ? [
-                  _counter.buildCounterWidget(myProvider),
-                  buildSubmitWidget(myProvider),
-                ]
-              : [],
-        ),
-      ),
-    );
-  }
-
-  Widget buildSubmitWidget(Data myProvider) {
-    const double gainFontSize = 70;
-
-    const gainTestStyle = TextStyle(
-      fontFamily: 'Blocks',
-      fontSize: gainFontSize,
-      fontWeight: FontWeight.bold,
-    );
-    const submitIcon = Icon(
-      Icons.done_all,
-      color: Colors.orange,
-      size: gainFontSize / 2,
-    );
-
-    const topBorderBlack = BoxDecoration(
-      border: Border(
-        top: BorderSide(
-          color: Colors.black,
-          width: 2,
-        ),
-      ),
-    );
-
-    return Container(
-      decoration: topBorderBlack,
-      padding: const EdgeInsets.only(
-        top: 10,
-      ),
-      child: Row(
-        mainAxisAlignment: MainAxisAlignment.end,
-        crossAxisAlignment: CrossAxisAlignment.center,
-        children: [
-          Text(
-            '+${_counter.computePoints()}',
-            style: gainTestStyle,
-            textHeightBehavior: const TextHeightBehavior(
-              applyHeightToFirstAscent: false,
-              applyHeightToLastDescent: false,
-            ),
-          ),
-          const SizedBox(width: 10),
-          IconButton(
-            padding: EdgeInsets.zero,
-            constraints: const BoxConstraints(),
-            style: const ButtonStyle(
-              tapTargetSize: MaterialTapTargetSize.shrinkWrap,
-            ),
-            icon: submitIcon,
-            onPressed: () {
-              _score = _score + _counter.computePoints();
-              _counter.reset();
-              myProvider.toggleCurrentPlayer();
-            },
-          ),
-          const SizedBox(width: 10),
-        ],
-      ),
-    );
-  }
-
-  Widget buildPlayerBoard(Data myProvider, double screenWidth, bool isActive) {
-    final double tetriminoBlockWidth = screenWidth / 2.3;
-    final Color borderColor = isActive ? Colors.greenAccent : Colors.blueGrey;
-    const double borderWidth = 10;
-
-    return Column(
-      mainAxisAlignment: MainAxisAlignment.center,
-      crossAxisAlignment: CrossAxisAlignment.end,
-      children: [
-        Text(
-          _score.toString(),
-          style: const TextStyle(
-            fontFamily: 'Blocks',
-            fontSize: 130,
-            fontWeight: FontWeight.bold,
-          ),
-          textHeightBehavior: const TextHeightBehavior(
-            applyHeightToFirstAscent: false,
-            applyHeightToLastDescent: false,
-          ),
-        ),
-        Container(
-          decoration: BoxDecoration(
-            border: Border.all(
-              color: borderColor,
-              width: borderWidth,
-            ),
-          ),
-          child: Row(
-            mainAxisAlignment: MainAxisAlignment.spaceBetween,
-            crossAxisAlignment: CrossAxisAlignment.center,
-            children: [
-              isActive
-                  ? buildTetriminoWidget(myProvider, tetriminoBlockWidth)
-                  : SizedBox(
-                      height: tetriminoBlockWidth,
-                    ),
-              buildManagerWidget(myProvider),
-            ],
-          ),
-        )
-      ],
-    );
-  }
-
-  void pickRandomTetrimino() {
-    // ensure new tetrimino is not same as current one
-    int newTetrimino = _currentTetrimino;
-    while (newTetrimino == _currentTetrimino) {
-      newTetrimino = Random().nextInt(5) + 1;
-    }
-
-    _currentTetrimino = newTetrimino;
-  }
-
-  void resetTetrimino() {
-    _currentTetrimino = 0;
-  }
-
-  void submitPoints() {
-    _score = _score + _counter.computePoints();
-    _counter.reset();
-  }
-}
diff --git a/lib/layout/board.dart b/lib/layout/board.dart
deleted file mode 100644
index b324c6ff4d0dfd583761f8e95a157ae889dfbf46..0000000000000000000000000000000000000000
--- a/lib/layout/board.dart
+++ /dev/null
@@ -1,46 +0,0 @@
-import 'package:flutter/material.dart';
-
-import 'package:tetrisdual/provider/data.dart';
-
-class Board {
-  static Widget buildGameBoard(Data myProvider, double screenWidth) {
-    final Widget player1 = RotatedBox(
-      quarterTurns: 2,
-      child: myProvider
-          .getPlayer(1)
-          .buildPlayerBoard(myProvider, screenWidth, myProvider.currentPlayer == 1),
-    );
-
-    final Widget player2 = myProvider
-        .getPlayer(2)
-        .buildPlayerBoard(myProvider, screenWidth, myProvider.currentPlayer == 2);
-
-    final Widget togglePlayerWidget = GestureDetector(
-      onTapUp: (details) {
-        myProvider.toggleCurrentPlayer();
-      },
-      child: const Text(
-        '🔄',
-        style: TextStyle(
-          fontSize: 50,
-        ),
-        textHeightBehavior: TextHeightBehavior(
-          applyHeightToFirstAscent: false,
-          applyHeightToLastDescent: false,
-        ),
-      ),
-    );
-
-    return Center(
-      child: Column(
-        mainAxisAlignment: MainAxisAlignment.spaceBetween,
-        crossAxisAlignment: CrossAxisAlignment.center,
-        children: [
-          player1,
-          togglePlayerWidget,
-          player2,
-        ],
-      ),
-    );
-  }
-}
diff --git a/lib/layout/game.dart b/lib/layout/game.dart
deleted file mode 100644
index b8a54c803d424b0059b7c509f2d2e82072d453cf..0000000000000000000000000000000000000000
--- a/lib/layout/game.dart
+++ /dev/null
@@ -1,58 +0,0 @@
-import 'package:flutter/material.dart';
-
-import 'package:tetrisdual/layout/board.dart';
-import 'package:tetrisdual/provider/data.dart';
-import 'package:tetrisdual/utils/game_utils.dart';
-
-class Game {
-  static Widget buildGameWidget(Data myProvider, double screenWidth) {
-    return !myProvider.isGameFinished
-        ? Board.buildGameBoard(myProvider, screenWidth)
-        : Game.buildEndGameMessage(myProvider);
-  }
-
-  static TextButton buildQuitGameButton(Data myProvider) {
-    return TextButton(
-      child: const Image(
-        image: AssetImage('assets/icons/button_back.png'),
-        fit: BoxFit.fill,
-      ),
-      onPressed: () => GameUtils.quitGame(myProvider),
-    );
-  }
-
-  static Container buildEndGameMessage(Data myProvider) {
-    const String decorationImageAssetName = 'assets/icons/game_fail.png';
-
-    final Widget decorationWidget = TextButton(
-      child: const Image(
-        image: AssetImage(decorationImageAssetName),
-        fit: BoxFit.fill,
-      ),
-      onPressed: () {},
-    );
-
-    return Container(
-      margin: const EdgeInsets.all(2),
-      padding: const EdgeInsets.all(2),
-      child: Table(
-        defaultColumnWidth: const IntrinsicColumnWidth(),
-        children: [
-          TableRow(
-            children: [
-              Column(
-                children: [decorationWidget],
-              ),
-              Column(
-                children: [buildQuitGameButton(myProvider)],
-              ),
-              Column(
-                children: [decorationWidget],
-              ),
-            ],
-          ),
-        ],
-      ),
-    );
-  }
-}
diff --git a/lib/layout/parameters.dart b/lib/layout/parameters.dart
deleted file mode 100644
index 97f81cfd2939f44abb01b4b754ca3a139b477f9a..0000000000000000000000000000000000000000
--- a/lib/layout/parameters.dart
+++ /dev/null
@@ -1,150 +0,0 @@
-import 'package:flutter/material.dart';
-
-import 'package:tetrisdual/provider/data.dart';
-import 'package:tetrisdual/utils/game_utils.dart';
-
-class Parameters {
-  static const double separatorHeight = 2.0;
-  static const double blockMargin = 3.0;
-  static const double blockPadding = 2.0;
-  static const Color buttonBackgroundColor = Colors.white;
-  static const Color buttonBorderColorActive = Colors.blue;
-  static const Color buttonBorderColorInactive = Colors.white;
-  static const double buttonBorderWidth = 10.0;
-  static const double buttonBorderRadius = 8.0;
-  static const double buttonPadding = 0.0;
-  static const double buttonMargin = 0.0;
-
-  static Widget buildParametersSelector(Data myProvider) {
-    final List<Widget> lines = [];
-
-    final List<String> parameters = myProvider.availableParameters;
-    for (int index = 0; index < parameters.length; index++) {
-      lines.add(buildParameterSelector(myProvider, parameters[index]));
-      lines.add(const SizedBox(height: separatorHeight));
-    }
-
-    final Widget buttonsBlock = buildStartNewGameButton(myProvider);
-
-    return Column(
-      mainAxisAlignment: MainAxisAlignment.start,
-      crossAxisAlignment: CrossAxisAlignment.center,
-      children: [
-        const SizedBox(height: separatorHeight),
-        Expanded(
-          child: Column(
-            mainAxisSize: MainAxisSize.min,
-            mainAxisAlignment: MainAxisAlignment.center,
-            children: lines,
-          ),
-        ),
-        const SizedBox(height: separatorHeight),
-        Container(
-          child: buttonsBlock,
-        ),
-      ],
-    );
-  }
-
-  static Image buildImageWidget(String imageAssetCode) {
-    return Image(
-      image: AssetImage('assets/icons/$imageAssetCode.png'),
-      fit: BoxFit.fill,
-    );
-  }
-
-  static Container buildImageContainerWidget(String imageAssetCode) {
-    return Container(
-      child: buildImageWidget(imageAssetCode),
-    );
-  }
-
-  static Column buildDecorationImageWidget() {
-    return Column(
-      children: [
-        TextButton(
-          child: buildImageContainerWidget('placeholder'),
-          onPressed: () {},
-        ),
-      ],
-    );
-  }
-
-  static Container buildStartNewGameButton(Data myProvider) {
-    return Container(
-      margin: const EdgeInsets.all(blockMargin),
-      padding: const EdgeInsets.all(blockPadding),
-      child: Table(
-        defaultColumnWidth: const IntrinsicColumnWidth(),
-        children: [
-          TableRow(
-            children: [
-              buildDecorationImageWidget(),
-              Column(
-                children: [
-                  TextButton(
-                    child: buildImageContainerWidget('button_start'),
-                    onPressed: () => GameUtils.startNewGame(myProvider),
-                  ),
-                ],
-              ),
-              buildDecorationImageWidget(),
-            ],
-          ),
-        ],
-      ),
-    );
-  }
-
-  static Widget buildParameterSelector(Data myProvider, String parameterCode) {
-    final List<String> availableValues = myProvider.getParameterAvailableValues(parameterCode);
-
-    if (availableValues.length == 1) {
-      return const SizedBox(height: 0.0);
-    }
-
-    return Table(
-      defaultColumnWidth: const IntrinsicColumnWidth(),
-      children: [
-        TableRow(
-          children: [
-            for (int index = 0; index < availableValues.length; index++)
-              Column(
-                children: [
-                  _buildParameterButton(myProvider, parameterCode, availableValues[index])
-                ],
-              ),
-          ],
-        ),
-      ],
-    );
-  }
-
-  static Widget _buildParameterButton(
-    Data myProvider,
-    String parameterCode,
-    String parameterValue,
-  ) {
-    final String currentValue = myProvider.getParameterValue(parameterCode).toString();
-
-    final bool isActive = (parameterValue == currentValue);
-    final String imageAsset = '${parameterCode}_$parameterValue';
-
-    return TextButton(
-      child: Container(
-        margin: const EdgeInsets.all(buttonMargin),
-        padding: const EdgeInsets.all(buttonPadding),
-        decoration: BoxDecoration(
-          color: buttonBackgroundColor,
-          borderRadius: BorderRadius.circular(buttonBorderRadius),
-          border: Border.all(
-            color: isActive ? buttonBorderColorActive : buttonBorderColorInactive,
-            width: buttonBorderWidth,
-          ),
-        ),
-        child: buildImageWidget(imageAsset),
-      ),
-      onPressed: () => myProvider.setParameterValue(parameterCode, parameterValue),
-    );
-  }
-}
diff --git a/lib/main.dart b/lib/main.dart
index 121d1109f25290bd210df2442c310a169db8ab28..5677b41cb14516a35418cfff53d60eacbe5382b0 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,15 +1,42 @@
+import 'dart:io';
+
+import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
-import 'package:provider/provider.dart';
-import 'package:overlay_support/overlay_support.dart';
+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:tetrisdual/provider/data.dart';
-import 'package:tetrisdual/screens/home.dart';
+import 'package:tetrisdual/config/theme.dart';
+import 'package:tetrisdual/cubit/game_cubit.dart';
+import 'package:tetrisdual/cubit/nav_cubit.dart';
+import 'package:tetrisdual/cubit/settings_game_cubit.dart';
+import 'package:tetrisdual/cubit/settings_global_cubit.dart';
+import 'package:tetrisdual/cubit/theme_cubit.dart';
+import 'package:tetrisdual/ui/skeleton.dart';
 
-void main() {
+void main() async {
+  // Initialize packages
   WidgetsFlutterBinding.ensureInitialized();
+  await EasyLocalization.ensureInitialized();
+  final Directory tmpDir = await getTemporaryDirectory();
+  Hive.init(tmpDir.toString());
+  HydratedBloc.storage = await HydratedStorage.build(
+    storageDirectory: tmpDir,
+  );
+
   SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp])
-      .then((value) => runApp(const MyApp()));
+      .then((value) => runApp(EasyLocalization(
+            path: 'assets/translations',
+            supportedLocales: const <Locale>[
+              Locale('en'),
+              Locale('fr'),
+            ],
+            fallbackLocale: const Locale('en'),
+            useFallbackTranslations: true,
+            child: const MyApp(),
+          )));
 }
 
 class MyApp extends StatelessWidget {
@@ -17,22 +44,56 @@ class MyApp extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    return ChangeNotifierProvider(
-      create: (BuildContext context) => Data(),
-      child: Consumer<Data>(builder: (context, data, child) {
-        return OverlaySupport(
-          child: MaterialApp(
-            theme: ThemeData(
-              primaryColor: Colors.blue,
-              visualDensity: VisualDensity.adaptivePlatformDensity,
-            ),
-            home: const Home(),
-            routes: {
-              Home.id: (context) => const Home(),
-            },
-          ),
-        );
-      }),
+    final List<String> assets = getImagesAssets();
+    for (String asset in assets) {
+      precacheImage(AssetImage(asset), context);
+    }
+
+    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: BlocBuilder<ThemeCubit, ThemeModeState>(
+        builder: (BuildContext context, ThemeModeState state) {
+          return MaterialApp(
+            title: 'Tetris',
+            home: const SkeletonScreen(),
+
+            // Theme stuff
+            theme: lightTheme,
+            darkTheme: darkTheme,
+            themeMode: state.themeMode,
+
+            // Localization stuff
+            localizationsDelegates: context.localizationDelegates,
+            supportedLocales: context.supportedLocales,
+            locale: context.locale,
+            debugShowCheckedModeBanner: false,
+          );
+        },
+      ),
     );
   }
+
+  List<String> getImagesAssets() {
+    final List<String> assets = [];
+
+    final List<String> gameImages = [
+      'button_back',
+      'button_delete_saved_game',
+      'button_resume_game',
+      'button_start',
+      'placeholder',
+    ];
+
+    for (String image in gameImages) {
+      assets.add('assets/ui/$image.png');
+    }
+
+    return assets;
+  }
 }
diff --git a/lib/models/game/counter.dart b/lib/models/game/counter.dart
new file mode 100644
index 0000000000000000000000000000000000000000..b991b9e239c8be90968b5fd57de60ce13267bd05
--- /dev/null
+++ b/lib/models/game/counter.dart
@@ -0,0 +1,35 @@
+class Counter {
+  // Current counter
+  bool touch = false; // Does this new tetrimino touch an other tetrimino of same player
+  int lines = 0; // Count lines fully filled by this new tetrimino
+  int holes = 0; // Count non fillable holes caused by this new tetrimino
+
+  // Points definitions
+  static const int base = 50;
+  static const int pointsIfTouch = 50;
+  static const int pointsPerLine = 60;
+  static const int pointsPerHole = -10;
+
+  int computePoints() {
+    return base + (touch ? pointsIfTouch : 0) + lines * pointsPerLine + holes * pointsPerHole;
+  }
+
+  void reset() {
+    touch = false;
+    lines = 0;
+    holes = 0;
+  }
+
+  @override
+  String toString() {
+    return '$Counter(${toJson()})';
+  }
+
+  Map<String, dynamic> toJson() {
+    return {
+      'touch': touch,
+      'lines': lines,
+      'holes': holes,
+    };
+  }
+}
diff --git a/lib/models/game/game.dart b/lib/models/game/game.dart
new file mode 100644
index 0000000000000000000000000000000000000000..afb0a573f5a4c284656073388421562bfb4c35ac
--- /dev/null
+++ b/lib/models/game/game.dart
@@ -0,0 +1,115 @@
+import 'package:tetrisdual/models/game/player.dart';
+import 'package:tetrisdual/models/settings/settings_game.dart';
+import 'package:tetrisdual/models/settings/settings_global.dart';
+import 'package:tetrisdual/utils/tools.dart';
+
+class Game {
+  Game({
+    // Settings
+    required this.gameSettings,
+    required this.globalSettings,
+
+    // State
+    this.isRunning = false,
+    this.isStarted = false,
+    this.isFinished = false,
+    this.animationInProgress = false,
+
+    // Base data
+    this.players = const [],
+
+    // Game data
+    this.currentPlayer = 0,
+  });
+
+  // Settings
+  final GameSettings gameSettings;
+  final GlobalSettings globalSettings;
+
+  // State
+  bool isRunning;
+  bool isStarted;
+  bool isFinished;
+  bool animationInProgress;
+
+  // Base data
+  List<Player> players;
+
+  // Game data
+  int currentPlayer;
+
+  factory Game.createEmpty() {
+    return Game(
+      // Settings
+      gameSettings: GameSettings.createDefault(),
+      globalSettings: GlobalSettings.createDefault(),
+    );
+  }
+
+  factory Game.createNew({
+    GameSettings? gameSettings,
+    GlobalSettings? globalSettings,
+  }) {
+    final GameSettings newGameSettings = gameSettings ?? GameSettings.createDefault();
+    final GlobalSettings newGlobalSettings = globalSettings ?? GlobalSettings.createDefault();
+
+    return Game(
+      // Settings
+      gameSettings: newGameSettings,
+      globalSettings: newGlobalSettings,
+      // State
+      isRunning: true,
+      // Base data
+      players: [
+        Player(1),
+        Player(2),
+      ],
+      // Game data
+      currentPlayer: 0,
+    );
+  }
+
+  bool get canBeResumed => isStarted && !isFinished;
+
+  void dump() {
+    printlog('');
+    printlog('## Current game dump:');
+    printlog('');
+    printlog('$Game:');
+    printlog('  Settings');
+    gameSettings.dump();
+    globalSettings.dump();
+    printlog('  State');
+    printlog('    isRunning: $isRunning');
+    printlog('    isStarted: $isStarted');
+    printlog('    isFinished: $isFinished');
+    printlog('    animationInProgress: $animationInProgress');
+    printlog('  Base data');
+    printlog('    players: $players');
+    printlog('  Game data');
+    printlog('    currentPlayer: $currentPlayer');
+    printlog('');
+  }
+
+  @override
+  String toString() {
+    return '$Game(${toJson()})';
+  }
+
+  Map<String, dynamic>? toJson() {
+    return <String, dynamic>{
+      // Settings
+      'gameSettings': gameSettings.toJson(),
+      'globalSettings': globalSettings.toJson(),
+      // State
+      'isRunning': isRunning,
+      'isStarted': isStarted,
+      'isFinished': isFinished,
+      'animationInProgress': animationInProgress,
+      // Base data
+      'players': players,
+      // Game data
+      'currentPlayer': currentPlayer,
+    };
+  }
+}
diff --git a/lib/models/game/player.dart b/lib/models/game/player.dart
new file mode 100644
index 0000000000000000000000000000000000000000..4d5aed113bb74fab5d0b8dbb336082e168754656
--- /dev/null
+++ b/lib/models/game/player.dart
@@ -0,0 +1,45 @@
+import 'dart:math';
+
+import 'package:tetrisdual/models/game/counter.dart';
+
+class Player {
+  Player(this.playerId);
+  int playerId;
+
+  int score = 0;
+  int currentTetrimino = 0;
+  final Counter counter = Counter();
+
+  void pickRandomTetrimino() {
+    // ensure new tetrimino is not same as current one
+    int newTetrimino = currentTetrimino;
+    while (newTetrimino == currentTetrimino) {
+      newTetrimino = Random().nextInt(5) + 1;
+    }
+
+    currentTetrimino = newTetrimino;
+  }
+
+  void resetTetrimino() {
+    currentTetrimino = 0;
+  }
+
+  void submitPoints() {
+    score = score + counter.computePoints();
+    counter.reset();
+  }
+
+  @override
+  String toString() {
+    return '$Player(${toJson()})';
+  }
+
+  Map<String, dynamic> toJson() {
+    return {
+      'playerId': playerId,
+      'score': score,
+      'currentTetrimino': currentTetrimino,
+      'counter': counter,
+    };
+  }
+}
diff --git a/lib/models/settings/settings_game.dart b/lib/models/settings/settings_game.dart
new file mode 100644
index 0000000000000000000000000000000000000000..e94f857dc46a4e148f6c85891497cd7167d0284b
--- /dev/null
+++ b/lib/models/settings/settings_game.dart
@@ -0,0 +1,41 @@
+import 'package:tetrisdual/config/default_game_settings.dart';
+import 'package:tetrisdual/utils/tools.dart';
+
+class GameSettings {
+  final String gameType;
+
+  GameSettings({
+    required this.gameType,
+  });
+
+  static String getGameTypeValueFromUnsafe(String gameType) {
+    if (DefaultGameSettings.allowedGameTypeValues.contains(gameType)) {
+      return gameType;
+    }
+
+    return DefaultGameSettings.defaultGameTypeValue;
+  }
+
+  factory GameSettings.createDefault() {
+    return GameSettings(
+      gameType: DefaultGameSettings.defaultGameTypeValue,
+    );
+  }
+
+  void dump() {
+    printlog('$GameSettings:');
+    printlog('  ${DefaultGameSettings.parameterCodeGameType}: $gameType');
+    printlog('');
+  }
+
+  @override
+  String toString() {
+    return '$GameSettings(${toJson()})';
+  }
+
+  Map<String, dynamic>? toJson() {
+    return <String, dynamic>{
+      DefaultGameSettings.parameterCodeGameType: gameType,
+    };
+  }
+}
diff --git a/lib/models/settings/settings_global.dart b/lib/models/settings/settings_global.dart
new file mode 100644
index 0000000000000000000000000000000000000000..ac327d732f6bfd50de5900a3f39e10e6b0785c14
--- /dev/null
+++ b/lib/models/settings/settings_global.dart
@@ -0,0 +1,41 @@
+import 'package:tetrisdual/config/default_global_settings.dart';
+import 'package:tetrisdual/utils/tools.dart';
+
+class GlobalSettings {
+  String skin;
+
+  GlobalSettings({
+    required this.skin,
+  });
+
+  static String getSkinValueFromUnsafe(String skin) {
+    if (DefaultGlobalSettings.allowedSkinValues.contains(skin)) {
+      return skin;
+    }
+
+    return DefaultGlobalSettings.defaultSkinValue;
+  }
+
+  factory GlobalSettings.createDefault() {
+    return GlobalSettings(
+      skin: DefaultGlobalSettings.defaultSkinValue,
+    );
+  }
+
+  void dump() {
+    printlog('$GlobalSettings:');
+    printlog('  ${DefaultGlobalSettings.parameterCodeSkin}: $skin');
+    printlog('');
+  }
+
+  @override
+  String toString() {
+    return '$GlobalSettings(${toJson()})';
+  }
+
+  Map<String, dynamic>? toJson() {
+    return <String, dynamic>{
+      DefaultGlobalSettings.parameterCodeSkin: skin,
+    };
+  }
+}
diff --git a/lib/provider/data.dart b/lib/provider/data.dart
deleted file mode 100644
index b1f4ad0e5d4c79c0252a7762b9fa900f5da04412..0000000000000000000000000000000000000000
--- a/lib/provider/data.dart
+++ /dev/null
@@ -1,102 +0,0 @@
-import 'dart:math';
-
-import 'package:flutter/foundation.dart';
-import 'package:shared_preferences/shared_preferences.dart';
-
-import 'package:tetrisdual/entity/player.dart';
-
-class Data extends ChangeNotifier {
-  // Configuration available parameters
-  final List<String> _availableParameters = [];
-
-  List<String> get availableParameters => _availableParameters;
-
-  // Application default configuration
-
-  // Application current configuration
-  String getParameterValue(String parameterCode) {
-    switch (parameterCode) {}
-    return '';
-  }
-
-  List<String> getParameterAvailableValues(String parameterCode) {
-    switch (parameterCode) {}
-    return [];
-  }
-
-  void setParameterValue(String parameterCode, String parameterValue) async {
-    switch (parameterCode) {}
-    final prefs = await SharedPreferences.getInstance();
-    prefs.setString(parameterCode, parameterValue);
-  }
-
-  // Game data
-  bool _gameIsRunning = false;
-  bool _gameIsFinished = false;
-  int _currentPlayer = 0;
-  List<Player?> _players = [null, null];
-
-  bool get isGameRunning => _gameIsRunning;
-  void updateGameIsRunning(bool gameIsRunning) {
-    _gameIsRunning = gameIsRunning;
-    notifyListeners();
-  }
-
-  bool get isGameFinished => _gameIsFinished;
-  void updateGameIsFinished(bool gameIsFinished) {
-    _gameIsFinished = gameIsFinished;
-    notifyListeners();
-  }
-
-  int get currentPlayer => _currentPlayer;
-  void toggleCurrentPlayer() {
-    if (_currentPlayer == 0) {
-      // start game
-      _currentPlayer = 1;
-    } else {
-      // Reset current player tetrimino
-      getCurrentPlayer().resetTetrimino();
-
-      // 1 -> 2 ; 2 -> 1
-      _currentPlayer = 3 - _currentPlayer;
-    }
-
-    // Pick new tetrimino
-    getCurrentPlayer().pickRandomTetrimino();
-
-    notifyListeners();
-  }
-
-  void enableRandomPlayer() {
-    _currentPlayer = Random().nextInt(2) + 1;
-    toggleCurrentPlayer();
-  }
-
-  Player getPlayer(int playerId) {
-    final int playerIndex = playerId - 1;
-    Player? player = _players[playerIndex];
-
-    // Create new player if none
-    if (null == player) {
-      player = Player(playerId);
-      _players[playerIndex] = player;
-    }
-
-    return player;
-  }
-
-  Player getCurrentPlayer() {
-    return getPlayer(currentPlayer);
-  }
-
-  void redraw() {
-    notifyListeners();
-  }
-
-  void resetGame() {
-    _gameIsRunning = false;
-    _gameIsFinished = false;
-    _players = [Player(1), Player(2)];
-    notifyListeners();
-  }
-}
diff --git a/lib/screens/home.dart b/lib/screens/home.dart
deleted file mode 100644
index 34cdc098057ec2fe06739f673a9aec55382fbbf2..0000000000000000000000000000000000000000
--- a/lib/screens/home.dart
+++ /dev/null
@@ -1,52 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:provider/provider.dart';
-import 'package:overlay_support/overlay_support.dart';
-
-import 'package:tetrisdual/layout/game.dart';
-import 'package:tetrisdual/layout/parameters.dart';
-import 'package:tetrisdual/provider/data.dart';
-import 'package:tetrisdual/utils/game_utils.dart';
-
-class Home extends StatefulWidget {
-  const Home({super.key});
-
-  static const String id = 'home';
-
-  @override
-  HomeState createState() => HomeState();
-}
-
-class HomeState extends State<Home> {
-  @override
-  void initState() {
-    super.initState();
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    final Data myProvider = Provider.of<Data>(context);
-    final double screenWidth = MediaQuery.of(context).size.width;
-
-    final List<Widget> menuActions = [];
-
-    if (myProvider.isGameRunning) {
-      menuActions.add(TextButton(
-        child: const Image(
-          image: AssetImage('assets/icons/button_back.png'),
-          fit: BoxFit.fill,
-        ),
-        onPressed: () => toast('Long press to quit game...'),
-        onLongPress: () => GameUtils.quitGame(myProvider),
-      ));
-    }
-
-    return Scaffold(
-      appBar: AppBar(
-        actions: menuActions,
-      ),
-      body: myProvider.isGameRunning
-          ? Game.buildGameWidget(myProvider, screenWidth)
-          : Parameters.buildParametersSelector(myProvider),
-    );
-  }
-}
diff --git a/lib/ui/helpers/app_titles.dart b/lib/ui/helpers/app_titles.dart
new file mode 100644
index 0000000000000000000000000000000000000000..b98107b12fabc3114ebfbec994166b588abcf1ad
--- /dev/null
+++ b/lib/ui/helpers/app_titles.dart
@@ -0,0 +1,32 @@
+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 Text(
+      tr(text),
+      textAlign: TextAlign.start,
+      style: Theme.of(context).textTheme.headlineMedium!.apply(fontWeightDelta: 2),
+    );
+  }
+}
+
+class AppTitle extends StatelessWidget {
+  const AppTitle({super.key, required this.text});
+
+  final String text;
+
+  @override
+  Widget build(BuildContext context) {
+    return Text(
+      tr(text),
+      textAlign: TextAlign.start,
+      style: Theme.of(context).textTheme.titleLarge!.apply(fontWeightDelta: 2),
+    );
+  }
+}
diff --git a/lib/ui/helpers/outlined_text_widget.dart b/lib/ui/helpers/outlined_text_widget.dart
new file mode 100644
index 0000000000000000000000000000000000000000..7cecab58dec9b861d573ae93127ac27387e22d6a
--- /dev/null
+++ b/lib/ui/helpers/outlined_text_widget.dart
@@ -0,0 +1,51 @@
+import 'package:flutter/material.dart';
+
+import 'package:tetrisdual/utils/color_extensions.dart';
+
+class OutlinedText extends StatelessWidget {
+  const OutlinedText({
+    super.key,
+    required this.text,
+    required this.fontSize,
+    required this.textColor,
+    this.outlineColor,
+  });
+
+  final String text;
+  final double fontSize;
+  final Color textColor;
+  final Color? outlineColor;
+
+  @override
+  Widget build(BuildContext context) {
+    final double delta = fontSize / 30;
+
+    return Text(
+      text,
+      style: TextStyle(
+        inherit: true,
+        fontSize: fontSize,
+        fontWeight: FontWeight.w600,
+        color: textColor,
+        shadows: [
+          Shadow(
+            offset: Offset(-delta, -delta),
+            color: outlineColor ?? textColor.darken(),
+          ),
+          Shadow(
+            offset: Offset(delta, -delta),
+            color: outlineColor ?? textColor.darken(),
+          ),
+          Shadow(
+            offset: Offset(delta, delta),
+            color: outlineColor ?? textColor.darken(),
+          ),
+          Shadow(
+            offset: Offset(-delta, delta),
+            color: outlineColor ?? textColor.darken(),
+          ),
+        ],
+      ),
+    );
+  }
+}
diff --git a/lib/ui/layouts/game_layout.dart b/lib/ui/layouts/game_layout.dart
new file mode 100644
index 0000000000000000000000000000000000000000..d49b3119d04bd0500e34ad003d100013b83ca2ea
--- /dev/null
+++ b/lib/ui/layouts/game_layout.dart
@@ -0,0 +1,12 @@
+import 'package:flutter/material.dart';
+
+import 'package:tetrisdual/ui/widgets/game/game_board.dart';
+
+class GameLayout extends StatelessWidget {
+  const GameLayout({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return const GameBoardWidget();
+  }
+}
diff --git a/lib/ui/layouts/parameters_layout.dart b/lib/ui/layouts/parameters_layout.dart
new file mode 100644
index 0000000000000000000000000000000000000000..87c48ad8562d6d67b482a11074d4fb55c177cc77
--- /dev/null
+++ b/lib/ui/layouts/parameters_layout.dart
@@ -0,0 +1,154 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:tetrisdual/config/default_game_settings.dart';
+import 'package:tetrisdual/config/default_global_settings.dart';
+import 'package:tetrisdual/cubit/settings_game_cubit.dart';
+import 'package:tetrisdual/cubit/settings_global_cubit.dart';
+import 'package:tetrisdual/ui/parameters/parameter_image.dart';
+import 'package:tetrisdual/ui/parameters/parameter_painter.dart';
+import 'package:tetrisdual/ui/widgets/actions/button_delete_saved_game.dart';
+import 'package:tetrisdual/ui/widgets/actions/button_game_start_new.dart';
+import 'package:tetrisdual/ui/widgets/actions/button_resume_saved_game.dart';
+
+class ParametersLayout extends StatelessWidget {
+  const ParametersLayout({super.key, required this.canResume});
+
+  final bool canResume;
+
+  final double separatorHeight = 8.0;
+
+  @override
+  Widget build(BuildContext context) {
+    final List<Widget> lines = [];
+
+    // Game settings
+    for (String code in DefaultGameSettings.availableParameters) {
+      lines.add(Row(
+        mainAxisAlignment: MainAxisAlignment.spaceBetween,
+        children: buildParametersLine(
+          code: code,
+          isGlobal: false,
+        ),
+      ));
+
+      lines.add(SizedBox(height: separatorHeight));
+    }
+
+    lines.add(SizedBox(height: separatorHeight));
+
+    if (canResume == false) {
+      // Start new game
+      lines.add(const Expanded(
+        child: StartNewGameButton(),
+      ));
+    } else {
+      // Resume game
+      lines.add(const Expanded(
+        child: ResumeSavedGameButton(),
+      ));
+      // Delete saved game
+      lines.add(SizedBox.square(
+        dimension: MediaQuery.of(context).size.width / 4,
+        child: const DeleteSavedGameButton(),
+      ));
+    }
+
+    lines.add(SizedBox(height: separatorHeight));
+
+    // Global settings
+    for (String code in DefaultGlobalSettings.availableParameters) {
+      lines.add(Row(
+        mainAxisAlignment: MainAxisAlignment.spaceBetween,
+        children: buildParametersLine(
+          code: code,
+          isGlobal: true,
+        ),
+      ));
+
+      lines.add(SizedBox(height: separatorHeight));
+    }
+
+    return Column(
+      children: lines,
+    );
+  }
+
+  List<Widget> buildParametersLine({
+    required String code,
+    required bool isGlobal,
+  }) {
+    final List<Widget> parameterButtons = [];
+
+    final List<String> availableValues = isGlobal
+        ? DefaultGlobalSettings.getAvailableValues(code)
+        : DefaultGameSettings.getAvailableValues(code);
+
+    if (availableValues.length <= 1) {
+      return [];
+    }
+
+    for (String value in availableValues) {
+      final Widget parameterButton = BlocBuilder<GameSettingsCubit, GameSettingsState>(
+        builder: (BuildContext context, GameSettingsState gameSettingsState) {
+          return BlocBuilder<GlobalSettingsCubit, GlobalSettingsState>(
+            builder: (BuildContext context, GlobalSettingsState globalSettingsState) {
+              final GameSettingsCubit gameSettingsCubit =
+                  BlocProvider.of<GameSettingsCubit>(context);
+              final GlobalSettingsCubit globalSettingsCubit =
+                  BlocProvider.of<GlobalSettingsCubit>(context);
+
+              final String currentValue = isGlobal
+                  ? globalSettingsCubit.getParameterValue(code)
+                  : gameSettingsCubit.getParameterValue(code);
+
+              final bool isActive = (value == currentValue);
+
+              final double displayWidth = MediaQuery.of(context).size.width;
+              final double itemWidth = displayWidth / availableValues.length - 26;
+
+              final bool displayedWithAssets =
+                  DefaultGlobalSettings.displayedWithAssets.contains(code) ||
+                      DefaultGameSettings.displayedWithAssets.contains(code);
+
+              return TextButton(
+                child: Container(
+                  child: displayedWithAssets
+                      ? SizedBox.square(
+                          dimension: itemWidth,
+                          child: ParameterImage(
+                            code: code,
+                            value: value,
+                            isSelected: isActive,
+                          ),
+                        )
+                      : CustomPaint(
+                          size: Size(itemWidth, itemWidth),
+                          willChange: false,
+                          painter: ParameterPainter(
+                            code: code,
+                            value: value,
+                            isSelected: isActive,
+                            gameSettings: gameSettingsState.settings,
+                            globalSettings: globalSettingsState.settings,
+                          ),
+                          isComplex: true,
+                        ),
+                ),
+                onPressed: () {
+                  isGlobal
+                      ? globalSettingsCubit.setParameterValue(code, value)
+                      : gameSettingsCubit.setParameterValue(code, value);
+                },
+              );
+            },
+          );
+        },
+      );
+
+      parameterButtons.add(parameterButton);
+    }
+
+    return parameterButtons;
+  }
+}
diff --git a/lib/ui/parameters/parameter_image.dart b/lib/ui/parameters/parameter_image.dart
new file mode 100644
index 0000000000000000000000000000000000000000..fc4b576f85b01158b74548400d11a4d027c57fbe
--- /dev/null
+++ b/lib/ui/parameters/parameter_image.dart
@@ -0,0 +1,38 @@
+import 'package:flutter/material.dart';
+
+class ParameterImage extends StatelessWidget {
+  const ParameterImage({
+    super.key,
+    required this.code,
+    required this.value,
+    required this.isSelected,
+  });
+
+  final String code;
+  final String value;
+  final bool isSelected;
+
+  static const Color buttonBackgroundColor = Colors.white;
+  static const Color buttonBorderColorActive = Colors.blue;
+  static const Color buttonBorderColorInactive = Colors.white;
+  static const double buttonBorderWidth = 8.0;
+  static const double buttonBorderRadius = 8.0;
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      decoration: BoxDecoration(
+        color: buttonBackgroundColor,
+        borderRadius: BorderRadius.circular(buttonBorderRadius),
+        border: Border.all(
+          color: isSelected ? buttonBorderColorActive : buttonBorderColorInactive,
+          width: buttonBorderWidth,
+        ),
+      ),
+      child: Image(
+        image: AssetImage('assets/ui/${code}_$value.png'),
+        fit: BoxFit.fill,
+      ),
+    );
+  }
+}
diff --git a/lib/ui/parameters/parameter_painter.dart b/lib/ui/parameters/parameter_painter.dart
new file mode 100644
index 0000000000000000000000000000000000000000..b4f633bef1dbf286c582376011caa51e92e8bf07
--- /dev/null
+++ b/lib/ui/parameters/parameter_painter.dart
@@ -0,0 +1,90 @@
+import 'dart:math';
+
+import 'package:flutter/material.dart';
+
+import 'package:tetrisdual/models/settings/settings_game.dart';
+import 'package:tetrisdual/models/settings/settings_global.dart';
+import 'package:tetrisdual/utils/tools.dart';
+
+class ParameterPainter extends CustomPainter {
+  const ParameterPainter({
+    required this.code,
+    required this.value,
+    required this.isSelected,
+    required this.gameSettings,
+    required this.globalSettings,
+  });
+
+  final String code;
+  final String value;
+  final bool isSelected;
+  final GameSettings gameSettings;
+  final GlobalSettings globalSettings;
+
+  @override
+  void paint(Canvas canvas, Size size) {
+    // force square
+    final double canvasSize = min(size.width, size.height);
+
+    const Color borderColorEnabled = Colors.blue;
+    const Color borderColorDisabled = Colors.white;
+
+    // "enabled/disabled" border
+    final paint = Paint();
+    paint.style = PaintingStyle.stroke;
+    paint.color = isSelected ? borderColorEnabled : borderColorDisabled;
+    paint.strokeJoin = StrokeJoin.round;
+    paint.strokeWidth = 10;
+    canvas.drawRect(
+        Rect.fromPoints(const Offset(0, 0), Offset(canvasSize, canvasSize)), paint);
+
+    // content
+    switch (code) {
+      default:
+        printlog('Unknown parameter: $code/$value');
+        paintUnknownParameterItem(value, canvas, canvasSize);
+    }
+  }
+
+  @override
+  bool shouldRepaint(CustomPainter oldDelegate) {
+    return false;
+  }
+
+  // "unknown" parameter -> simple block with text
+  void paintUnknownParameterItem(
+    final String value,
+    final Canvas canvas,
+    final double size,
+  ) {
+    final paint = Paint();
+    paint.strokeJoin = StrokeJoin.round;
+    paint.strokeWidth = 3;
+
+    paint.color = Colors.grey;
+    paint.style = PaintingStyle.fill;
+    canvas.drawRect(Rect.fromPoints(const Offset(0, 0), Offset(size, size)), paint);
+
+    final textSpan = TextSpan(
+      text: '?\n$value',
+      style: const TextStyle(
+        color: Colors.black,
+        fontSize: 18,
+        fontWeight: FontWeight.bold,
+      ),
+    );
+    final textPainter = TextPainter(
+      text: textSpan,
+      textDirection: TextDirection.ltr,
+      textAlign: TextAlign.center,
+    );
+    textPainter.layout();
+    textPainter.paint(
+      canvas,
+      Offset(
+        (size - textPainter.width) * 0.5,
+        (size - textPainter.height) * 0.5,
+      ),
+    );
+  }
+}
diff --git a/lib/ui/screens/page_about.dart b/lib/ui/screens/page_about.dart
new file mode 100644
index 0000000000000000000000000000000000000000..284a45b0cdb6ea2220584a303903783f9c5f709b
--- /dev/null
+++ b/lib/ui/screens/page_about.dart
@@ -0,0 +1,41 @@
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:package_info_plus/package_info_plus.dart';
+
+import 'package:tetrisdual/ui/helpers/app_titles.dart';
+
+class PageAbout extends StatelessWidget {
+  const PageAbout({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: const EdgeInsets.symmetric(horizontal: 8),
+      child: Column(
+        mainAxisAlignment: MainAxisAlignment.start,
+        crossAxisAlignment: CrossAxisAlignment.start,
+        mainAxisSize: MainAxisSize.max,
+        children: <Widget>[
+          const SizedBox(height: 8),
+          const AppTitle(text: 'about_title'),
+          const Text('about_content').tr(),
+          FutureBuilder<PackageInfo>(
+            future: PackageInfo.fromPlatform(),
+            builder: (context, snapshot) {
+              switch (snapshot.connectionState) {
+                case ConnectionState.done:
+                  return const Text('about_version').tr(
+                    namedArgs: {
+                      'version': snapshot.data!.version,
+                    },
+                  );
+                default:
+                  return const SizedBox();
+              }
+            },
+          ),
+        ],
+      ),
+    );
+  }
+}
diff --git a/lib/ui/screens/page_game.dart b/lib/ui/screens/page_game.dart
new file mode 100644
index 0000000000000000000000000000000000000000..8bf1a663aa5c8347a268351a5f6437a1bfca5209
--- /dev/null
+++ b/lib/ui/screens/page_game.dart
@@ -0,0 +1,24 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:tetrisdual/cubit/game_cubit.dart';
+import 'package:tetrisdual/models/game/game.dart';
+import 'package:tetrisdual/ui/layouts/game_layout.dart';
+import 'package:tetrisdual/ui/layouts/parameters_layout.dart';
+
+class PageGame extends StatelessWidget {
+  const PageGame({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<GameCubit, GameState>(
+      builder: (BuildContext context, GameState gameState) {
+        final Game currentGame = gameState.currentGame;
+
+        return currentGame.isRunning
+            ? const GameLayout()
+            : ParametersLayout(canResume: currentGame.canBeResumed);
+      },
+    );
+  }
+}
diff --git a/lib/ui/screens/page_settings.dart b/lib/ui/screens/page_settings.dart
new file mode 100644
index 0000000000000000000000000000000000000000..1813ba69b216956034a94631aebed2a21a8141bb
--- /dev/null
+++ b/lib/ui/screens/page_settings.dart
@@ -0,0 +1,26 @@
+import 'package:flutter/material.dart';
+
+import 'package:tetrisdual/ui/helpers/app_titles.dart';
+import 'package:tetrisdual/ui/settings/settings_form.dart';
+
+class PageSettings extends StatelessWidget {
+  const PageSettings({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),
+          AppTitle(text: 'settings_title'),
+          SizedBox(height: 8),
+          SettingsForm(),
+        ],
+      ),
+    );
+  }
+}
diff --git a/lib/ui/settings/settings_form.dart b/lib/ui/settings/settings_form.dart
new file mode 100644
index 0000000000000000000000000000000000000000..55b4a7f3e1a13c7ab667bd7639efc0104bc0317c
--- /dev/null
+++ b/lib/ui/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:tetrisdual/ui/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/settings/theme_card.dart b/lib/ui/settings/theme_card.dart
new file mode 100644
index 0000000000000000000000000000000000000000..6aef993934224bb47e87940ade39dcc3e6fd5502
--- /dev/null
+++ b/lib/ui/settings/theme_card.dart
@@ -0,0 +1,47 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:tetrisdual/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/lib/ui/skeleton.dart b/lib/ui/skeleton.dart
new file mode 100644
index 0000000000000000000000000000000000000000..0e4b38fceaac1e8e3ab1819d1cee06e752436d6c
--- /dev/null
+++ b/lib/ui/skeleton.dart
@@ -0,0 +1,34 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:tetrisdual/config/menu.dart';
+import 'package:tetrisdual/cubit/nav_cubit.dart';
+import 'package:tetrisdual/ui/widgets/global_app_bar.dart';
+
+class SkeletonScreen extends StatelessWidget {
+  const SkeletonScreen({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: const GlobalAppBar(),
+      extendBodyBehindAppBar: false,
+      body: Material(
+        color: Theme.of(context).colorScheme.surface,
+        child: BlocBuilder<NavCubit, int>(
+          builder: (BuildContext context, int pageIndex) {
+            return Padding(
+              padding: const EdgeInsets.only(
+                top: 8,
+                left: 2,
+                right: 2,
+              ),
+              child: Menu.getPageWidget(pageIndex),
+            );
+          },
+        ),
+      ),
+      backgroundColor: Theme.of(context).colorScheme.surface,
+    );
+  }
+}
diff --git a/lib/ui/widgets/actions/button_delete_saved_game.dart b/lib/ui/widgets/actions/button_delete_saved_game.dart
new file mode 100644
index 0000000000000000000000000000000000000000..6e701828ab1daab181edaf08e89e0bc5a376a7ed
--- /dev/null
+++ b/lib/ui/widgets/actions/button_delete_saved_game.dart
@@ -0,0 +1,21 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:tetrisdual/cubit/game_cubit.dart';
+
+class DeleteSavedGameButton extends StatelessWidget {
+  const DeleteSavedGameButton({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return TextButton(
+      child: const Image(
+        image: AssetImage('assets/ui/button_delete_saved_game.png'),
+        fit: BoxFit.fill,
+      ),
+      onPressed: () {
+        BlocProvider.of<GameCubit>(context).deleteSavedGame();
+      },
+    );
+  }
+}
diff --git a/lib/ui/widgets/actions/button_game_quit.dart b/lib/ui/widgets/actions/button_game_quit.dart
new file mode 100644
index 0000000000000000000000000000000000000000..b16e72645ef71bb772c961a94064ef120db699ff
--- /dev/null
+++ b/lib/ui/widgets/actions/button_game_quit.dart
@@ -0,0 +1,21 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:tetrisdual/cubit/game_cubit.dart';
+
+class QuitGameButton extends StatelessWidget {
+  const QuitGameButton({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return TextButton(
+      child: const Image(
+        image: AssetImage('assets/ui/button_back.png'),
+        fit: BoxFit.fill,
+      ),
+      onPressed: () {
+        BlocProvider.of<GameCubit>(context).quitGame();
+      },
+    );
+  }
+}
diff --git a/lib/ui/widgets/actions/button_game_start_new.dart b/lib/ui/widgets/actions/button_game_start_new.dart
new file mode 100644
index 0000000000000000000000000000000000000000..6e609bea6941f02e65fb84f3700762f1ca6ccf19
--- /dev/null
+++ b/lib/ui/widgets/actions/button_game_start_new.dart
@@ -0,0 +1,34 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:tetrisdual/cubit/game_cubit.dart';
+import 'package:tetrisdual/cubit/settings_game_cubit.dart';
+import 'package:tetrisdual/cubit/settings_global_cubit.dart';
+
+class StartNewGameButton extends StatelessWidget {
+  const StartNewGameButton({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<GameSettingsCubit, GameSettingsState>(
+      builder: (BuildContext context, GameSettingsState gameSettingsState) {
+        return BlocBuilder<GlobalSettingsCubit, GlobalSettingsState>(
+          builder: (BuildContext context, GlobalSettingsState globalSettingsState) {
+            return TextButton(
+              child: const Image(
+                image: AssetImage('assets/ui/button_start.png'),
+                fit: BoxFit.fill,
+              ),
+              onPressed: () {
+                BlocProvider.of<GameCubit>(context).startNewGame(
+                  gameSettings: gameSettingsState.settings,
+                  globalSettings: globalSettingsState.settings,
+                );
+              },
+            );
+          },
+        );
+      },
+    );
+  }
+}
diff --git a/lib/ui/widgets/actions/button_resume_saved_game.dart b/lib/ui/widgets/actions/button_resume_saved_game.dart
new file mode 100644
index 0000000000000000000000000000000000000000..bbd0c93f29dc238ad23b283d310c29e02fd80e7f
--- /dev/null
+++ b/lib/ui/widgets/actions/button_resume_saved_game.dart
@@ -0,0 +1,21 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:tetrisdual/cubit/game_cubit.dart';
+
+class ResumeSavedGameButton extends StatelessWidget {
+  const ResumeSavedGameButton({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return TextButton(
+      child: const Image(
+        image: AssetImage('assets/ui/button_resume_game.png'),
+        fit: BoxFit.fill,
+      ),
+      onPressed: () {
+        BlocProvider.of<GameCubit>(context).resumeSavedGame();
+      },
+    );
+  }
+}
diff --git a/lib/layout/board_painter.dart b/lib/ui/widgets/game/board_painter.dart
similarity index 100%
rename from lib/layout/board_painter.dart
rename to lib/ui/widgets/game/board_painter.dart
diff --git a/lib/entity/counter.dart b/lib/ui/widgets/game/counter.dart
similarity index 69%
rename from lib/entity/counter.dart
rename to lib/ui/widgets/game/counter.dart
index 31fedae0d2c1f1bf3858fc36569b109c3caacbc0..0105014f7a20472fc7cbfdbbc9d000392c4aee14 100644
--- a/lib/entity/counter.dart
+++ b/lib/ui/widgets/game/counter.dart
@@ -1,20 +1,15 @@
 import 'dart:math';
 
 import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
 
-import 'package:tetrisdual/provider/data.dart';
+import 'package:tetrisdual/cubit/game_cubit.dart';
+import 'package:tetrisdual/models/game/counter.dart';
 
-class Counter {
-  // Current counter
-  bool _match = false; // Does this new tetrimino touch an other tetrimino of same player
-  int _lines = 0; // Count lines fully filled by this new tetrimino
-  int _holes = 0; // Count non fillable holes caused by this new tetrimino
+class CounterWidget extends StatelessWidget {
+  const CounterWidget({super.key, required this.counter});
 
-  // Points definitions
-  static const int _base = 50;
-  static const int _pointsIfMatch = 50;
-  static const int _pointsPerLine = 60;
-  static const int _pointsPerHole = -10;
+  final Counter counter;
 
   static const double iconSize = 30.0;
   static const double fontSize = 50.0;
@@ -51,29 +46,30 @@ class Counter {
     size: iconSize,
   );
 
-  Widget buildCounterWidget(Data myProvider) {
-    return Table(
-      children: [
-        buildMatchWidget(myProvider),
-        buildSpacerRow(),
-        buildLinesWidget(myProvider),
-        buildSpacerRow(),
-        buildHolesWidget(myProvider),
-        buildSpacerRow(),
-      ],
-    );
-  }
+  @override
+  Widget build(BuildContext context) {
+    final GameCubit gameCubit = BlocProvider.of<GameCubit>(context);
 
-  TableRow buildSpacerRow() {
-    return const TableRow(children: [
+    const spacer = TableRow(children: [
+      SizedBox(height: spacerHeight),
       SizedBox(height: spacerHeight),
       SizedBox(height: spacerHeight),
       SizedBox(height: spacerHeight),
-      SizedBox(height: spacerHeight)
     ]);
+
+    return Table(
+      children: [
+        buildTouchWidget(gameCubit),
+        spacer,
+        buildLinesWidget(gameCubit),
+        spacer,
+        buildHolesWidget(gameCubit),
+        spacer,
+      ],
+    );
   }
 
-  TableRow buildMatchWidget(Data myProvider) {
+  TableRow buildTouchWidget(GameCubit gameCubit) {
     return TableRow(
       children: [
         iconTouchingColor,
@@ -85,13 +81,13 @@ class Counter {
           ),
           icon: iconRemove,
           onPressed: () {
-            _match = false;
-            myProvider.redraw();
+            counter.touch = false;
+            gameCubit.refresh();
           },
         ),
         Center(
           child: Icon(
-            _match ? Icons.radio_button_checked : Icons.radio_button_unchecked,
+            counter.touch ? Icons.radio_button_checked : Icons.radio_button_unchecked,
             color: categoryIconColor,
             size: iconSize,
           ),
@@ -104,15 +100,15 @@ class Counter {
           ),
           icon: iconAdd,
           onPressed: () {
-            _match = true;
-            myProvider.redraw();
+            counter.touch = true;
+            gameCubit.refresh();
           },
         ),
       ],
     );
   }
 
-  TableRow buildLinesWidget(Data myProvider) {
+  TableRow buildLinesWidget(GameCubit gameCubit) {
     return TableRow(
       children: [
         iconRowsCount,
@@ -124,13 +120,13 @@ class Counter {
           ),
           icon: iconRemove,
           onPressed: () {
-            _lines = max(_lines - 1, 0);
-            myProvider.redraw();
+            counter.lines = max(counter.lines - 1, 0);
+            gameCubit.refresh();
           },
         ),
         Center(
           child: Text(
-            _lines.toString(),
+            counter.lines.toString(),
             style: const TextStyle(
               fontFamily: 'Blocks',
               fontSize: fontSize,
@@ -149,15 +145,15 @@ class Counter {
           ),
           icon: iconAdd,
           onPressed: () {
-            _lines = min(_lines + 1, 4);
-            myProvider.redraw();
+            counter.lines = min(counter.lines + 1, 4);
+            gameCubit.refresh();
           },
         ),
       ],
     );
   }
 
-  TableRow buildHolesWidget(Data myProvider) {
+  TableRow buildHolesWidget(GameCubit gameCubit) {
     return TableRow(
       children: [
         iconHolesCount,
@@ -169,13 +165,13 @@ class Counter {
           ),
           icon: iconRemove,
           onPressed: () {
-            _holes = max(_holes - 1, 0);
-            myProvider.redraw();
+            counter.holes = max(counter.holes - 1, 0);
+            gameCubit.refresh();
           },
         ),
         Center(
           child: Text(
-            _holes.toString(),
+            counter.holes.toString(),
             style: const TextStyle(
               fontFamily: 'Blocks',
               fontSize: fontSize,
@@ -194,24 +190,11 @@ class Counter {
           ),
           icon: iconAdd,
           onPressed: () {
-            _holes = min(_holes + 1, 9);
-            myProvider.redraw();
+            counter.holes = min(counter.holes + 1, 9);
+            gameCubit.refresh();
           },
         ),
       ],
     );
   }
-
-  int computePoints() {
-    return _base +
-        (_match ? _pointsIfMatch : 0) +
-        _lines * _pointsPerLine +
-        _holes * _pointsPerHole;
-  }
-
-  void reset() {
-    _match = false;
-    _lines = 0;
-    _holes = 0;
-  }
 }
diff --git a/lib/ui/widgets/game/game_board.dart b/lib/ui/widgets/game/game_board.dart
new file mode 100644
index 0000000000000000000000000000000000000000..5a46f801dbe6410ebb65507398238c981ea948ee
--- /dev/null
+++ b/lib/ui/widgets/game/game_board.dart
@@ -0,0 +1,34 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:tetrisdual/cubit/game_cubit.dart';
+import 'package:tetrisdual/ui/widgets/game/player.dart';
+import 'package:tetrisdual/ui/widgets/game/toggle_player.dart';
+
+class GameBoardWidget extends StatelessWidget {
+  const GameBoardWidget({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    final GameCubit gameCubit = BlocProvider.of<GameCubit>(context);
+
+    return Center(
+      child: Column(
+        mainAxisAlignment: MainAxisAlignment.spaceBetween,
+        crossAxisAlignment: CrossAxisAlignment.center,
+        children: [
+          RotatedBox(
+            quarterTurns: 2,
+            child: PlayerWidget(
+              player: gameCubit.getPlayer(1),
+            ),
+          ),
+          const TogglePlayerWidget(),
+          PlayerWidget(
+            player: gameCubit.getPlayer(2),
+          ),
+        ],
+      ),
+    );
+  }
+}
diff --git a/lib/ui/widgets/game/manager.dart b/lib/ui/widgets/game/manager.dart
new file mode 100644
index 0000000000000000000000000000000000000000..e02062a7b20f989443d427827aa8293b95b0a863
--- /dev/null
+++ b/lib/ui/widgets/game/manager.dart
@@ -0,0 +1,36 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:tetrisdual/cubit/game_cubit.dart';
+import 'package:tetrisdual/models/game/player.dart';
+import 'package:tetrisdual/ui/widgets/game/counter.dart';
+import 'package:tetrisdual/ui/widgets/game/submit.dart';
+
+class ManagerWidget extends StatelessWidget {
+  const ManagerWidget({super.key, required this.player});
+
+  final Player player;
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<GameCubit, GameState>(
+      builder: (BuildContext context, GameState gameState) {
+        return Expanded(
+          child: Container(
+            margin: const EdgeInsets.all(5),
+            child: Column(
+              mainAxisAlignment: MainAxisAlignment.start,
+              crossAxisAlignment: CrossAxisAlignment.center,
+              children: gameState.currentGame.currentPlayer == player.playerId
+                  ? [
+                      CounterWidget(counter: player.counter),
+                      SubmitWidget(player: player),
+                    ]
+                  : [],
+            ),
+          ),
+        );
+      },
+    );
+  }
+}
diff --git a/lib/ui/widgets/game/player.dart b/lib/ui/widgets/game/player.dart
new file mode 100644
index 0000000000000000000000000000000000000000..92109dd8a245737f4cca9c2d32811aecf11afddb
--- /dev/null
+++ b/lib/ui/widgets/game/player.dart
@@ -0,0 +1,66 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:tetrisdual/cubit/game_cubit.dart';
+import 'package:tetrisdual/models/game/player.dart';
+import 'package:tetrisdual/ui/widgets/game/manager.dart';
+import 'package:tetrisdual/ui/widgets/game/tetrimino.dart';
+
+class PlayerWidget extends StatelessWidget {
+  const PlayerWidget({super.key, required this.player});
+
+  final Player player;
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<GameCubit, GameState>(
+      builder: (BuildContext context, GameState gameState) {
+        final bool isActive = (gameState.currentGame.currentPlayer == player.playerId);
+        final double screenWidth = MediaQuery.of(context).size.width;
+        final double tetriminoBlockWidth = screenWidth / 2.3;
+
+        final Color borderColor = isActive ? Colors.greenAccent : Colors.blueGrey;
+        const double borderWidth = 10;
+
+        return Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          crossAxisAlignment: CrossAxisAlignment.end,
+          children: [
+            Text(
+              player.score.toString(),
+              style: const TextStyle(
+                fontFamily: 'Blocks',
+                fontSize: 130,
+                fontWeight: FontWeight.bold,
+              ),
+              textHeightBehavior: const TextHeightBehavior(
+                applyHeightToFirstAscent: false,
+                applyHeightToLastDescent: false,
+              ),
+            ),
+            Container(
+              decoration: BoxDecoration(
+                border: Border.all(
+                  color: borderColor,
+                  width: borderWidth,
+                ),
+              ),
+              child: Row(
+                mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                crossAxisAlignment: CrossAxisAlignment.center,
+                children: [
+                  SizedBox.square(
+                    dimension: tetriminoBlockWidth,
+                    child:
+                        isActive ? TetriminoWidget(player: player) : const SizedBox.shrink(),
+                  ),
+                  ManagerWidget(player: player),
+                ],
+              ),
+            )
+          ],
+        );
+      },
+    );
+  }
+}
diff --git a/lib/ui/widgets/game/submit.dart b/lib/ui/widgets/game/submit.dart
new file mode 100644
index 0000000000000000000000000000000000000000..875001b384306279dd9edbe1fa3b3b8710ed2834
--- /dev/null
+++ b/lib/ui/widgets/game/submit.dart
@@ -0,0 +1,72 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:tetrisdual/cubit/game_cubit.dart';
+import 'package:tetrisdual/models/game/player.dart';
+
+class SubmitWidget extends StatelessWidget {
+  const SubmitWidget({super.key, required this.player});
+
+  final Player player;
+
+  @override
+  Widget build(BuildContext context) {
+    const double gainFontSize = 70;
+
+    const gainTestStyle = TextStyle(
+      fontFamily: 'Blocks',
+      fontSize: gainFontSize,
+      fontWeight: FontWeight.bold,
+    );
+    const submitIcon = Icon(
+      Icons.done_all,
+      color: Colors.orange,
+      size: gainFontSize / 2,
+    );
+
+    const topBorderBlack = BoxDecoration(
+      border: Border(
+        top: BorderSide(
+          color: Colors.black,
+          width: 2,
+        ),
+      ),
+    );
+
+    return Container(
+      decoration: topBorderBlack,
+      padding: const EdgeInsets.only(
+        top: 10,
+      ),
+      child: Row(
+        mainAxisAlignment: MainAxisAlignment.end,
+        crossAxisAlignment: CrossAxisAlignment.center,
+        children: [
+          Text(
+            '+${player.counter.computePoints()}',
+            style: gainTestStyle,
+            textHeightBehavior: const TextHeightBehavior(
+              applyHeightToFirstAscent: false,
+              applyHeightToLastDescent: false,
+            ),
+          ),
+          const SizedBox(width: 10),
+          IconButton(
+            padding: EdgeInsets.zero,
+            constraints: const BoxConstraints(),
+            style: const ButtonStyle(
+              tapTargetSize: MaterialTapTargetSize.shrinkWrap,
+            ),
+            icon: submitIcon,
+            onPressed: () {
+              player.score = player.score + player.counter.computePoints();
+              player.counter.reset();
+
+              BlocProvider.of<GameCubit>(context).toggleCurrentPlayer();
+            },
+          ),
+          const SizedBox(width: 10),
+        ],
+      ),
+    );
+  }
+}
diff --git a/lib/ui/widgets/game/tetrimino.dart b/lib/ui/widgets/game/tetrimino.dart
new file mode 100644
index 0000000000000000000000000000000000000000..d883487c746c21cf6f0ea09e875408f2aa98efd7
--- /dev/null
+++ b/lib/ui/widgets/game/tetrimino.dart
@@ -0,0 +1,34 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:tetrisdual/cubit/game_cubit.dart';
+import 'package:tetrisdual/models/game/player.dart';
+import 'package:tetrisdual/ui/widgets/game/board_painter.dart';
+
+class TetriminoWidget extends StatelessWidget {
+  const TetriminoWidget({super.key, required this.player});
+
+  final Player player;
+
+  @override
+  Widget build(BuildContext context) {
+    final GameCubit gameCubit = BlocProvider.of<GameCubit>(context);
+    final double width = MediaQuery.of(context).size.width;
+
+    return GestureDetector(
+      onTapUp: (details) {
+        if (player.playerId == gameCubit.getCurrentPlayer().playerId) {
+          player.pickRandomTetrimino();
+          gameCubit.refresh();
+        }
+      },
+      child: CustomPaint(
+        size: Size(width, width),
+        willChange: false,
+        painter: BoardPainter(player.currentTetrimino),
+        isComplex: true,
+        key: Key(player.currentTetrimino.toString()),
+      ),
+    );
+  }
+}
diff --git a/lib/ui/widgets/game/toggle_player.dart b/lib/ui/widgets/game/toggle_player.dart
new file mode 100644
index 0000000000000000000000000000000000000000..1d15b942250e1d4d21fbf210e8d11b5e0c7bad57
--- /dev/null
+++ b/lib/ui/widgets/game/toggle_player.dart
@@ -0,0 +1,27 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:tetrisdual/cubit/game_cubit.dart';
+
+class TogglePlayerWidget extends StatelessWidget {
+  const TogglePlayerWidget({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return GestureDetector(
+      onTapUp: (details) {
+        BlocProvider.of<GameCubit>(context).toggleCurrentPlayer();
+      },
+      child: const Text(
+        '🔄',
+        style: TextStyle(
+          fontSize: 50,
+        ),
+        textHeightBehavior: TextHeightBehavior(
+          applyHeightToFirstAscent: false,
+          applyHeightToLastDescent: false,
+        ),
+      ),
+    );
+  }
+}
diff --git a/lib/ui/widgets/global_app_bar.dart b/lib/ui/widgets/global_app_bar.dart
new file mode 100644
index 0000000000000000000000000000000000000000..240c5f2e7f2f9bd708fb4bc0d36b57a61d1c6b9c
--- /dev/null
+++ b/lib/ui/widgets/global_app_bar.dart
@@ -0,0 +1,83 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:tetrisdual/config/menu.dart';
+import 'package:tetrisdual/cubit/game_cubit.dart';
+import 'package:tetrisdual/cubit/nav_cubit.dart';
+import 'package:tetrisdual/models/game/game.dart';
+import 'package:tetrisdual/ui/helpers/app_titles.dart';
+
+class GlobalAppBar extends StatelessWidget implements PreferredSizeWidget {
+  const GlobalAppBar({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<GameCubit, GameState>(
+      builder: (BuildContext context, GameState gameState) {
+        return BlocBuilder<NavCubit, int>(
+          builder: (BuildContext context, int pageIndex) {
+            final Game currentGame = gameState.currentGame;
+
+            final List<Widget> menuActions = [];
+
+            if (currentGame.isRunning && !currentGame.isFinished) {
+              menuActions.add(TextButton(
+                child: const Image(
+                  image: AssetImage('assets/ui/button_back.png'),
+                  fit: BoxFit.fill,
+                ),
+                onPressed: () {},
+                onLongPress: () {
+                  BlocProvider.of<GameCubit>(context).quitGame();
+                },
+              ));
+            } else {
+              if (pageIndex == Menu.indexGame) {
+                // go to Settings page
+                menuActions.add(ElevatedButton(
+                  onPressed: () {
+                    context.read<NavCubit>().goToSettingsPage();
+                  },
+                  style: ElevatedButton.styleFrom(
+                    shape: const CircleBorder(),
+                  ),
+                  child: Menu.menuItemSettings.icon,
+                ));
+
+                // go to About page
+                menuActions.add(ElevatedButton(
+                  onPressed: () {
+                    context.read<NavCubit>().goToAboutPage();
+                  },
+                  style: ElevatedButton.styleFrom(
+                    shape: const CircleBorder(),
+                  ),
+                  child: Menu.menuItemAbout.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 AppHeader(text: 'app_name'),
+              actions: menuActions,
+            );
+          },
+        );
+      },
+    );
+  }
+
+  @override
+  Size get preferredSize => const Size.fromHeight(50);
+}
diff --git a/lib/utils/color_extensions.dart b/lib/utils/color_extensions.dart
new file mode 100644
index 0000000000000000000000000000000000000000..4e55e338f0d3ed98b233d1ef887b7b3e17e29d97
--- /dev/null
+++ b/lib/utils/color_extensions.dart
@@ -0,0 +1,33 @@
+import 'dart:ui';
+
+extension ColorExtension on Color {
+  Color darken([int percent = 40]) {
+    assert(1 <= percent && percent <= 100);
+    final value = 1 - percent / 100;
+    return Color.fromARGB(
+      alpha,
+      (red * value).round(),
+      (green * value).round(),
+      (blue * value).round(),
+    );
+  }
+
+  Color lighten([int percent = 40]) {
+    assert(1 <= percent && percent <= 100);
+    final value = percent / 100;
+    return Color.fromARGB(
+      alpha,
+      (red + ((255 - red) * value)).round(),
+      (green + ((255 - green) * value)).round(),
+      (blue + ((255 - blue) * value)).round(),
+    );
+  }
+
+  Color avg(Color other) {
+    final red = (this.red + other.red) ~/ 2;
+    final green = (this.green + other.green) ~/ 2;
+    final blue = (this.blue + other.blue) ~/ 2;
+    final alpha = (this.alpha + other.alpha) ~/ 2;
+    return Color.fromARGB(alpha, red, green, blue);
+  }
+}
diff --git a/lib/utils/game_utils.dart b/lib/utils/game_utils.dart
deleted file mode 100644
index fed7b66ae2d25e27d9b6e4fc1ef9d702228f0ca2..0000000000000000000000000000000000000000
--- a/lib/utils/game_utils.dart
+++ /dev/null
@@ -1,14 +0,0 @@
-import 'package:tetrisdual/provider/data.dart';
-
-class GameUtils {
-  static Future<void> quitGame(Data myProvider) async {
-    myProvider.updateGameIsRunning(false);
-  }
-
-  static Future<void> startNewGame(Data myProvider) async {
-    myProvider.resetGame();
-    myProvider.enableRandomPlayer();
-    myProvider.getCurrentPlayer().pickRandomTetrimino();
-    myProvider.updateGameIsRunning(true);
-  }
-}
diff --git a/pubspec.lock b/pubspec.lock
index 6b1fdfef808496ebd4ff8ce5bb9e51bfbc6469e8..e0ab96ebb656b1260018d45af586d9ec14ba4a7e 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -1,6 +1,14 @@
 # Generated by pub
 # See https://dart.dev/tools/pub/glossary#lockfile
 packages:
+  args:
+    dependency: transitive
+    description:
+      name: args
+      sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.5.0"
   async:
     dependency: transitive
     description:
@@ -9,6 +17,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.11.0"
+  bloc:
+    dependency: transitive
+    description:
+      name: bloc
+      sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e"
+      url: "https://pub.dev"
+    source: hosted
+    version: "8.1.4"
   characters:
     dependency: transitive
     description:
@@ -17,6 +33,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.3.0"
+  clock:
+    dependency: transitive
+    description:
+      name: clock
+      sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.1.1"
   collection:
     dependency: transitive
     description:
@@ -25,6 +49,38 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.18.0"
+  crypto:
+    dependency: transitive
+    description:
+      name: crypto
+      sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.0.3"
+  easy_localization:
+    dependency: "direct main"
+    description:
+      name: easy_localization
+      sha256: fa59bcdbbb911a764aa6acf96bbb6fa7a5cf8234354fc45ec1a43a0349ef0201
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.0.7"
+  easy_logger:
+    dependency: transitive
+    description:
+      name: easy_logger
+      sha256: c764a6e024846f33405a2342caf91c62e357c24b02c04dbc712ef232bf30ffb7
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.0.2"
+  equatable:
+    dependency: "direct main"
+    description:
+      name: equatable
+      sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.5"
   ffi:
     dependency: transitive
     description:
@@ -46,27 +102,80 @@ packages:
     description: flutter
     source: sdk
     version: "0.0.0"
+  flutter_bloc:
+    dependency: "direct main"
+    description:
+      name: flutter_bloc
+      sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a
+      url: "https://pub.dev"
+    source: hosted
+    version: "8.1.6"
   flutter_lints:
     dependency: "direct dev"
     description:
       name: flutter_lints
-      sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7
+      sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c"
       url: "https://pub.dev"
     source: hosted
-    version: "3.0.1"
+    version: "4.0.0"
+  flutter_localizations:
+    dependency: transitive
+    description: flutter
+    source: sdk
+    version: "0.0.0"
   flutter_web_plugins:
     dependency: transitive
     description: flutter
     source: sdk
     version: "0.0.0"
+  hive:
+    dependency: "direct main"
+    description:
+      name: hive
+      sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.2.3"
+  http:
+    dependency: transitive
+    description:
+      name: http
+      sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.2.1"
+  http_parser:
+    dependency: transitive
+    description:
+      name: http_parser
+      sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.0.2"
+  hydrated_bloc:
+    dependency: "direct main"
+    description:
+      name: hydrated_bloc
+      sha256: af35b357739fe41728df10bec03aad422cdc725a1e702e03af9d2a41ea05160c
+      url: "https://pub.dev"
+    source: hosted
+    version: "9.1.5"
+  intl:
+    dependency: transitive
+    description:
+      name: intl
+      sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.19.0"
   lints:
     dependency: transitive
     description:
       name: lints
-      sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
+      sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235"
       url: "https://pub.dev"
     source: hosted
-    version: "3.0.0"
+    version: "4.0.0"
   material_color_utilities:
     dependency: transitive
     description:
@@ -79,10 +188,10 @@ packages:
     dependency: transitive
     description:
       name: meta
-      sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
+      sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
       url: "https://pub.dev"
     source: hosted
-    version: "1.11.0"
+    version: "1.12.0"
   nested:
     dependency: transitive
     description:
@@ -91,14 +200,22 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.0.0"
-  overlay_support:
+  package_info_plus:
     dependency: "direct main"
     description:
-      name: overlay_support
-      sha256: fc39389bfd94e6985e1e13b2a88a125fc4027608485d2d4e2847afe1b2bb339c
+      name: package_info_plus
+      sha256: b93d8b4d624b4ea19b0a5a208b2d6eff06004bc3ce74c06040b120eeadd00ce0
       url: "https://pub.dev"
     source: hosted
-    version: "2.1.0"
+    version: "8.0.0"
+  package_info_plus_platform_interface:
+    dependency: transitive
+    description:
+      name: package_info_plus_platform_interface
+      sha256: f49918f3433a3146047372f9d4f1f847511f2acd5cd030e1f44fe5a50036b70e
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.0.0"
   path:
     dependency: transitive
     description:
@@ -107,6 +224,30 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.9.0"
+  path_provider:
+    dependency: "direct main"
+    description:
+      name: path_provider
+      sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.3"
+  path_provider_android:
+    dependency: transitive
+    description:
+      name: path_provider_android
+      sha256: "9c96da072b421e98183f9ea7464898428e764bc0ce5567f27ec8693442e72514"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.2.5"
+  path_provider_foundation:
+    dependency: transitive
+    description:
+      name: path_provider_foundation
+      sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.4.0"
   path_provider_linux:
     dependency: transitive
     description:
@@ -135,10 +276,10 @@ packages:
     dependency: transitive
     description:
       name: platform
-      sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
+      sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
       url: "https://pub.dev"
     source: hosted
-    version: "3.1.4"
+    version: "3.1.5"
   plugin_platform_interface:
     dependency: transitive
     description:
@@ -148,7 +289,7 @@ packages:
     source: hosted
     version: "2.1.8"
   provider:
-    dependency: "direct main"
+    dependency: transitive
     description:
       name: provider
       sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c
@@ -156,29 +297,29 @@ packages:
     source: hosted
     version: "6.1.2"
   shared_preferences:
-    dependency: "direct main"
+    dependency: transitive
     description:
       name: shared_preferences
-      sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02"
+      sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180
       url: "https://pub.dev"
     source: hosted
-    version: "2.2.2"
+    version: "2.2.3"
   shared_preferences_android:
     dependency: transitive
     description:
       name: shared_preferences_android
-      sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06"
+      sha256: "93d0ec9dd902d85f326068e6a899487d1f65ffcd5798721a95330b26c8131577"
       url: "https://pub.dev"
     source: hosted
-    version: "2.2.1"
+    version: "2.2.3"
   shared_preferences_foundation:
     dependency: transitive
     description:
       name: shared_preferences_foundation
-      sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c"
+      sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7"
       url: "https://pub.dev"
     source: hosted
-    version: "2.3.5"
+    version: "2.4.0"
   shared_preferences_linux:
     dependency: transitive
     description:
@@ -216,6 +357,54 @@ packages:
     description: flutter
     source: sdk
     version: "0.0.99"
+  source_span:
+    dependency: transitive
+    description:
+      name: source_span
+      sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.10.0"
+  string_scanner:
+    dependency: transitive
+    description:
+      name: string_scanner
+      sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.2.0"
+  synchronized:
+    dependency: transitive
+    description:
+      name: synchronized
+      sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.1.0+1"
+  term_glyph:
+    dependency: transitive
+    description:
+      name: term_glyph
+      sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.2.1"
+  typed_data:
+    dependency: transitive
+    description:
+      name: typed_data
+      sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
+      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:
@@ -228,18 +417,18 @@ packages:
     dependency: transitive
     description:
       name: web
-      sha256: "1d9158c616048c38f712a6646e317a3426da10e884447626167240d45209cbad"
+      sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
       url: "https://pub.dev"
     source: hosted
-    version: "0.5.0"
+    version: "0.5.1"
   win32:
     dependency: transitive
     description:
       name: win32
-      sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8"
+      sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4
       url: "https://pub.dev"
     source: hosted
-    version: "5.2.0"
+    version: "5.5.1"
   xdg_directories:
     dependency: transitive
     description:
@@ -249,5 +438,5 @@ packages:
     source: hosted
     version: "1.0.4"
 sdks:
-  dart: ">=3.3.0 <4.0.0"
-  flutter: ">=3.19.0"
+  dart: ">=3.4.0 <4.0.0"
+  flutter: ">=3.22.0"
diff --git a/pubspec.yaml b/pubspec.yaml
index 553c373d9b1a925596874ad75fe8c4fb5a246534..ddd843645aaf7b8c8166d73aafb04c5607b6f715 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,26 +1,51 @@
 name: tetrisdual
 description: Tetris Dual Game
-publish_to: 'none'
-version: 0.0.12+12
+
+publish_to: "none"
+
+version: 0.1.0+13
 
 environment:
-  sdk: '^3.0.0'
+  sdk: "^3.0.0"
 
 dependencies:
   flutter:
     sdk: flutter
-  provider: ^6.0.5
-  shared_preferences: ^2.2.1
-  overlay_support: ^2.1.0
+
+  # base
+  easy_localization: ^3.0.1
+  equatable: ^2.0.5
+  flutter_bloc: ^8.1.1
+  hive: ^2.2.3
+  hydrated_bloc: ^9.0.0
+  package_info_plus: ^8.0.0
+  path_provider: ^2.0.11
+  unicons: ^2.1.1
+
+  # specific
+  # (none)
 
 dev_dependencies:
-  flutter_lints: ^3.0.1
+  flutter_lints: ^4.0.0
 
 flutter:
   uses-material-design: true
   assets:
-    - assets/icons/
+    - assets/ui/
+    - assets/translations/
+
   fonts:
     - family: Blocks
       fonts:
         - asset: assets/fonts/blocks.ttf
+    - family: Nunito
+      fonts:
+        - asset: assets/fonts/Nunito-Bold.ttf
+          weight: 700
+        - asset: assets/fonts/Nunito-Medium.ttf
+          weight: 500
+        - asset: assets/fonts/Nunito-Regular.ttf
+          weight: 400
+        - asset: assets/fonts/Nunito-Light.ttf
+          weight: 300
+
diff --git a/icons/build_repository_icons.sh b/resources/app/build_application_resources.sh
similarity index 98%
rename from icons/build_repository_icons.sh
rename to resources/app/build_application_resources.sh
index 27dbe2647fe4e6d562fbd99451716d1b7d448570..6d67b8f4f9eca701d1aed7331ef41dfb0bd44f20 100755
--- a/icons/build_repository_icons.sh
+++ b/resources/app/build_application_resources.sh
@@ -6,7 +6,7 @@ command -v scour >/dev/null 2>&1 || { echo >&2 "I require scour but it's not ins
 command -v optipng >/dev/null 2>&1 || { echo >&2 "I require optipng but it's not installed. Aborting."; exit 1; }
 
 CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
-BASE_DIR="$(dirname "${CURRENT_DIR}")"
+BASE_DIR="$(dirname "$(dirname "${CURRENT_DIR}")")"
 
 SOURCE_ICON="${CURRENT_DIR}/icon.svg"
 SOURCE_FASTLANE="${CURRENT_DIR}/featureGraphic.svg"
diff --git a/icons/featureGraphic.svg b/resources/app/featureGraphic.svg
similarity index 100%
rename from icons/featureGraphic.svg
rename to resources/app/featureGraphic.svg
diff --git a/icons/icon.svg b/resources/app/icon.svg
similarity index 100%
rename from icons/icon.svg
rename to resources/app/icon.svg
diff --git a/icons/build_icons.sh b/resources/build_resources.sh
similarity index 50%
rename from icons/build_icons.sh
rename to resources/build_resources.sh
index 915a16cb0b0b9b3a7109ef6686af811fc93cb8bb..659697a1c043cfe1c7654635cfaec3e4a0ff8a1a 100755
--- a/icons/build_icons.sh
+++ b/resources/build_resources.sh
@@ -2,5 +2,6 @@
 
 CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
 
-"${CURRENT_DIR}/build_application_icons.sh"
-"${CURRENT_DIR}/build_repository_icons.sh"
+${CURRENT_DIR}/app/build_application_resources.sh
+${CURRENT_DIR}/ui/build_ui_resources.sh
+
diff --git a/resources/ui/build_ui_resources.sh b/resources/ui/build_ui_resources.sh
new file mode 100755
index 0000000000000000000000000000000000000000..4f365ede7d83140ce6309a3083580f2662b30990
--- /dev/null
+++ b/resources/ui/build_ui_resources.sh
@@ -0,0 +1,110 @@
+#! /bin/bash
+
+# Check dependencies
+command -v inkscape >/dev/null 2>&1 || { echo >&2 "I require inkscape but it's not installed. Aborting."; exit 1; }
+command -v scour >/dev/null 2>&1 || { echo >&2 "I require scour but it's not installed. Aborting."; exit 1; }
+command -v optipng >/dev/null 2>&1 || { echo >&2 "I require optipng but it's not installed. Aborting."; exit 1; }
+
+CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
+BASE_DIR="$(dirname "$(dirname "${CURRENT_DIR}")")"
+ASSETS_DIR="${BASE_DIR}/assets"
+
+OPTIPNG_OPTIONS="-preserve -quiet -o7"
+ICON_SIZE=192
+
+#######################################################
+
+# Game images (svg files found in `images` folder)
+AVAILABLE_GAME_IMAGES=""
+if [ -d "${CURRENT_DIR}/images" ]; then
+  AVAILABLE_GAME_IMAGES="$(find "${CURRENT_DIR}/images" -type f -name "*.svg" | awk -F/ '{print $NF}' | cut -d"." -f1 | sort)"
+fi
+
+# Skins (subfolders found in `skins` folder)
+AVAILABLE_SKINS=""
+if [ -d "${CURRENT_DIR}/skins" ]; then
+  AVAILABLE_SKINS="$(find "${CURRENT_DIR}/skins" -mindepth 1 -type d | awk -F/ '{print $NF}')"
+fi
+
+# Images per skin (svg files found recursively in `skins` folder and subfolders)
+SKIN_IMAGES=""
+if [ -d "${CURRENT_DIR}/skins" ]; then
+  SKIN_IMAGES="$(find "${CURRENT_DIR}/skins" -type f -name "*.svg" | awk -F/ '{print $NF}' | cut -d"." -f1 | sort | uniq)"
+fi
+
+#######################################################
+
+# optimize svg
+function optimize_svg() {
+  SOURCE="$1"
+
+  cp ${SOURCE} ${SOURCE}.tmp
+  scour \
+      --remove-descriptive-elements \
+      --enable-id-stripping \
+      --enable-viewboxing \
+      --enable-comment-stripping \
+      --nindent=4 \
+      --quiet \
+      -i ${SOURCE}.tmp \
+      -o ${SOURCE}
+  rm ${SOURCE}.tmp
+}
+
+# build icons
+function build_image() {
+  SOURCE="$1"
+  TARGET="$2"
+
+  echo "Building ${TARGET}"
+
+  if [ ! -f "${SOURCE}" ]; then
+    echo "Missing file: ${SOURCE}"
+    exit 1
+  fi
+
+  optimize_svg "${SOURCE}"
+
+  mkdir -p "$(dirname "${TARGET}")"
+
+  inkscape \
+      --export-width=${ICON_SIZE} \
+      --export-height=${ICON_SIZE} \
+      --export-filename=${TARGET} \
+      "${SOURCE}"
+
+  optipng ${OPTIPNG_OPTIONS} "${TARGET}"
+}
+
+function build_image_for_skin() {
+  SKIN_CODE="$1"
+
+  # skin images
+  for SKIN_IMAGE in ${SKIN_IMAGES}
+  do
+    build_image ${CURRENT_DIR}/skins/${SKIN_CODE}/${SKIN_IMAGE}.svg ${ASSETS_DIR}/skins/${SKIN_CODE}_${SKIN_IMAGE}.png
+  done
+}
+
+#######################################################
+
+# Delete existing generated images
+if [ -d "${ASSETS_DIR}/ui" ]; then
+  find ${ASSETS_DIR}/ui -type f -name "*.png" -delete
+fi
+if [ -d "${ASSETS_DIR}/skins" ]; then
+  find ${ASSETS_DIR}/skins -type f -name "*.png" -delete
+fi
+
+# build game images
+for GAME_IMAGE in ${AVAILABLE_GAME_IMAGES}
+do
+  build_image ${CURRENT_DIR}/images/${GAME_IMAGE}.svg ${ASSETS_DIR}/ui/${GAME_IMAGE}.png
+done
+
+# build skins images
+for SKIN in ${AVAILABLE_SKINS}
+do
+  build_image_for_skin "${SKIN}"
+done
+
diff --git a/icons/button_back.svg b/resources/ui/images/button_back.svg
similarity index 100%
rename from icons/button_back.svg
rename to resources/ui/images/button_back.svg
diff --git a/resources/ui/images/button_delete_saved_game.svg b/resources/ui/images/button_delete_saved_game.svg
new file mode 100644
index 0000000000000000000000000000000000000000..ac7eefef476f761903fe781b8c86d0c94323550a
--- /dev/null
+++ b/resources/ui/images/button_delete_saved_game.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect x=".44662" y=".89101" width="92.772" height="91.894" ry="11.689" fill="#ee7d49" stroke="#fff" stroke-width=".238"/><path d="m61.07 35.601-1.7399 27.837c-0.13442 2.1535-1.9205 3.8312-4.0781 3.8312h-16.84c-2.1576 0-3.9437-1.6777-4.0781-3.8312l-1.7399-27.837h-2.6176c-0.84621 0-1.5323-0.68613-1.5323-1.5323 0-0.84655 0.68613-1.5323 1.5323-1.5323h33.711c0.84621 0 1.5323 0.68578 1.5323 1.5323 0 0.84621-0.68613 1.5323-1.5323 1.5323zm-3.2617 0h-21.953l1.4715 26.674c0.05985 1.0829 0.95531 1.9305 2.0403 1.9305h14.929c1.085 0 1.9804-0.84757 2.0403-1.9305zm-10.977 3.0647c0.78977 0 1.4301 0.6403 1.4301 1.4301v19.614c0 0.78977-0.6403 1.4301-1.4301 1.4301s-1.4301-0.6403-1.4301-1.4301v-19.614c0-0.78977 0.6403-1.4301 1.4301-1.4301zm-6.1293 0c0.80004 0 1.4588 0.62935 1.495 1.4286l0.89647 19.719c0.03182 0.70016-0.50998 1.2933-1.2101 1.3255-0.01915 7.02e-4 -0.03831 1e-3 -0.05781 1e-3 -0.74462 0-1.3596-0.58215-1.4003-1.3261l-1.0757-19.719c-0.0407-0.74701 0.53188-1.3852 1.2786-1.4259 0.02462-0.0014 0.04926-2e-3 0.07388-2e-3zm12.259 0c0.74804 0 1.3541 0.60609 1.3541 1.3541 0 0.02462-3.28e-4 0.04926-0.0017 0.07388l-1.0703 19.618c-0.04379 0.80106-0.70597 1.4281-1.5081 1.4281-0.74804 0-1.3541-0.60609-1.3541-1.3541 0-0.02462 3.49e-4 -0.04925 0.0017-0.07388l1.0703-19.618c0.04379-0.80106 0.70597-1.4281 1.5081-1.4281zm-10.216-12.259h8.1728c2.2567 0 4.086 1.8293 4.086 4.086v2.0433h-16.344v-2.0433c0-2.2567 1.8293-4.086 4.086-4.086zm0.20453 3.0647c-0.67725 0-1.2259 0.54863-1.2259 1.2259v1.8388h10.215v-1.8388c0-0.67725-0.54863-1.2259-1.2259-1.2259z" fill="#fff" fill-rule="evenodd" stroke="#bd4812" stroke-width=".75383"/></svg>
diff --git a/resources/ui/images/button_resume_game.svg b/resources/ui/images/button_resume_game.svg
new file mode 100644
index 0000000000000000000000000000000000000000..6ad8b64202d0e70f898c16c520e756fe8a934add
--- /dev/null
+++ b/resources/ui/images/button_resume_game.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect x=".44662" y=".89101" width="92.772" height="91.894" ry="11.689" fill="#49a1ee" stroke="#fff" stroke-width=".238"/><path d="m39.211 31.236c-0.84086-0.84489-2.9911-0.84489-2.9911 0v34.329c0 0.84594 2.1554 0.84594 2.9993 0l28.178-15.637c0.84392-0.84086 0.85812-2.2091 0.01623-3.053z" fill="#fefeff" stroke="#105ca1" stroke-linecap="round" stroke-linejoin="round" stroke-width="6.1726"/><path d="m40.355 33.714c-0.71948-0.72294-2.5594-0.72294-2.5594 0v29.373c0 0.72383 1.8442 0.72383 2.5663 0l24.11-13.38c0.7221-0.71948 0.73426-1.8902 0.01389-2.6124z" fill="#fefeff" stroke="#feffff" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.225"/><path d="m28.369 66.919v-37.591" fill="#105ca2" stroke="#105ca2" stroke-linecap="round" stroke-width="4.0337"/></svg>
diff --git a/icons/button_start.svg b/resources/ui/images/button_start.svg
similarity index 100%
rename from icons/button_start.svg
rename to resources/ui/images/button_start.svg
diff --git a/icons/placeholder.svg b/resources/ui/images/placeholder.svg
similarity index 100%
rename from icons/placeholder.svg
rename to resources/ui/images/placeholder.svg