diff --git a/android/gradle.properties b/android/gradle.properties index 2ee39041593560d6cd2a6b3cb4d73aebcc41f837..9ebd19f8aea61eb37ed1b92717dc56b426e5a1b4 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,5 +1,5 @@ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true -app.versionName=0.1.16 -app.versionCode=37 +app.versionName=0.1.17 +app.versionCode=38 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..a598046a8ec88733c979127d5ed13aebf0a0f453 --- /dev/null +++ b/assets/translations/en.json @@ -0,0 +1,16 @@ +{ + "app_name": "Minehunter", + + "long_press_to_quit": "Long press to quit game...", + + "bottom_nav_home": "Game", + "bottom_nav_settings": "Settings", + "bottom_nav_about": "About", + + "settings_title": "Settings", + "settings_label_theme": "Theme mode", + + "about_title": "About", + "about_content": "Minehunter.", + "about_version": "Version: {version}" +} diff --git a/assets/translations/fr.json b/assets/translations/fr.json new file mode 100644 index 0000000000000000000000000000000000000000..9243b745c43ec3c9f03af8f7127d356dd7a3fa85 --- /dev/null +++ b/assets/translations/fr.json @@ -0,0 +1,16 @@ +{ + "app_name": "Démineur", + + "long_press_to_quit": "Appuyer longtemps pour quitter le jeu...", + + "bottom_nav_home": "Jeu", + "bottom_nav_settings": "Réglages", + "bottom_nav_about": "Infos", + + "settings_title": "Réglages", + "settings_label_theme": "Thème de couleurs", + + "about_title": "Informations", + "about_content": "Démineur.", + "about_version": "Version : {version}" +} diff --git a/fastlane/metadata/android/en-US/changelogs/38.txt b/fastlane/metadata/android/en-US/changelogs/38.txt new file mode 100644 index 0000000000000000000000000000000000000000..6b884ec4ea3e942b988f5815114a1c3ab59810b8 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/38.txt @@ -0,0 +1 @@ +Improve game conception/architecture. diff --git a/fastlane/metadata/android/fr-FR/changelogs/38.txt b/fastlane/metadata/android/fr-FR/changelogs/38.txt new file mode 100644 index 0000000000000000000000000000000000000000..386b2cdcc090f19264599ba6dc6b215246489bce --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/38.txt @@ -0,0 +1 @@ +Amélioration de la conception/architecture du jeu. diff --git a/lib/config/menu.dart b/lib/config/menu.dart new file mode 100644 index 0000000000000000000000000000000000000000..689ccfa302de7d5d9eaa7eedbd3f62e1e5776989 --- /dev/null +++ b/lib/config/menu.dart @@ -0,0 +1,53 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:minehunter/ui/screens/about_page.dart'; +import 'package:minehunter/ui/screens/game_page.dart'; +import 'package:minehunter/ui/screens/settings_page.dart'; +import 'package:unicons/unicons.dart'; + +class MenuItem { + final String code; + final Icon icon; + final Widget page; + + const MenuItem({ + required this.code, + required this.icon, + required this.page, + }); +} + +class Menu { + static List<MenuItem> items = [ + const MenuItem( + code: 'bottom_nav_home', + icon: Icon(UniconsLine.home), + page: GamePage(), + ), + const MenuItem( + code: 'bottom_nav_settings', + icon: Icon(UniconsLine.setting), + page: SettingsPage(), + ), + const MenuItem( + code: 'bottom_nav_about', + icon: Icon(UniconsLine.info_circle), + page: AboutPage(), + ), + ]; + + static Widget getPageWidget(int pageIndex) { + return Menu.items.elementAt(pageIndex).page; + } + + static List<BottomNavigationBarItem> getMenuItems() { + return Menu.items + .map((MenuItem item) => BottomNavigationBarItem( + icon: item.icon, + label: tr(item.code), + )) + .toList(); + } + + static int itemsCount = Menu.items.length; +} diff --git a/lib/config/theme.dart b/lib/config/theme.dart new file mode 100644 index 0000000000000000000000000000000000000000..be390348c7868e7c63387df13e13c46de43f8a23 --- /dev/null +++ b/lib/config/theme.dart @@ -0,0 +1,196 @@ +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, + background: textSwatch.shade200, + onBackground: textSwatch.shade500, + onSurface: textSwatch.shade500, + surface: textSwatch.shade50, + surfaceVariant: Colors.white, + shadow: textSwatch.shade900.withOpacity(.1), +); + +final ColorScheme darkColorScheme = ColorScheme.dark( + primary: primarySwatch.shade500, + secondary: primarySwatch.shade500, + onSecondary: Colors.white, + error: errorColor, + background: const Color(0xFF171724), + onBackground: textSwatch.shade400, + onSurface: textSwatch.shade300, + surface: const Color(0xFF262630), + surfaceVariant: 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', + ), + ), +); + +final ThemeData appTheme = darkTheme; diff --git a/lib/cubit/bottom_nav_cubit.dart b/lib/cubit/bottom_nav_cubit.dart new file mode 100644 index 0000000000000000000000000000000000000000..54261399606dd4f345ef4c753fedef10fc2b1f18 --- /dev/null +++ b/lib/cubit/bottom_nav_cubit.dart @@ -0,0 +1,30 @@ +import 'package:hydrated_bloc/hydrated_bloc.dart'; +import 'package:minehunter/config/menu.dart'; + +class BottomNavCubit extends HydratedCubit<int> { + BottomNavCubit() : super(0); + + void updateIndex(int index) { + if (isIndexAllowed(index)) { + emit(index); + } else { + goToHomePage(); + } + } + + bool isIndexAllowed(int index) { + return (index >= 0) && (index < Menu.itemsCount); + } + + void goToHomePage() => emit(0); + + @override + int fromJson(Map<String, dynamic> json) { + return 0; + } + + @override + Map<String, dynamic>? toJson(int state) { + return <String, int>{'pageIndex': state}; + } +} diff --git a/lib/cubit/theme_cubit.dart b/lib/cubit/theme_cubit.dart new file mode 100644 index 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/entities/cell.dart b/lib/entities/cell.dart index 3185ed5910a6d3b94398c0577e653a6dcd737ad2..ff05ad891d83af959e8afd498a0d5457eabf4a8a 100644 --- a/lib/entities/cell.dart +++ b/lib/entities/cell.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; - import 'package:minehunter/provider/data.dart'; import 'package:minehunter/utils/board_animate.dart'; import 'package:minehunter/utils/board_utils.dart'; @@ -43,7 +42,7 @@ class Cell { onTap: () { // Set mines on board after first player try if (!myProvider.isBoardMined) { - myProvider.updateCells(BoardUtils.createBoard(myProvider, row, col)); + myProvider.updateBoard(BoardUtils.createBoard(myProvider, row, col)); myProvider.updateIsBoardMined(true); } diff --git a/lib/layout/board.dart b/lib/layout/board.dart deleted file mode 100644 index 857a84f9e8b107d12df5f3bfb6b3501bc6ac2e1b..0000000000000000000000000000000000000000 --- a/lib/layout/board.dart +++ /dev/null @@ -1,111 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:minehunter/entities/cell.dart'; -import 'package:minehunter/provider/data.dart'; - -class Board { - static Container buildGameBoard(Data myProvider) { - return Container( - margin: const EdgeInsets.all(2), - padding: const EdgeInsets.all(2), - child: Column( - children: [ - buildGameTileset(myProvider), - ], - ), - ); - } - - static Widget buildGameTileset(Data myProvider) { - List<List<Cell>> cells = myProvider.cells; - Color borderColor = myProvider.reportMode ? Colors.blue : Colors.black; - - return Container( - decoration: BoxDecoration( - color: borderColor, - borderRadius: BorderRadius.circular(2), - border: Border.all( - color: borderColor, - width: 2, - ), - ), - child: Table( - defaultColumnWidth: const IntrinsicColumnWidth(), - children: [ - for (int row = 0; row < myProvider.sizeVertical; row++) - TableRow( - children: [ - for (int col = 0; col < myProvider.sizeHorizontal; col++) - Column( - children: [ - cells[row][col].widget(myProvider, row, col), - ], - ), - ], - ), - ], - ), - ); - } - - static TextButton buildWalkIndicator(Data myProvider) { - String reportModeSuffix = myProvider.reportMode ? 'off' : 'on'; - - return TextButton( - child: Image( - image: AssetImage( - 'assets/skins/${myProvider.parameterSkin}_indicator_walk_$reportModeSuffix.png'), - fit: BoxFit.fill, - ), - onPressed: () => myProvider.updateReportMode(false), - ); - } - - static TextButton buildReportIndicator(Data myProvider) { - String reportModeSuffix = myProvider.reportMode ? 'on' : 'off'; - - return TextButton( - child: Image( - image: AssetImage( - 'assets/skins/${myProvider.parameterSkin}_indicator_report_$reportModeSuffix.png'), - fit: BoxFit.fill, - ), - onPressed: () => myProvider.updateReportMode(true), - ); - } - - static TextButton buildToggleFlagModeButton(Data myProvider) { - String reportModeSuffix = myProvider.reportMode ? 'on' : 'off'; - - return TextButton( - child: Image( - image: AssetImage( - 'assets/skins/${myProvider.parameterSkin}_button_mark_mine_$reportModeSuffix.png'), - fit: BoxFit.fill, - ), - onPressed: () => myProvider.updateReportMode(!myProvider.reportMode), - ); - } - - static Widget buildToggleFlagModeLayout(Data myProvider) { - Image paddingBlock = Image( - image: AssetImage('assets/skins/${myProvider.parameterSkin}_empty.png'), - fit: BoxFit.fill, - ); - - return Table( - defaultColumnWidth: const IntrinsicColumnWidth(), - children: [ - TableRow( - children: [ - TableCell(child: paddingBlock), - TableCell(child: buildWalkIndicator(myProvider)), - TableCell(child: buildToggleFlagModeButton(myProvider)), - TableCell(child: buildReportIndicator(myProvider)), - TableCell(child: paddingBlock), - ], - ), - ], - ); - } -} diff --git a/lib/layout/game.dart b/lib/layout/game.dart deleted file mode 100644 index 28cc6992a22131e8cac0906b9ae485cf78ffe640..0000000000000000000000000000000000000000 --- a/lib/layout/game.dart +++ /dev/null @@ -1,133 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:minehunter/layout/board.dart'; -import 'package:minehunter/provider/data.dart'; -import 'package:minehunter/utils/board_utils.dart'; -import 'package:minehunter/utils/game_utils.dart'; - -class Game { - static Widget buildGameWidget(Data myProvider) { - bool gameIsFinished = myProvider.gameWin || myProvider.gameFail; - - return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const SizedBox(height: 8), - Game.buildTopIndicatorWidget(myProvider), - const SizedBox(height: 2), - Expanded( - child: Board.buildGameBoard(myProvider), - ), - const SizedBox(height: 2), - Container( - child: gameIsFinished - ? Game.buildEndGameMessage(myProvider) - : Board.buildToggleFlagModeLayout(myProvider), - ), - const SizedBox(height: 8), - ], - ); - } - - static Widget buildTopIndicatorWidget(Data myProvider) { - int flaggedCellsCount = BoardUtils.countFlaggedCells(myProvider.cells); - int minesCount = myProvider.minesCount; - double blockSize = 40; - - Image flagIconBlock = Image( - image: AssetImage('assets/skins/${myProvider.parameterSkin}_tile_flag.png'), - fit: BoxFit.fill, - height: blockSize, - width: blockSize, - ); - Image mineIconBlock = Image( - image: AssetImage('assets/skins/${myProvider.parameterSkin}_tile_mine.png'), - fit: BoxFit.fill, - height: blockSize, - width: blockSize, - ); - Text markedMinesCountBlock = Text( - flaggedCellsCount.toString(), - style: TextStyle( - fontSize: blockSize, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - ); - Text placedMinesCountBlock = Text( - minesCount.toString(), - style: TextStyle( - fontSize: blockSize, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - ); - - return Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - markedMinesCountBlock, - flagIconBlock, - SizedBox(width: blockSize * 2), - mineIconBlock, - placedMinesCountBlock, - ], - ) - ], - ); - } - - static TextButton buildRestartGameButton(Data myProvider) { - return TextButton( - child: const Image( - image: AssetImage('assets/icons/button_back.png'), - fit: BoxFit.fill, - ), - onPressed: () => GameUtils.quitGame(myProvider), - ); - } - - static Widget buildEndGameMessage(Data myProvider) { - Image decorationImage = Image( - image: AssetImage(myProvider.gameWin - ? 'assets/icons/game_win.png' - : myProvider.gameFail - ? 'assets/icons/game_fail.png' - : ''), - fit: BoxFit.fill, - ); - - return Container( - margin: const EdgeInsets.all(2), - padding: const EdgeInsets.all(2), - child: Table( - defaultColumnWidth: const IntrinsicColumnWidth(), - children: [ - TableRow( - children: [ - Column( - children: [decorationImage], - ), - Column( - children: [ - myProvider.animationInProgress - ? decorationImage - : buildRestartGameButton(myProvider) - ], - ), - Column( - children: [decorationImage], - ), - ], - ), - ], - ), - ); - } -} diff --git a/lib/layout/parameters.dart b/lib/layout/parameters.dart deleted file mode 100644 index 5eef4e86923733fcdb9a25d93862b65174c20335..0000000000000000000000000000000000000000 --- a/lib/layout/parameters.dart +++ /dev/null @@ -1,183 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:minehunter/provider/data.dart'; -import 'package:minehunter/utils/game_utils.dart'; - -class Parameters { - static double separatorHeight = 2.0; - static double blockMargin = 3.0; - static double blockPadding = 2.0; - static Color buttonBackgroundColor = Colors.white; - static Color buttonBorderColorActive = Colors.blue; - static Color buttonBorderColorInactive = Colors.white; - static double buttonBorderWidth = 10.0; - static double buttonBorderRadius = 8.0; - static double buttonPadding = 0.0; - static double buttonMargin = 0.0; - - static Widget buildParametersSelector(Data myProvider) { - List<Widget> lines = []; - - List<String> parameters = myProvider.availableParameters; - for (int index = 0; index < parameters.length; index++) { - lines.add(buildParameterSelector(myProvider, parameters[index])); - lines.add(SizedBox(height: separatorHeight)); - } - - myProvider.loadCurrentSavedState(); - Widget buttonsBlock = myProvider.hasCurrentSavedState() - ? buildResumeGameButton(myProvider) - : buildStartNewGameButton(myProvider); - - return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox(height: separatorHeight), - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: lines, - ), - ), - SizedBox(height: separatorHeight), - Container( - child: buttonsBlock, - ), - ], - ); - } - - static Image buildImageWidget(String imageAssetCode) { - return Image( - image: AssetImage('assets/icons/$imageAssetCode.png'), - fit: BoxFit.fill, - ); - } - - static Widget buildImageContainerWidget(String imageAssetCode) { - return Container( - child: buildImageWidget(imageAssetCode), - ); - } - - static Column buildDecorationImageWidget() { - return Column( - children: [ - TextButton( - child: buildImageContainerWidget('placeholder'), - onPressed: () {}, - ), - ], - ); - } - - static Widget buildStartNewGameButton(Data myProvider) { - return Container( - margin: EdgeInsets.all(blockMargin), - padding: 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 buildResumeGameButton(Data myProvider) { - return Container( - margin: EdgeInsets.all(blockMargin), - padding: EdgeInsets.all(blockPadding), - child: Table( - defaultColumnWidth: const IntrinsicColumnWidth(), - children: [ - TableRow( - children: [ - Column( - children: [ - TextButton( - child: buildImageContainerWidget('button_delete_saved_game'), - onPressed: () => GameUtils.deleteSavedGame(myProvider), - ), - ], - ), - Column( - children: [ - TextButton( - child: buildImageContainerWidget('button_resume_game'), - onPressed: () => GameUtils.resumeSavedGame(myProvider), - ), - ], - ), - buildDecorationImageWidget(), - ], - ), - ], - ), - ); - } - - static Widget buildParameterSelector(Data myProvider, String parameterCode) { - 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) { - String currentValue = myProvider.getParameterValue(parameterCode).toString(); - - bool isActive = (parameterValue == currentValue); - String imageAsset = '${parameterCode}_$parameterValue'; - - return TextButton( - child: Container( - margin: EdgeInsets.all(buttonMargin), - padding: 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 b2ecccb6d0cc7b0c09c9b602e7dfebddff56d48b..15ee02989e0c11b9604421e91764241a28edf641 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:flutter_bloc/flutter_bloc.dart'; +import 'package:hive/hive.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:overlay_support/overlay_support.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:provider/provider.dart'; +import 'package:minehunter/config/theme.dart'; +import 'package:minehunter/cubit/bottom_nav_cubit.dart'; +import 'package:minehunter/cubit/theme_cubit.dart'; import 'package:minehunter/provider/data.dart'; -import 'package:minehunter/screens/home.dart'; +import 'package:minehunter/ui/skeleton.dart'; -void main() { +void main() async { + /// Initialize packages WidgetsFlutterBinding.ensureInitialized(); - SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]) - .then((value) => runApp(const MyApp())); + await EasyLocalization.ensureInitialized(); + final Directory tmpDir = await getTemporaryDirectory(); + Hive.init(tmpDir.toString()); + HydratedBloc.storage = await HydratedStorage.build( + storageDirectory: tmpDir, + ); + + 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,19 +44,34 @@ 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( - debugShowCheckedModeBanner: false, - theme: ThemeData( - primaryColor: Colors.blue, - visualDensity: VisualDensity.adaptivePlatformDensity, - ), - home: const Home(), - routes: { - Home.id: (context) => const Home(), + return MultiBlocProvider( + providers: [ + BlocProvider<BottomNavCubit>(create: (context) => BottomNavCubit()), + BlocProvider<ThemeCubit>(create: (context) => ThemeCubit()), + ], + child: BlocBuilder<ThemeCubit, ThemeModeState>( + builder: (BuildContext context, ThemeModeState state) { + return ChangeNotifierProvider( + create: (BuildContext context) => Data(), + child: Consumer<Data>( + builder: (context, data, child) { + return OverlaySupport( + child: MaterialApp( + title: 'Minehunter', + 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, + ), + ); }, ), ); diff --git a/lib/provider/data.dart b/lib/provider/data.dart index f1a385a5902ef22e832e63dc269aee25af344ad7..c37658a4d8ada37c3d0d216ed3aeb8cd32ca7f16 100644 --- a/lib/provider/data.dart +++ b/lib/provider/data.dart @@ -4,6 +4,10 @@ import 'package:flutter/foundation.dart'; import 'package:minehunter/entities/cell.dart'; import 'package:shared_preferences/shared_preferences.dart'; +typedef Board = List<List<Cell>>; +typedef AnimatedBoard = List<List<bool>>; +typedef AnimatedBoardSequence = List<AnimatedBoard>; + class Data extends ChangeNotifier { // Configuration available parameters final List<String> _availableParameters = ['level', 'size', 'skin']; @@ -37,7 +41,7 @@ class Data extends ChangeNotifier { bool _animationInProgress = false; int _sizeVertical = 0; int _sizeHorizontal = 0; - List<List<Cell>> _cells = []; + Board _board = []; bool _isBoardMined = false; int _minesCount = 0; bool _reportMode = false; @@ -136,13 +140,13 @@ class Data extends ChangeNotifier { String computeCurrentGameState() { String cellsValues = ''; - for (int rowIndex = 0; rowIndex < _cells.length; rowIndex++) { - for (int colIndex = 0; colIndex < _cells[rowIndex].length; colIndex++) { - cellsValues += _cells[rowIndex][colIndex].isMined ? 'X' : ' '; - cellsValues += _cells[rowIndex][colIndex].isExplored ? 'E' : ' '; - cellsValues += _cells[rowIndex][colIndex].isMarked ? 'P' : ' '; - cellsValues += _cells[rowIndex][colIndex].isExploded ? '*' : ' '; - cellsValues += _cells[rowIndex][colIndex].minesCountAround.toString(); + for (int rowIndex = 0; rowIndex < _board.length; rowIndex++) { + for (int colIndex = 0; colIndex < _board[rowIndex].length; colIndex++) { + cellsValues += _board[rowIndex][colIndex].isMined ? 'X' : ' '; + cellsValues += _board[rowIndex][colIndex].isExplored ? 'E' : ' '; + cellsValues += _board[rowIndex][colIndex].isMarked ? 'P' : ' '; + cellsValues += _board[rowIndex][colIndex].isExploded ? '*' : ' '; + cellsValues += _board[rowIndex][colIndex].minesCountAround.toString(); cellsValues += ';'; } } @@ -216,17 +220,17 @@ class Data extends ChangeNotifier { notifyListeners(); } - List<List<Cell>> get cells => _cells; - void updateCells(List<List<Cell>> cells) { - _cells = cells; + Board get board => _board; + void updateBoard(Board board) { + _board = board; notifyListeners(); } void setCellAsExplored(int row, int col) { - _cells[row][col].isExplored = true; - _cells[row][col].isMarked = false; - if (_cells[row][col].isMined) { - _cells[row][col].isExploded = true; + _board[row][col].isExplored = true; + _board[row][col].isMarked = false; + if (_board[row][col].isMined) { + _board[row][col].isExploded = true; } saveCurrentGameState(); @@ -234,7 +238,7 @@ class Data extends ChangeNotifier { } void toggleCellMark(int row, int col) { - _cells[row][col].isMarked = !_cells[row][col].isMarked; + _board[row][col].isMarked = !_board[row][col].isMarked; saveCurrentGameState(); notifyListeners(); @@ -271,7 +275,7 @@ class Data extends ChangeNotifier { void setAnimatedBackground(List animatedCellsPattern) { for (int row = 0; row < _sizeVertical; row++) { for (int col = 0; col < _sizeHorizontal; col++) { - _cells[row][col].isAnimated = animatedCellsPattern[row][col]; + _board[row][col].isAnimated = animatedCellsPattern[row][col]; } } notifyListeners(); @@ -280,7 +284,7 @@ class Data extends ChangeNotifier { void resetAnimatedBackground() { for (int row = 0; row < _sizeVertical; row++) { for (int col = 0; col < _sizeHorizontal; col++) { - _cells[row][col].isAnimated = false; + _board[row][col].isAnimated = false; } } } diff --git a/lib/ui/layout/game.dart b/lib/ui/layout/game.dart new file mode 100644 index 0000000000000000000000000000000000000000..36796492ca10e14dc7f8d78aa8251118ec275ced --- /dev/null +++ b/lib/ui/layout/game.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +import 'package:minehunter/provider/data.dart'; +import 'package:minehunter/ui/layout/tileset.dart'; +import 'package:minehunter/ui/widgets/game/indicator_top.dart'; +import 'package:minehunter/ui/widgets/game/message_game_end.dart'; +import 'package:minehunter/ui/widgets/game/mode_toggle.dart'; + +class Game extends StatelessWidget { + const Game({super.key, required this.myProvider}); + + final Data myProvider; + + @override + Widget build(BuildContext context) { + final bool gameIsFinished = myProvider.gameWin || myProvider.gameFail; + + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 8), + TopIndicator(myProvider: myProvider), + const SizedBox(height: 2), + Expanded( + child: Tileset(myProvider: myProvider), + ), + const SizedBox(height: 2), + Container( + child: gameIsFinished + ? EndGameMessage(myProvider: myProvider) + : ToggleGameMode(myProvider: myProvider), + ), + const SizedBox(height: 8), + ], + ); + } +} diff --git a/lib/ui/layout/parameters.dart b/lib/ui/layout/parameters.dart new file mode 100644 index 0000000000000000000000000000000000000000..ee2502a4129b96df781a6d66b1156d56ee17f9a3 --- /dev/null +++ b/lib/ui/layout/parameters.dart @@ -0,0 +1,129 @@ +import 'package:flutter/material.dart'; + +import 'package:minehunter/provider/data.dart'; +import 'package:minehunter/ui/widgets/home/button_game_resume.dart'; +import 'package:minehunter/ui/widgets/home/button_game_start_new.dart'; + +class Parameters extends StatelessWidget { + const Parameters({super.key, required this.myProvider}); + + final Data myProvider; + + 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; + + @override + Widget build(BuildContext context) { + 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)); + } + + myProvider.loadCurrentSavedState(); + Widget buttonsBlock = myProvider.hasCurrentSavedState() + ? ResumeGameButton(myProvider: myProvider) + : StartNewGameButton(myProvider: 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: () {}, + ), + ], + ); + } + + 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]) + ], + ), + ], + ), + ], + ); + } + + 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/ui/layout/tileset.dart b/lib/ui/layout/tileset.dart new file mode 100644 index 0000000000000000000000000000000000000000..f5986aab892ea72467bd80636da3ee85c1cf600f --- /dev/null +++ b/lib/ui/layout/tileset.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; + +import 'package:minehunter/provider/data.dart'; + +class Tileset extends StatelessWidget { + const Tileset({super.key, required this.myProvider}); + + final Data myProvider; + + @override + Widget build(BuildContext context) { + final Board board = myProvider.board; + final Color borderColor = myProvider.reportMode ? Colors.blue : Colors.black; + + return Container( + margin: const EdgeInsets.all(2), + padding: const EdgeInsets.all(2), + child: Column( + children: [ + Container( + decoration: BoxDecoration( + color: borderColor, + borderRadius: BorderRadius.circular(2), + border: Border.all( + color: borderColor, + width: 2, + ), + ), + child: Table( + defaultColumnWidth: const IntrinsicColumnWidth(), + children: [ + for (int row = 0; row < myProvider.sizeVertical; row++) + TableRow( + children: [ + for (int col = 0; col < myProvider.sizeHorizontal; col++) + Column( + children: [ + board[row][col].widget(myProvider, row, col), + ], + ), + ], + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/ui/screens/about_page.dart b/lib/ui/screens/about_page.dart new file mode 100644 index 0000000000000000000000000000000000000000..39efd8cc91f36d4d43227ff614a00e5bb1c60ddf --- /dev/null +++ b/lib/ui/screens/about_page.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:minehunter/ui/widgets/header_app.dart'; + +class AboutPage extends StatelessWidget { + const AboutPage({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 AppHeader(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/screens/home.dart b/lib/ui/screens/game_page.dart similarity index 61% rename from lib/screens/home.dart rename to lib/ui/screens/game_page.dart index fa7236ac84c423b7ed4f47debfbc436ed7e6050a..cce017474d37d22a29b137981cbdd6e2bd3eb8ae 100644 --- a/lib/screens/home.dart +++ b/lib/ui/screens/game_page.dart @@ -1,27 +1,23 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:overlay_support/overlay_support.dart'; -import 'package:minehunter/layout/game.dart'; -import 'package:minehunter/layout/parameters.dart'; import 'package:minehunter/provider/data.dart'; -import 'package:minehunter/utils/game_utils.dart'; +import 'package:minehunter/ui/layout/game.dart'; +import 'package:minehunter/ui/layout/parameters.dart'; -class Home extends StatefulWidget { - const Home({super.key}); - - static const String id = 'home'; +class GamePage extends StatefulWidget { + const GamePage({super.key}); @override - HomeState createState() => HomeState(); + GamePageState createState() => GamePageState(); } -class HomeState extends State<Home> { +class GamePageState extends State<GamePage> { @override void initState() { super.initState(); - Data myProvider = Provider.of<Data>(context, listen: false); + final Data myProvider = Provider.of<Data>(context, listen: false); myProvider.initParametersValues(); } @@ -84,29 +80,11 @@ class HomeState extends State<Home> { myProvider.updateAssetsPreloaded(true); } - final List<Widget> menuActions = []; - - if (myProvider.gameIsRunning) { - 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: SafeArea( - child: Center( - child: myProvider.gameIsRunning - ? Game.buildGameWidget(myProvider) - : Parameters.buildParametersSelector(myProvider), - ), + return SafeArea( + child: Center( + child: myProvider.gameIsRunning + ? Game(myProvider: myProvider) + : Parameters(myProvider: myProvider), ), ); } diff --git a/lib/ui/screens/settings_page.dart b/lib/ui/screens/settings_page.dart new file mode 100644 index 0000000000000000000000000000000000000000..7f2146872b3e8a613ea407c3d3517d6c540e026e --- /dev/null +++ b/lib/ui/screens/settings_page.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +import 'package:minehunter/ui/widgets/header_app.dart'; +import 'package:minehunter/ui/widgets/settings/settings_form.dart'; + +class SettingsPage extends StatelessWidget { + const SettingsPage({super.key}); + + @override + Widget build(BuildContext context) { + return const Padding( + padding: EdgeInsets.symmetric(horizontal: 8), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: <Widget>[ + SizedBox(height: 8), + AppHeader(text: 'settings_title'), + SizedBox(height: 8), + SettingsForm(), + ], + ), + ); + } +} diff --git a/lib/ui/skeleton.dart b/lib/ui/skeleton.dart new file mode 100644 index 0000000000000000000000000000000000000000..6e732da251c47f722436a90562c5c6c4ab2d484e --- /dev/null +++ b/lib/ui/skeleton.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_swipe/flutter_swipe.dart'; +import 'package:provider/provider.dart'; + +import 'package:minehunter/config/menu.dart'; +import 'package:minehunter/cubit/bottom_nav_cubit.dart'; +import 'package:minehunter/provider/data.dart'; +import 'package:minehunter/ui/widgets/app_bar.dart'; +import 'package:minehunter/ui/widgets/bottom_nav_bar.dart'; + +class SkeletonScreen extends StatefulWidget { + const SkeletonScreen({super.key}); + + @override + State<SkeletonScreen> createState() => _SkeletonScreenState(); +} + +class _SkeletonScreenState extends State<SkeletonScreen> { + @override + Widget build(BuildContext context) { + final Data myProvider = Provider.of<Data>(context); + + return Scaffold( + extendBodyBehindAppBar: false, + appBar: StandardAppBar(myProvider: myProvider), + body: Swiper( + itemCount: Menu.itemsCount, + itemBuilder: (BuildContext context, int index) { + return Menu.getPageWidget(index); + }, + pagination: SwiperPagination( + margin: const EdgeInsets.all(0), + builder: SwiperCustomPagination( + builder: (BuildContext context, SwiperPluginConfig config) { + return BottomNavBar(swipeController: config.controller); + }, + ), + ), + onIndexChanged: (newPageIndex) { + BlocProvider.of<BottomNavCubit>(context).updateIndex(newPageIndex); + }, + outer: true, + loop: false, + ), + backgroundColor: Theme.of(context).colorScheme.background, + ); + } +} diff --git a/lib/ui/widgets/app_bar.dart b/lib/ui/widgets/app_bar.dart new file mode 100644 index 0000000000000000000000000000000000000000..c53fa30eb979bd30e677d20e7bd09b146328f47e --- /dev/null +++ b/lib/ui/widgets/app_bar.dart @@ -0,0 +1,37 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:overlay_support/overlay_support.dart'; + +import 'package:minehunter/provider/data.dart'; +import 'package:minehunter/ui/widgets/header_app.dart'; +import 'package:minehunter/utils/game_utils.dart'; + +class StandardAppBar extends StatelessWidget implements PreferredSizeWidget { + const StandardAppBar({super.key, required this.myProvider}); + + final Data myProvider; + + @override + Widget build(BuildContext context) { + final List<Widget> menuActions = []; + + if (myProvider.gameIsRunning) { + menuActions.add(TextButton( + child: const Image( + image: AssetImage('assets/icons/button_back.png'), + fit: BoxFit.fill, + ), + onPressed: () => toast(tr('long_press_to_quit')), + onLongPress: () => GameUtils.quitGame(myProvider), + )); + } + + return AppBar( + title: const AppHeader(text: 'app_name'), + actions: menuActions, + ); + } + + @override + Size get preferredSize => const Size.fromHeight(50); +} diff --git a/lib/ui/widgets/bottom_nav_bar.dart b/lib/ui/widgets/bottom_nav_bar.dart new file mode 100644 index 0000000000000000000000000000000000000000..341d93ea63a3808dfc9aa8e079bfc6b8d13430ff --- /dev/null +++ b/lib/ui/widgets/bottom_nav_bar.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_swipe/flutter_swipe.dart'; + +import 'package:minehunter/config/menu.dart'; +import 'package:minehunter/cubit/bottom_nav_cubit.dart'; + +class BottomNavBar extends StatelessWidget { + const BottomNavBar({super.key, required this.swipeController}); + + final SwiperController swipeController; + + @override + Widget build(BuildContext context) { + return Card( + margin: const EdgeInsets.all(0), + elevation: 4, + shadowColor: Theme.of(context).colorScheme.shadow, + color: Theme.of(context).colorScheme.surfaceVariant, + shape: const ContinuousRectangleBorder(), + child: BlocBuilder<BottomNavCubit, int>( + builder: (BuildContext context, int state) { + return BottomNavigationBar( + currentIndex: state, + onTap: (int index) { + context.read<BottomNavCubit>().updateIndex(index); + swipeController.move(index); + }, + type: BottomNavigationBarType.fixed, + elevation: 0, + backgroundColor: Colors.transparent, + selectedItemColor: Theme.of(context).colorScheme.primary, + unselectedItemColor: Theme.of(context).textTheme.bodySmall!.color, + items: Menu.getMenuItems(), + ); + }, + ), + ); + } +} diff --git a/lib/ui/widgets/game/indicator_top.dart b/lib/ui/widgets/game/indicator_top.dart new file mode 100644 index 0000000000000000000000000000000000000000..bf9ea9af42c4b24332880420b92b73d0db0a97dd --- /dev/null +++ b/lib/ui/widgets/game/indicator_top.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; + +import 'package:minehunter/provider/data.dart'; +import 'package:minehunter/utils/board_utils.dart'; + +class TopIndicator extends StatelessWidget { + const TopIndicator({super.key, required this.myProvider}); + + final Data myProvider; + + @override + Widget build(BuildContext context) { + final int flaggedCellsCount = BoardUtils.countFlaggedCells(myProvider.board); + final int minesCount = myProvider.minesCount; + const double blockSize = 40; + + final Image flagIconBlock = Image( + image: AssetImage('assets/skins/${myProvider.parameterSkin}_tile_flag.png'), + fit: BoxFit.fill, + height: blockSize, + width: blockSize, + ); + final Image mineIconBlock = Image( + image: AssetImage('assets/skins/${myProvider.parameterSkin}_tile_mine.png'), + fit: BoxFit.fill, + height: blockSize, + width: blockSize, + ); + final Text markedMinesCountBlock = Text( + flaggedCellsCount.toString(), + style: TextStyle( + fontSize: blockSize, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ); + final Text placedMinesCountBlock = Text( + minesCount.toString(), + style: TextStyle( + fontSize: blockSize, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ); + + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + markedMinesCountBlock, + flagIconBlock, + const SizedBox(width: blockSize * 2), + mineIconBlock, + placedMinesCountBlock, + ], + ) + ], + ); + } +} diff --git a/lib/ui/widgets/game/message_game_end.dart b/lib/ui/widgets/game/message_game_end.dart new file mode 100644 index 0000000000000000000000000000000000000000..9eff7f183a81f104fd60228649a1a8745c525a2a --- /dev/null +++ b/lib/ui/widgets/game/message_game_end.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; + +import 'package:minehunter/provider/data.dart'; +import 'package:minehunter/ui/widgets/home/button_game_restart.dart'; + +class EndGameMessage extends StatelessWidget { + const EndGameMessage({super.key, required this.myProvider}); + + final Data myProvider; + + @override + Widget build(BuildContext context) { + final Image decorationImage = Image( + image: AssetImage(myProvider.gameWin + ? 'assets/icons/game_win.png' + : myProvider.gameFail + ? 'assets/icons/game_fail.png' + : ''), + fit: BoxFit.fill, + ); + + return Container( + margin: const EdgeInsets.all(2), + padding: const EdgeInsets.all(2), + child: Table( + defaultColumnWidth: const IntrinsicColumnWidth(), + children: [ + TableRow( + children: [ + Column( + children: [decorationImage], + ), + Column( + children: [ + myProvider.animationInProgress + ? decorationImage + : RestartGameButton(myProvider: myProvider) + ], + ), + Column( + children: [decorationImage], + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/ui/widgets/game/mode_button_toggle.dart b/lib/ui/widgets/game/mode_button_toggle.dart new file mode 100644 index 0000000000000000000000000000000000000000..7f6f7aae3c52a5725871fc8839a07e5c03f4582d --- /dev/null +++ b/lib/ui/widgets/game/mode_button_toggle.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +import 'package:minehunter/provider/data.dart'; + +class ToggleGameModeButton extends StatelessWidget { + const ToggleGameModeButton({super.key, required this.myProvider}); + + final Data myProvider; + + @override + Widget build(BuildContext context) { + final String reportModeSuffix = myProvider.reportMode ? 'on' : 'off'; + + return TextButton( + child: Image( + image: AssetImage( + 'assets/skins/${myProvider.parameterSkin}_button_mark_mine_$reportModeSuffix.png'), + fit: BoxFit.fill, + ), + onPressed: () => myProvider.updateReportMode(!myProvider.reportMode), + ); + } +} diff --git a/lib/ui/widgets/game/mode_indicator_report.dart b/lib/ui/widgets/game/mode_indicator_report.dart new file mode 100644 index 0000000000000000000000000000000000000000..aac8757d3e808c3a8e98d0d9a561ffcb3f2e06d0 --- /dev/null +++ b/lib/ui/widgets/game/mode_indicator_report.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +import 'package:minehunter/provider/data.dart'; + +class GameModeIndicatorReport extends StatelessWidget { + const GameModeIndicatorReport({super.key, required this.myProvider}); + + final Data myProvider; + + @override + Widget build(BuildContext context) { + final String reportModeSuffix = myProvider.reportMode ? 'on' : 'off'; + + return TextButton( + child: Image( + image: AssetImage( + 'assets/skins/${myProvider.parameterSkin}_indicator_report_$reportModeSuffix.png'), + fit: BoxFit.fill, + ), + onPressed: () => myProvider.updateReportMode(true), + ); + } +} diff --git a/lib/ui/widgets/game/mode_indicator_walk.dart b/lib/ui/widgets/game/mode_indicator_walk.dart new file mode 100644 index 0000000000000000000000000000000000000000..be4cef83e9adce5e990bbc0106f8267b7c22d1ed --- /dev/null +++ b/lib/ui/widgets/game/mode_indicator_walk.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +import 'package:minehunter/provider/data.dart'; + +class GameModeIndicatorWalk extends StatelessWidget { + const GameModeIndicatorWalk({super.key, required this.myProvider}); + + final Data myProvider; + + @override + Widget build(BuildContext context) { + final String reportModeSuffix = myProvider.reportMode ? 'off' : 'on'; + + return TextButton( + child: Image( + image: AssetImage( + 'assets/skins/${myProvider.parameterSkin}_indicator_walk_$reportModeSuffix.png'), + fit: BoxFit.fill, + ), + onPressed: () => myProvider.updateReportMode(false), + ); + } +} diff --git a/lib/ui/widgets/game/mode_toggle.dart b/lib/ui/widgets/game/mode_toggle.dart new file mode 100644 index 0000000000000000000000000000000000000000..6a438d6cf16a4791366f4cba64574ea2f73287d6 --- /dev/null +++ b/lib/ui/widgets/game/mode_toggle.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; + +import 'package:minehunter/provider/data.dart'; +import 'package:minehunter/ui/widgets/game/mode_button_toggle.dart'; +import 'package:minehunter/ui/widgets/game/mode_indicator_report.dart'; +import 'package:minehunter/ui/widgets/game/mode_indicator_walk.dart'; + +class ToggleGameMode extends StatelessWidget { + const ToggleGameMode({super.key, required this.myProvider}); + + final Data myProvider; + + @override + Widget build(BuildContext context) { + final Image paddingBlock = Image( + image: AssetImage('assets/skins/${myProvider.parameterSkin}_empty.png'), + fit: BoxFit.fill, + ); + + return Table( + defaultColumnWidth: const IntrinsicColumnWidth(), + children: [ + TableRow( + children: [ + TableCell(child: paddingBlock), + TableCell(child: GameModeIndicatorWalk(myProvider: myProvider)), + TableCell(child: ToggleGameModeButton(myProvider: myProvider)), + TableCell(child: GameModeIndicatorReport(myProvider: myProvider)), + TableCell(child: paddingBlock), + ], + ), + ], + ); + } +} diff --git a/lib/ui/widgets/header_app.dart b/lib/ui/widgets/header_app.dart new file mode 100644 index 0000000000000000000000000000000000000000..bf54b77375fbd0260f876f2885d0572b71715383 --- /dev/null +++ b/lib/ui/widgets/header_app.dart @@ -0,0 +1,23 @@ +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 Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + tr(text), + textAlign: TextAlign.start, + style: Theme.of(context).textTheme.headlineMedium!.apply(fontWeightDelta: 2), + ), + ], + ); + } +} diff --git a/lib/ui/widgets/home/button_game_restart.dart b/lib/ui/widgets/home/button_game_restart.dart new file mode 100644 index 0000000000000000000000000000000000000000..2fecb871a622f85764e40a512fbed7948da3a53a --- /dev/null +++ b/lib/ui/widgets/home/button_game_restart.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; + +import 'package:minehunter/provider/data.dart'; +import 'package:minehunter/utils/game_utils.dart'; + +class RestartGameButton extends StatelessWidget { + const RestartGameButton({super.key, required this.myProvider}); + + final Data myProvider; + + @override + Widget build(BuildContext context) { + return TextButton( + child: const Image( + image: AssetImage('assets/icons/button_back.png'), + fit: BoxFit.fill, + ), + onPressed: () => GameUtils.quitGame(myProvider), + ); + } +} diff --git a/lib/ui/widgets/home/button_game_resume.dart b/lib/ui/widgets/home/button_game_resume.dart new file mode 100644 index 0000000000000000000000000000000000000000..2a1cc71f359762fe09962f62b787912cb7ddf351 --- /dev/null +++ b/lib/ui/widgets/home/button_game_resume.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; + +import 'package:minehunter/ui/layout/parameters.dart'; +import 'package:minehunter/utils/game_utils.dart'; + +class ResumeGameButton extends Parameters { + const ResumeGameButton({super.key, required super.myProvider}); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.all(Parameters.blockMargin), + padding: const EdgeInsets.all(Parameters.blockPadding), + child: Table( + defaultColumnWidth: const IntrinsicColumnWidth(), + children: [ + TableRow( + children: [ + Column( + children: [ + TextButton( + child: Parameters.buildImageContainerWidget('button_delete_saved_game'), + onPressed: () => GameUtils.deleteSavedGame(myProvider), + ), + ], + ), + Column( + children: [ + TextButton( + child: Parameters.buildImageContainerWidget('button_resume_game'), + onPressed: () => GameUtils.resumeSavedGame(myProvider), + ), + ], + ), + Parameters.buildDecorationImageWidget(), + ], + ), + ], + ), + ); + } +} diff --git a/lib/ui/widgets/home/button_game_start_new.dart b/lib/ui/widgets/home/button_game_start_new.dart new file mode 100644 index 0000000000000000000000000000000000000000..1d58c490152db767a5a0a64c5a5ce5655f2ef6ec --- /dev/null +++ b/lib/ui/widgets/home/button_game_start_new.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +import 'package:minehunter/provider/data.dart'; +import 'package:minehunter/ui/layout/parameters.dart'; +import 'package:minehunter/utils/game_utils.dart'; + +class StartNewGameButton extends StatelessWidget { + const StartNewGameButton({super.key, required this.myProvider}); + + final Data myProvider; + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.all(Parameters.blockMargin), + padding: const EdgeInsets.all(Parameters.blockPadding), + child: Table( + defaultColumnWidth: const IntrinsicColumnWidth(), + children: [ + TableRow( + children: [ + Parameters.buildDecorationImageWidget(), + Column( + children: [ + TextButton( + child: Parameters.buildImageContainerWidget('button_start'), + onPressed: () => GameUtils.startNewGame(myProvider), + ), + ], + ), + Parameters.buildDecorationImageWidget(), + ], + ), + ], + ), + ); + } +} diff --git a/lib/ui/widgets/settings/settings_form.dart b/lib/ui/widgets/settings/settings_form.dart new file mode 100644 index 0000000000000000000000000000000000000000..910c9507b2d9f4778e2e046faa35c93af55a6377 --- /dev/null +++ b/lib/ui/widgets/settings/settings_form.dart @@ -0,0 +1,63 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:unicons/unicons.dart'; + +import 'package:minehunter/ui/widgets/settings/theme_card.dart'; + +class SettingsForm extends StatefulWidget { + const SettingsForm({super.key}); + + @override + State<SettingsForm> createState() => _SettingsFormState(); +} + +class _SettingsFormState extends State<SettingsForm> { + @override + void dispose() { + super.dispose(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: <Widget>[ + // Light/dark theme + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: <Widget>[ + const Text('settings_label_theme').tr(), + const Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ThemeCard( + mode: ThemeMode.system, + icon: UniconsLine.cog, + ), + ThemeCard( + mode: ThemeMode.light, + icon: UniconsLine.sun, + ), + ThemeCard( + mode: ThemeMode.dark, + icon: UniconsLine.moon, + ) + ], + ), + ], + ), + + const SizedBox(height: 16), + ], + ); + } +} diff --git a/lib/ui/widgets/settings/theme_card.dart b/lib/ui/widgets/settings/theme_card.dart new file mode 100644 index 0000000000000000000000000000000000000000..f90b335498779f51326e4bc0e12552be4f3c325d --- /dev/null +++ b/lib/ui/widgets/settings/theme_card.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:minehunter/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/utils/board_animate.dart b/lib/utils/board_animate.dart index a85c2de5a9cdb5cb52e9196d5a8773620e7822f9..45e1352a3469ffe2331713491cb2291c53f3dbd4 100644 --- a/lib/utils/board_animate.dart +++ b/lib/utils/board_animate.dart @@ -5,15 +5,15 @@ import 'package:minehunter/provider/data.dart'; class BoardAnimate { // Start game animation: blinking tiles - static List<List<List<bool>>> createStartGameAnimationPatterns(Data myProvider) { - final List<List<List<bool>>> patterns = []; + static AnimatedBoardSequence createStartGameAnimationPatterns(Data myProvider) { + final AnimatedBoardSequence patterns = []; const int patternsCount = 4; final int sizeHorizontal = myProvider.sizeHorizontal; final int sizeVertical = myProvider.sizeVertical; for (int patternIndex = 0; patternIndex < patternsCount; patternIndex++) { - final List<List<bool>> pattern = []; + final AnimatedBoard pattern = []; for (int row = 0; row < sizeVertical; row++) { final List<bool> patternRow = []; for (int col = 0; col < sizeHorizontal; col++) { @@ -28,8 +28,8 @@ class BoardAnimate { } // Failed game animation: explosions blowing from exploded mines - static List<List<List<bool>>> createExplosionAnimationPatterns(Data myProvider) { - final List<List<List<bool>>> patterns = []; + static AnimatedBoardSequence createExplosionAnimationPatterns(Data myProvider) { + final AnimatedBoardSequence patterns = []; final int sizeHorizontal = myProvider.sizeHorizontal; final int sizeVertical = myProvider.sizeVertical; @@ -37,7 +37,7 @@ class BoardAnimate { final List<List<int>> explodedMines = []; for (int row = 0; row < sizeVertical; row++) { for (int col = 0; col < sizeHorizontal; col++) { - if (myProvider.cells[row][col].isExploded) { + if (myProvider.board[row][col].isExploded) { explodedMines.add([row, col]); } } @@ -49,7 +49,7 @@ class BoardAnimate { final int patternsCount = max(sizeHorizontal, sizeVertical); for (int patternIndex = 0; patternIndex < patternsCount; patternIndex++) { - final List<List<bool>> pattern = []; + final AnimatedBoard pattern = []; for (int row = 0; row < sizeVertical; row++) { final List<bool> patternRow = []; for (int col = 0; col < sizeHorizontal; col++) { @@ -70,15 +70,15 @@ class BoardAnimate { } // Win game animation: rotating rays from center - static List<List<List<bool>>> createWinGameAnimationPatterns(Data myProvider) { - final List<List<List<bool>>> patterns = []; + static AnimatedBoardSequence createWinGameAnimationPatterns(Data myProvider) { + final AnimatedBoardSequence patterns = []; const int patternsCount = 20; final int sizeHorizontal = myProvider.sizeHorizontal; final int sizeVertical = myProvider.sizeVertical; for (int patternIndex = 0; patternIndex < patternsCount; patternIndex++) { - List<List<bool>> pattern = []; + AnimatedBoard pattern = []; for (int row = 0; row < sizeVertical; row++) { List<bool> patternRow = []; for (int col = 0; col < sizeHorizontal; col++) { @@ -94,15 +94,15 @@ class BoardAnimate { } // Default multi-purpose animation: sliding stripes, from top left to right bottom - static List<List<List<bool>>> createDefaultAnimationPatterns(Data myProvider) { - final List<List<List<bool>>> patterns = []; + static AnimatedBoardSequence createDefaultAnimationPatterns(Data myProvider) { + final AnimatedBoardSequence patterns = []; const int patternsCount = 16; final int sizeHorizontal = myProvider.sizeHorizontal; final int sizeVertical = myProvider.sizeVertical; for (int patternIndex = 0; patternIndex < patternsCount; patternIndex++) { - final List<List<bool>> pattern = []; + final AnimatedBoard pattern = []; for (int row = 0; row < sizeVertical; row++) { final List<bool> patternRow = []; for (int col = 0; col < sizeHorizontal; col++) { @@ -117,7 +117,7 @@ class BoardAnimate { } static void startAnimation(Data myProvider, String animationType) { - List<List<List<bool>>> patterns = []; + AnimatedBoardSequence patterns = []; switch (animationType) { case 'start': diff --git a/lib/utils/board_utils.dart b/lib/utils/board_utils.dart index de04d0782a40fddf6cb39b0f43b2c2c04a2cfffb..695d5bf72c84b1a53c86b7ea1785e5da9e520e1d 100644 --- a/lib/utils/board_utils.dart +++ b/lib/utils/board_utils.dart @@ -45,17 +45,17 @@ class BoardUtils { printlog(''); } - static List<List<Cell>> createEmptyBoard(int sizeHorizontal, int sizeVertical) { - final List<List<Cell>> cells = []; + static Board createEmptyBoard(int sizeHorizontal, int sizeVertical) { + final Board board = []; for (int rowIndex = 0; rowIndex < sizeVertical; rowIndex++) { final List<Cell> row = []; for (int colIndex = 0; colIndex < sizeHorizontal; colIndex++) { row.add(Cell(false)); } - cells.add(row); + board.add(row); } - return cells; + return board; } static int getMinesCount(int sizeHorizontal, int sizeVertical, String level) { @@ -93,11 +93,11 @@ class BoardUtils { static void createInitialEmptyBoard(Data myProvider) { myProvider.updateIsBoardMined(false); myProvider - .updateCells(createEmptyBoard(myProvider.sizeHorizontal, myProvider.sizeVertical)); + .updateBoard(createEmptyBoard(myProvider.sizeHorizontal, myProvider.sizeVertical)); } - static List<List<Cell>> createBoard(Data myProvider, int forbiddenRow, int forbiddenCol) { - final List<List<Cell>> cells = myProvider.cells; + static Board createBoard(Data myProvider, int forbiddenRow, int forbiddenCol) { + final Board board = myProvider.board; final int sizeHorizontal = myProvider.sizeHorizontal; final int sizeVertical = myProvider.sizeVertical; @@ -114,23 +114,23 @@ class BoardUtils { // Put random mines on board for (int mineIndex = 0; mineIndex < myProvider.minesCount; mineIndex++) { - cells[allowedCells[mineIndex][0]][allowedCells[mineIndex][1]].isMined = true; + board[allowedCells[mineIndex][0]][allowedCells[mineIndex][1]].isMined = true; } // Compute all mines counts on cells for (int row = 0; row < sizeVertical; row++) { for (int col = 0; col < sizeHorizontal; col++) { - cells[row][col].minesCountAround = getMinesCountAround(cells, row, col); + board[row][col].minesCountAround = getMinesCountAround(board, row, col); } } - printGrid(cells); + printGrid(board); - return cells; + return board; } - static List<List<Cell>> createBoardFromSavedState(Data myProvider, String savedBoard) { - final List<List<Cell>> board = []; + static Board createBoardFromSavedState(Data myProvider, String savedBoard) { + final Board board = []; final int boardSize = pow((savedBoard.length / 6), 1 / 2).round(); final String boardSizeAsString = '${boardSize}x$boardSize'; myProvider.updateParameterSize(boardSizeAsString); @@ -163,7 +163,7 @@ class BoardUtils { } static void reportCell(Data myProvider, int row, int col) { - if (!myProvider.cells[row][col].isExplored) { + if (!myProvider.board[row][col].isExplored) { myProvider.toggleCellMark(row, col); } } @@ -171,12 +171,12 @@ class BoardUtils { static void walkOnCell(Data myProvider, int row, int col) { myProvider.setCellAsExplored(row, col); - if (myProvider.cells[row][col].minesCountAround == 0) { - final List<List<int>> safeCells = getAllSafeCellsAround(myProvider.cells, row, col); + if (myProvider.board[row][col].minesCountAround == 0) { + final List<List<int>> safeCells = getAllSafeCellsAround(myProvider.board, row, col); for (int safeCellIndex = 0; safeCellIndex < safeCells.length; safeCellIndex++) { final int safeCellRow = safeCells[safeCellIndex][0]; final int safeCellCol = safeCells[safeCellIndex][1]; - if (!myProvider.cells[safeCellRow][safeCellCol].isExplored) { + if (!myProvider.board[safeCellRow][safeCellCol].isExplored) { walkOnCell(myProvider, safeCellRow, safeCellCol); } } @@ -225,11 +225,11 @@ class BoardUtils { } static bool checkGameIsFinished(Data myProvider) { - final List<List<Cell>> cells = myProvider.cells; - final int sizeHorizontal = cells.length; - final int sizeVertical = cells[0].length; + final Board board = myProvider.board; + final int sizeHorizontal = board.length; + final int sizeVertical = board[0].length; - printGrid(cells); + printGrid(board); myProvider.updateGameWin(false); myProvider.updateGameFail(false); @@ -237,7 +237,7 @@ class BoardUtils { for (int row = 0; row < sizeVertical; row++) { for (int col = 0; col < sizeHorizontal; col++) { // Walked on a mine - if (cells[row][col].isExploded == true) { + if (board[row][col].isExploded == true) { myProvider.updateGameFail(true); return true; } @@ -248,9 +248,9 @@ class BoardUtils { for (int col = 0; col < sizeHorizontal; col++) { if ( // Mine not already found - (cells[row][col].isMined == true && cells[row][col].isMarked == false) || + (board[row][col].isMined == true && board[row][col].isMarked == false) || // Safe cell marked as mined - (cells[row][col].isMined == false && cells[row][col].isMarked == true)) { + (board[row][col].isMined == false && board[row][col].isMarked == true)) { return false; } } diff --git a/lib/utils/game_utils.dart b/lib/utils/game_utils.dart index 42bc51a2e3f3d5d6585cde10c68279056b48c419..383a287ce97a37702e808ecc9412a8da134e3b95 100644 --- a/lib/utils/game_utils.dart +++ b/lib/utils/game_utils.dart @@ -33,7 +33,7 @@ class GameUtils { myProvider.setParameterValue('size', savedState['size']); myProvider.setParameterValue('skin', savedState['skin']); - myProvider.updateCells( + myProvider.updateBoard( BoardUtils.createBoardFromSavedState(myProvider, savedState['board'])); myProvider.updateGameIsRunning(true); } catch (e) { diff --git a/pubspec.lock b/pubspec.lock index 6b1fdfef808496ebd4ff8ce5bb9e51bfbc6469e8..2885f1ae8482614c38711b57e048f2bd0e10e178 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: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + url: "https://pub.dev" + source: hosted + version: "2.4.2" 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: f53a110e3b48dcd78136c10daa5d51512443cea5e1348c9d80a320095fa2db9e + url: "https://pub.dev" + source: hosted + version: "8.1.3" 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: c145aeb6584aedc7c862ab8c737c3277788f47488bfdf9bae0fe112bd0a4789c + url: "https://pub.dev" + source: hosted + version: "3.0.5" + 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,6 +102,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_bloc: + dependency: "direct main" + description: + name: flutter_bloc + sha256: "87325da1ac757fcc4813e6b34ed5dd61169973871fdf181d6c2109dd6935ece1" + url: "https://pub.dev" + source: hosted + version: "8.1.4" flutter_lints: dependency: "direct dev" description: @@ -54,11 +118,64 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_swipe: + dependency: "direct main" + description: + name: flutter_swipe + sha256: dc6541bac3a0545ce15a3fa15913f6250532062960bf6b0ad4562d02f14a8545 + url: "https://pub.dev" + source: hosted + version: "1.0.1" 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: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba + url: "https://pub.dev" + source: hosted + version: "1.2.0" + 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: "00a2099680162e74b5a836b8a7f446e478520a9cae9f6032e028ad8129f4432d" + url: "https://pub.dev" + source: hosted + version: "9.1.4" + intl: + dependency: transitive + description: + name: intl + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + url: "https://pub.dev" + source: hosted + version: "0.18.1" lints: dependency: transitive description: @@ -99,14 +216,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" - path: + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: "88bc797f44a94814f2213db1c9bd5badebafdfb8290ca9f78d4b9ee2a3db4d79" + url: "https://pub.dev" + source: hosted + version: "5.0.1" + package_info_plus_platform_interface: dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + path: + dependency: "direct main" description: name: path sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted version: "1.9.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" + url: "https://pub.dev" + source: hosted + version: "2.3.2" path_provider_linux: dependency: transitive description: @@ -199,10 +356,10 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" + sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.2.2" shared_preferences_windows: dependency: transitive description: @@ -216,6 +373,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,10 +433,10 @@ packages: dependency: transitive description: name: web - sha256: "1d9158c616048c38f712a6646e317a3426da10e884447626167240d45209cbad" + sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.4.2" win32: dependency: transitive description: @@ -249,5 +454,5 @@ packages: source: hosted version: "1.0.4" sdks: - dart: ">=3.3.0 <4.0.0" - flutter: ">=3.19.0" + dart: ">=3.3.0-279.1.beta <4.0.0" + flutter: ">=3.16.0" diff --git a/pubspec.yaml b/pubspec.yaml index ca6e37179087ec5f58ecadc55923d48b32665e30..72d953f7bb217acdb47a62d5ecd83fb9aa33a945 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,8 @@ name: minehunter description: A minehunter game application. + publish_to: 'none' -version: 0.1.16+37 +version: 0.1.17+38 environment: sdk: '^3.0.0' @@ -9,9 +10,20 @@ environment: dependencies: flutter: sdk: flutter + + easy_localization: ^3.0.1 + equatable: ^2.0.5 + flutter_bloc: ^8.1.1 + flutter_swipe: ^1.0.1 + hive: ^2.2.3 + hydrated_bloc: ^9.0.0 + overlay_support: ^2.1.0 provider: ^6.0.5 shared_preferences: ^2.2.1 - overlay_support: ^2.1.0 + package_info_plus: ^5.0.1 + path: ^1.9.0 + path_provider: ^2.0.11 + unicons: ^2.1.1 dev_dependencies: flutter_lints: ^3.0.1 @@ -21,3 +33,17 @@ flutter: assets: - assets/icons/ - assets/skins/ + - assets/translations/ + + fonts: + - 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 +