diff --git a/android/app/build.gradle b/android/app/build.gradle index 703e3f5665b3acda38b0bd01bbc10a7a2c94a6be..861e1af4157a0a584205e671f83c482e6ac54fc4 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.puissance4" defaultConfig { diff --git a/android/gradle.properties b/android/gradle.properties index 3e4ee4f7fc752c7bb9d11bf7ffc76f46dd8039d7..bfd8ff8463f540149e419194f582356f7aeabfed 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=1.0.21 -app.versionCode=22 +app.versionName=1.1.0 +app.versionCode=23 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..82d188f9bd55d92364d62f560e2214d7dc6d60d8 --- /dev/null +++ b/assets/translations/en.json @@ -0,0 +1,12 @@ +{ + "app_name": "4 in a row", + + "settings_title": "Settings", + "settings_label_theme": "Theme mode", + + "about_title": "Informations", + "about_content": "4 in a row", + "about_version": "Version: {version}", + + "": "" +} diff --git a/assets/translations/fr.json b/assets/translations/fr.json new file mode 100644 index 0000000000000000000000000000000000000000..472a7d93f1e3017f5df6cd229c5a1f4a8b34ad12 --- /dev/null +++ b/assets/translations/fr.json @@ -0,0 +1,12 @@ +{ + "app_name": "Puissance 4", + + "settings_title": "Réglages", + "settings_label_theme": "Thème de couleurs", + + "about_title": "Informations", + "about_content": "Puissance 4.", + "about_version": "Version : {version}", + + "": "" +} diff --git a/assets/ui/button_back.png b/assets/ui/button_back.png new file mode 100644 index 0000000000000000000000000000000000000000..51d7a01d171f7d7f047ecf9dee2d7ceee23b310d Binary files /dev/null and b/assets/ui/button_back.png differ diff --git a/assets/ui/button_delete_saved_game.png b/assets/ui/button_delete_saved_game.png new file mode 100644 index 0000000000000000000000000000000000000000..4ca5b749c208c4b7eac2a4b141a1bd918d7cb98f 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..2fe433b7d18a39880a14e3f0af18cb75c4ccbaed Binary files /dev/null and b/assets/ui/button_resume_game.png differ diff --git a/assets/ui/button_start.png b/assets/ui/button_start.png new file mode 100644 index 0000000000000000000000000000000000000000..23c7a4f670de19ffac455d6c510c3c53653a048b Binary files /dev/null and b/assets/ui/button_start.png differ diff --git a/assets/ui/game_draw.png b/assets/ui/game_draw.png new file mode 100644 index 0000000000000000000000000000000000000000..047508dc7427561315ec1c3834f25ab888324c06 Binary files /dev/null and b/assets/ui/game_draw.png differ diff --git a/assets/ui/game_fail.png b/assets/ui/game_fail.png new file mode 100644 index 0000000000000000000000000000000000000000..047508dc7427561315ec1c3834f25ab888324c06 Binary files /dev/null and b/assets/ui/game_fail.png differ diff --git a/assets/ui/game_win.png b/assets/ui/game_win.png new file mode 100644 index 0000000000000000000000000000000000000000..876334279c1711b349a62131a33607eecf924eb6 Binary files /dev/null and b/assets/ui/game_win.png differ diff --git a/assets/ui/placeholder.png b/assets/ui/placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..814df31be6ddc4275ebe4490c79365578dbef1f0 Binary files /dev/null and b/assets/ui/placeholder.png differ diff --git a/fastlane/metadata/android/en-US/changelogs/23.txt b/fastlane/metadata/android/en-US/changelogs/23.txt new file mode 100644 index 0000000000000000000000000000000000000000..5482b7fa40ca24a8240ec8e571116215a31f6f61 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/23.txt @@ -0,0 +1 @@ +Clean / improve / update code and UI. diff --git a/fastlane/metadata/android/fr-FR/changelogs/23.txt b/fastlane/metadata/android/fr-FR/changelogs/23.txt new file mode 100644 index 0000000000000000000000000000000000000000..8e88019e5d88c4e6e123ad73c0c1c0dce6e4ff70 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/23.txt @@ -0,0 +1 @@ +Nettoyage / amélioration / mise à jour de code et de l'interface. diff --git a/icons/build_game_icons.sh b/icons/build_game_icons.sh deleted file mode 100755 index 218080d1eb12952690ba1e182a468f6e3cf03d65..0000000000000000000000000000000000000000 --- a/icons/build_game_icons.sh +++ /dev/null @@ -1,98 +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=" -" - -# 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} -} - -function build_settings_icons() { - INPUT_STRING="$1" - - SETTING_NAME="$(echo "${INPUT_STRING}" | cut -d":" -f1)" - SETTING_VALUES="$(echo "${INPUT_STRING}" | cut -d":" -f2 | tr "," " ")" - - for SETTING_VALUE in ${SETTING_VALUES} - do - SETTING_CODE="${SETTING_NAME}_${SETTING_VALUE}" - build_icon ${CURRENT_DIR}/${SETTING_CODE}.svg ${ASSETS_DIR}/icons/${SETTING_CODE}.png - done -} - -####################################################### - -# 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 - -# build settings images -for GAME_SETTING in ${AVAILABLES_GAME_SETTINGS} -do - build_settings_icons "${GAME_SETTING}" -done diff --git a/lib/board.dart b/lib/board.dart deleted file mode 100644 index e7498f80fee7c7455f0b20e64d8d2f1bcc089053..0000000000000000000000000000000000000000 --- a/lib/board.dart +++ /dev/null @@ -1,151 +0,0 @@ -import 'package:puissance4/coordinate.dart'; - -import 'package:puissance4/match_page.dart'; - -class Board { - List<List<Color?>> _boxes = List.generate( - 7, - (i) => List.generate( - 7, - (i) => null, - ), - ); - - Board(); - - Board.from(List<List<Color?>> boxes) { - _boxes = boxes; - } - - Color? getBox(Coordinate coordinate) => _boxes[coordinate.col ?? 0][coordinate.row ?? 0]; - - int getColumnTarget(int? col) => _boxes[col ?? 0].lastIndexOf(null); - - void setBox(Coordinate coordinate, Color? player) => - _boxes[coordinate.col ?? 0][coordinate.row ?? 0] = player; - - bool checkWinner(Coordinate coordinate, Color? player) { - return checkHorizontally(coordinate, player) || - checkVertically(coordinate, player) || - checkDiagonally(coordinate, player); - } - - bool checkHorizontally(Coordinate coordinate, Color? player) { - var r = 0; - for (; - (coordinate.col ?? 0) + r < 7 && - r < 4 && - getBox(coordinate.copyWith(col: (coordinate.col ?? 0) + r)) == player; - ++r) {} - if (r >= 4) { - return true; - } - - var l = 0; - for (; - (coordinate.col ?? 0) - l >= 0 && - l < 4 && - getBox(coordinate.copyWith(col: (coordinate.col ?? 0) - l)) == player; - ++l) {} - if (l >= 4 || l + r >= 5) { - return true; - } - - return false; - } - - bool checkDiagonally(Coordinate coordinate, Color? player) { - var ur = 0; - for (; - (coordinate.col ?? 0) + ur < 7 && - (coordinate.row ?? 0) + ur < 7 && - ur < 4 && - getBox(coordinate.copyWith( - col: (coordinate.col ?? 0) + ur, - row: (coordinate.row ?? 0) + ur, - )) == - player; - ++ur) {} - if (ur >= 4) { - return true; - } - var dl = 0; - for (; - (coordinate.col ?? 0) - dl >= 0 && - (coordinate.row ?? 0) - dl >= 0 && - dl < 4 && - getBox(coordinate.copyWith( - col: (coordinate.col ?? 0) - dl, - row: (coordinate.row ?? 0) - dl, - )) == - player; - ++dl) {} - if (dl >= 4 || dl + ur >= 5) { - return true; - } - - var dr = 0; - for (; - (coordinate.col ?? 0) + dr < 7 && - (coordinate.row ?? 0) - dr >= 0 && - dr < 4 && - getBox(coordinate.copyWith( - col: (coordinate.col ?? 0) + dr, - row: (coordinate.row ?? 0) - dr, - )) == - player; - ++dr) {} - if (dr >= 4) { - return true; - } - - var ul = 0; - for (; - (coordinate.col ?? 0) - ul >= 0 && - (coordinate.row ?? 0) + ul < 7 && - ul < 4 && - getBox(coordinate.copyWith( - col: (coordinate.col ?? 0) - ul, - row: (coordinate.row ?? 0) + ul, - )) == - player; - ++ul) {} - if (ul >= 4 || dr + ul >= 5) { - return true; - } - return false; - } - - bool checkVertically(Coordinate coordinate, Color? player) { - var u = 0; - for (; - (coordinate.row ?? 0) + u < 7 && - u < 4 && - getBox(coordinate.copyWith( - row: (coordinate.row ?? 0) + u, - )) == - player; - ++u) {} - if (u >= 4) { - return true; - } - var d = 0; - for (; - (coordinate.row ?? 0) - d >= 0 && - d < 4 && - getBox(coordinate.copyWith( - row: (coordinate.row ?? 0) - d, - )) == - player; - ++d) {} - if (d >= 4 || d + u >= 5) { - return true; - } - - return false; - } - - Board clone() { - return Board.from(_boxes.map((c) => c.map((b) => b).toList()).toList()); - } -} diff --git a/lib/config/default_game_settings.dart b/lib/config/default_game_settings.dart new file mode 100644 index 0000000000000000000000000000000000000000..63bfd58548fe86abdac1cc6850a5ec631da5d76a --- /dev/null +++ b/lib/config/default_game_settings.dart @@ -0,0 +1,46 @@ +import 'package:puissance4/utils/tools.dart'; + +class DefaultGameSettings { + // available game parameters codes + static const String parameterCodeGameMode = 'gameMode'; + static const String parameterCodeSize = 'size'; + static const List<String> availableParameters = [ + parameterCodeGameMode, + parameterCodeSize, + ]; + + // game mode: available values + static const String gameModeHumanVsHuman = 'human-vs-human'; + static const String gameModeHumanVsRobot = 'human-vs-robot'; + static const String gameModeRobotVsHuman = 'robot-vs-human'; + static const String gameModeRobotVsRobot = 'robot-vs-robot'; + static const List<String> allowedGameModeValues = [ + gameModeHumanVsHuman, + gameModeHumanVsRobot, + gameModeRobotVsHuman, + gameModeRobotVsRobot, + ]; + // game mode: default value + static const String defaultGameModeValue = gameModeHumanVsHuman; + + // size: available values + static const String sizeValueMedium = '7x6'; + static const List<String> allowedSizeValues = [ + sizeValueMedium, + ]; + // size: default value + static const String defaultSizeValue = sizeValueMedium; + + // available values from parameter code + static List<String> getAvailableValues(String parameterCode) { + switch (parameterCode) { + case parameterCodeGameMode: + return DefaultGameSettings.allowedGameModeValues; + case parameterCodeSize: + return DefaultGameSettings.allowedSizeValues; + } + + printlog('Did not find any available value for game parameter "$parameterCode".'); + return []; + } +} diff --git a/lib/config/default_global_settings.dart b/lib/config/default_global_settings.dart new file mode 100644 index 0000000000000000000000000000000000000000..bea2f7b57c479dc143dc521184f9e4285349652d --- /dev/null +++ b/lib/config/default_global_settings.dart @@ -0,0 +1,28 @@ +import 'package:puissance4/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 []; + } +} diff --git a/lib/config/menu.dart b/lib/config/menu.dart new file mode 100644 index 0000000000000000000000000000000000000000..b9acab474c2dca97261b9227b0115b174977e3a5 --- /dev/null +++ b/lib/config/menu.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:unicons/unicons.dart'; + +import 'package:puissance4/ui/screens/page_about.dart'; +import 'package:puissance4/ui/screens/page_game.dart'; +import 'package:puissance4/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/coordinate.dart b/lib/coordinate.dart deleted file mode 100644 index 3d137c74442be8a8baf6d4d9c26e06edb46d6040..0000000000000000000000000000000000000000 --- a/lib/coordinate.dart +++ /dev/null @@ -1,17 +0,0 @@ -class Coordinate { - final int? row, col; - - Coordinate( - this.col, - this.row, - ); - - Coordinate copyWith({ - int? col, - int? row, - }) => - Coordinate( - col ?? this.col, - row ?? this.row, - ); -} diff --git a/lib/cpu.dart b/lib/cpu.dart deleted file mode 100644 index 1f7ed4bf249747530b435220eb60d56f68b562d5..0000000000000000000000000000000000000000 --- a/lib/cpu.dart +++ /dev/null @@ -1,117 +0,0 @@ -import 'dart:math'; - -import 'package:puissance4/board.dart'; -import 'package:puissance4/coordinate.dart'; -import 'package:puissance4/match_page.dart'; - -abstract class Cpu { - final Color color; - final Random _random = Random(DateTime.now().millisecond); - - Cpu(this.color); - - Color get otherPlayer => color == Color.red ? Color.yellow : Color.red; - - Future<int> chooseCol(Board board); -} - -class DumbCpu extends Cpu { - DumbCpu(super.player); - - @override - Color get otherPlayer => color == Color.red ? Color.yellow : Color.red; - - @override - Future<int> chooseCol(Board board) async { - await Future.delayed(Duration(seconds: _random.nextInt(2))); - int col = _random.nextInt(7); - - return col; - } - - @override - String toString() => 'DUMB CPU'; -} - -class HarderCpu extends Cpu { - HarderCpu(super.player); - - @override - Future<int> chooseCol(Board board) async { - final List<double?> scores = List.filled(7, 0.0); - - await Future.delayed(Duration(seconds: 1 + _random.nextInt(2))); - return _compute(board, 0, 1, scores); - } - - int _compute(Board board, int step, int deepness, List<double?> scores) { - for (var i = 0; i < 7; ++i) { - final boardCopy = board.clone(); - - final target = boardCopy.getColumnTarget(i); - if (target == -1) { - scores[i] = null; - continue; - } - - final coordinate = Coordinate(i, target); - - boardCopy.setBox(coordinate, color); - if (boardCopy.checkWinner(coordinate, color)) { - scores[i] = (scores[i] ?? 0) + deepness / (step + 1); - continue; - } - - for (var j = 0; j < 7; ++j) { - final target = boardCopy.getColumnTarget(j); - if (target == -1) { - continue; - } - - final coordinate = Coordinate(j, target); - - boardCopy.setBox(coordinate, otherPlayer); - if (boardCopy.checkWinner(coordinate, otherPlayer)) { - scores[i] = (scores[i] ?? 0) - deepness / (step + 1); - continue; - } - - if (step + 1 < deepness) { - _compute(board, step + 1, deepness, scores); - } - } - } - - return _getBestScoreIndex(scores); - } - - int _getBestScoreIndex(List<double?> scores) { - int bestScoreIndex = scores.indexWhere((s) => s != null); - scores.asMap().forEach((index, score) { - if (score != null && - (score > (scores[bestScoreIndex] ?? 0) || - (score == scores[bestScoreIndex] && _random.nextBool()))) { - bestScoreIndex = index; - } - }); - return bestScoreIndex; - } - - @override - String toString() => 'HARDER CPU'; -} - -class HardestCpu extends HarderCpu { - HardestCpu(super.player); - - @override - Future<int> chooseCol(Board board) async { - final List<double?> scores = List.filled(7, 0); - - await Future.delayed(Duration(seconds: 2 + _random.nextInt(2))); - return _compute(board, 0, 4, scores); - } - - @override - String toString() => 'HARDEST CPU'; -} diff --git a/lib/cpu_level_page.dart b/lib/cpu_level_page.dart deleted file mode 100644 index 21074f4934db9024b041a9d8dd44f7558be3c273..0000000000000000000000000000000000000000 --- a/lib/cpu_level_page.dart +++ /dev/null @@ -1,92 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; - -import 'package:puissance4/cpu.dart'; -import 'package:puissance4/match_page.dart'; - -class CpuLevelPage extends StatelessWidget { - const CpuLevelPage({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - elevation: 0, - ), - backgroundColor: Colors.blue, - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - children: <Widget>[ - TextButton( - style: TextButton.styleFrom( - backgroundColor: Colors.yellow, - padding: const EdgeInsets.all(15), - ), - child: Text( - '☺️ FACILE', - style: - Theme.of(context).textTheme.headlineMedium?.copyWith(color: Colors.black), - ), - onPressed: () { - Navigator.pushNamed( - context, - '/match', - arguments: { - 'mode': Mode.pvc, - 'cpu': DumbCpu(Random().nextBool() ? Color.red : Color.yellow), - }, - ); - }, - ), - TextButton( - style: TextButton.styleFrom( - backgroundColor: Colors.red, - padding: const EdgeInsets.all(15), - ), - child: Text( - '🤔 DIFFICILE', - style: - Theme.of(context).textTheme.headlineMedium?.copyWith(color: Colors.white), - ), - onPressed: () { - Navigator.pushNamed( - context, - '/match', - arguments: { - 'mode': Mode.pvc, - 'cpu': HarderCpu(Random().nextBool() ? Color.red : Color.yellow), - }, - ); - }, - ), - TextButton( - style: TextButton.styleFrom( - backgroundColor: Colors.deepPurpleAccent, - padding: const EdgeInsets.all(15), - ), - child: Text( - '🤯 TRES DIFFICILE', - style: - Theme.of(context).textTheme.headlineMedium?.copyWith(color: Colors.white), - ), - onPressed: () { - Navigator.pushNamed( - context, - '/match', - arguments: { - 'mode': Mode.pvc, - 'cpu': HardestCpu(Random().nextBool() ? Color.red : Color.yellow), - }, - ); - }, - ), - ], - ), - ), - ); - } -} diff --git a/lib/cubit/game_cubit.dart b/lib/cubit/game_cubit.dart new file mode 100644 index 0000000000000000000000000000000000000000..7a08295655a55cd0bd63ac0905bf61cbaa0cd374 --- /dev/null +++ b/lib/cubit/game_cubit.dart @@ -0,0 +1,177 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; + +import 'package:puissance4/models/game/game.dart'; +import 'package:puissance4/models/settings/settings_game.dart'; +import 'package:puissance4/models/settings/settings_global.dart'; +import 'package:puissance4/robot/robot_player.dart'; +import 'package:puissance4/utils/tools.dart'; + +part 'game_state.dart'; + +class GameCubit extends HydratedCubit<GameState> { + GameCubit() + : super(GameState( + currentGame: Game.createNull(), + )); + + 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 + board: state.currentGame.board, + sizeHorizontal: state.currentGame.sizeHorizontal, + sizeVertical: state.currentGame.sizeVertical, + // 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); + refresh(); + + robotPlay(); + } + + 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.currentPlayer = 3 - state.currentGame.currentPlayer; + refresh(); + + robotPlay(); + } + + void robotPlay() async { + if (!state.currentGame.isFinished && !state.currentGame.isCurrentPlayerHuman()) { + final int? pickedCell = RobotPlayer.pickColumn(state.currentGame); + await Future.delayed(const Duration(milliseconds: 500)); + + if (pickedCell != null) { + tapOnColumn(col: pickedCell); + } + } + } + + void tapOnColumn({required int col}) async { + printlog('tap on column: $col'); + + if (!state.currentGame.isMoveAllowed(col: col)) { + printlog('not allowed'); + return; + } + + state.currentGame.animationInProgress = true; + refresh(); + + await dropCoin(col: col); + + if (state.currentGame.connected4()) { + printlog('user connected 4'); + state.currentGame.isFinished = true; + refresh(); + } + if (!state.currentGame.canPlay()) { + printlog('user has no more move to play'); + state.currentGame.isFinished = true; + refresh(); + } + + state.currentGame.animationInProgress = false; + if (state.currentGame.isFinished == false) { + toggleCurrentPlayer(); + } + + refresh(); + } + + // put coin on top cell + // and make it fall until bottom or other coin + Future<void> dropCoin({required int col}) async { + int currentRow = 0; + + do { + if (currentRow > 0) { + state.currentGame.board.setCellValue( + row: currentRow - 1, + col: col, + value: 0, + ); + } + state.currentGame.board.setCellValue( + row: currentRow++, + col: col, + value: state.currentGame.currentPlayer, + ); + refresh(); + + await Future.delayed(const Duration(milliseconds: 50)); + } while (currentRow < state.currentGame.sizeVertical && + state.currentGame.board.getCellValue( + row: currentRow, + col: col, + ) == + 0); + + refresh(); + } + + @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..89b3365ea089cfdf984ee0650818dad72d788f59 --- /dev/null +++ b/lib/cubit/nav_cubit.dart @@ -0,0 +1,37 @@ +import 'package:hydrated_bloc/hydrated_bloc.dart'; + +import 'package:puissance4/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..54fdfe01c809f523ebd262933c773214f29e51a1 --- /dev/null +++ b/lib/cubit/settings_game_cubit.dart @@ -0,0 +1,72 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; + +import 'package:puissance4/config/default_game_settings.dart'; +import 'package:puissance4/models/settings/settings_game.dart'; + +part 'settings_game_state.dart'; + +class GameSettingsCubit extends HydratedCubit<GameSettingsState> { + GameSettingsCubit() : super(GameSettingsState(settings: GameSettings.createDefault())); + + void setValues({ + String? gameMode, + String? size, + }) { + emit( + GameSettingsState( + settings: GameSettings( + gameMode: gameMode ?? state.settings.gameMode, + size: size ?? state.settings.size, + ), + ), + ); + } + + String getParameterValue(String code) { + switch (code) { + case DefaultGameSettings.parameterCodeGameMode: + return GameSettings.getGameModeValueFromUnsafe(state.settings.gameMode); + case DefaultGameSettings.parameterCodeSize: + return GameSettings.getSizeValueFromUnsafe(state.settings.size); + } + + return ''; + } + + void setParameterValue(String code, String value) { + final String gameMode = code == DefaultGameSettings.parameterCodeGameMode + ? value + : getParameterValue(DefaultGameSettings.parameterCodeGameMode); + final String size = code == DefaultGameSettings.parameterCodeSize + ? value + : getParameterValue(DefaultGameSettings.parameterCodeSize); + + setValues( + gameMode: gameMode, + size: size, + ); + } + + @override + GameSettingsState? fromJson(Map<String, dynamic> json) { + final String gameMode = json[DefaultGameSettings.parameterCodeGameMode] as String; + final String size = json[DefaultGameSettings.parameterCodeSize] as String; + + return GameSettingsState( + settings: GameSettings( + gameMode: gameMode, + size: size, + ), + ); + } + + @override + Map<String, dynamic>? toJson(GameSettingsState state) { + return <String, dynamic>{ + DefaultGameSettings.parameterCodeGameMode: state.settings.gameMode, + DefaultGameSettings.parameterCodeSize: state.settings.size, + }; + } +} 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..47bb401c83ef145baecb90f4c95d29e0d38f2219 --- /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:puissance4/config/default_global_settings.dart'; +import 'package:puissance4/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/game_chip.dart b/lib/game_chip.dart deleted file mode 100644 index 22d94167f72cfd2aa493765d985df3cab5525678..0000000000000000000000000000000000000000 --- a/lib/game_chip.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:puissance4/match_page.dart'; - -class GameChip extends StatelessWidget { - const GameChip({ - super.key, - this.translation, - this.color, - }); - - final Animation<double>? translation; - final Color? color; - - @override - Widget build(BuildContext context) { - return Center( - child: Container( - transform: Matrix4.translationValues( - 0, - ((translation?.value ?? 1) * 400) - 400, - 0, - ), - height: 40, - width: 40, - child: Material( - shape: const CircleBorder(), - color: color == Color.red ? Colors.red : Colors.yellow, - ), - ), - ); - } -} diff --git a/lib/hole_painter.dart b/lib/hole_painter.dart deleted file mode 100644 index ebabafb557ea9d066a22ec3702b16a10e3acab77..0000000000000000000000000000000000000000 --- a/lib/hole_painter.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; - -class HolePainter extends CustomPainter { - @override - void paint(Canvas canvas, Size size) { - final paint = Paint(); - paint.color = Colors.blue; - paint.style = PaintingStyle.fill; - - final center = Offset(size.height / 2, size.width / 2); - - final circleBounds = Rect.fromCircle(center: center, radius: 20); - - final topPath = Path() - ..moveTo(-1, -1) - ..lineTo(-1, (size.height / 2) + 1) - ..arcTo(circleBounds, -pi, pi, false) - ..lineTo(size.width + 1, (size.height / 2) + 1) - ..lineTo(size.width + 1, -1) - ..close(); - final bottomPath = Path() - ..moveTo(-1, size.height) - ..lineTo(-1, (size.height / 2) - 1) - ..arcTo(circleBounds, pi, -pi, false) - ..lineTo(size.width + 1, (size.height / 2) - 1) - ..lineTo(size.width + 1, size.height + 1) - ..close(); - - canvas.drawPath(topPath, paint); - canvas.drawPath(bottomPath, paint); - } - - @override - bool shouldRepaint(CustomPainter oldDelegate) { - return false; - } -} diff --git a/lib/home_page.dart b/lib/home_page.dart deleted file mode 100644 index ab7d3253c02d22d9abdc6bb3fa8e5db5714a187f..0000000000000000000000000000000000000000 --- a/lib/home_page.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; - -import 'package:puissance4/cpu.dart'; -import 'package:puissance4/match_page.dart'; - -class HomePage extends StatelessWidget { - const HomePage({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.blue, - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - children: <Widget>[ - TextButton( - style: TextButton.styleFrom( - backgroundColor: Colors.green, - padding: const EdgeInsets.all(15), - ), - child: Text( - '🧑 2 JOUEURS 🧑', - style: - Theme.of(context).textTheme.headlineMedium?.copyWith(color: Colors.white), - ), - onPressed: () { - Navigator.pushNamed( - context, - '/match', - arguments: { - 'mode': Mode.pvp, - }, - ); - }, - ), - TextButton( - style: TextButton.styleFrom( - backgroundColor: Colors.orange, - padding: const EdgeInsets.all(15), - ), - child: Text( - '🧑 1 JOUEUR 🤖', - style: - Theme.of(context).textTheme.headlineMedium?.copyWith(color: Colors.white), - ), - onPressed: () { - Navigator.pushNamed( - context, - '/cpu-level', - arguments: { - 'mode': Mode.pvc, - }, - ); - }, - ), - TextButton( - style: TextButton.styleFrom( - backgroundColor: Colors.white, - padding: const EdgeInsets.all(15), - ), - child: Text( - '🤖 DEMO 🤖', - style: - Theme.of(context).textTheme.headlineMedium?.copyWith(color: Colors.black), - ), - onPressed: () { - final harderCpu = HarderCpu(Random().nextBool() ? Color.red : Color.yellow); - Navigator.pushNamed( - context, - '/match', - arguments: { - 'mode': Mode.demo, - 'cpu': harderCpu, - 'cpu2': HardestCpu(harderCpu.otherPlayer), - }, - ); - }, - ), - ], - ), - ), - ); - } -} diff --git a/lib/main.dart b/lib/main.dart index 8a2676ab28835640daa254bec4580c814cb69636..1ced97dba6ad18a34de0d9e79bebd65fb7011462 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,14 +1,42 @@ +import 'dart:io'; + +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.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:puissance4/cpu_level_page.dart'; -import 'package:puissance4/home_page.dart'; -import 'package:puissance4/match_page.dart'; +import 'package:puissance4/config/theme.dart'; +import 'package:puissance4/cubit/game_cubit.dart'; +import 'package:puissance4/cubit/nav_cubit.dart'; +import 'package:puissance4/cubit/settings_game_cubit.dart'; +import 'package:puissance4/cubit/settings_global_cubit.dart'; +import 'package:puissance4/cubit/theme_cubit.dart'; +import 'package:puissance4/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 { @@ -16,30 +44,58 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( - title: 'Puissance 4', - theme: ThemeData( - primarySwatch: Colors.blue, - ), - home: const HomePage(), - onGenerateRoute: (settings) { - final args = settings.arguments as Map<String, dynamic>; - if (settings.name == '/match') { - return MaterialPageRoute( - builder: (context) => MatchPage( - mode: args['mode'], - cpu: args['cpu'], - cpu2: args['cpu2'], - ), - ); - } else if (settings.name == '/cpu-level') { - return MaterialPageRoute( - builder: (context) => const CpuLevelPage(), - ); - } + 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: 'Puissance4', + home: const SkeletonScreen(), + + // Theme stuff + theme: lightTheme, + darkTheme: darkTheme, + themeMode: state.themeMode, - return null; - }, + // Localization stuff + localizationsDelegates: context.localizationDelegates, + supportedLocales: context.supportedLocales, + locale: context.locale, + debugShowCheckedModeBanner: false, + ); + }, + ), ); } + + List<String> getImagesAssets() { + final List<String> assets = []; + + const List<String> gameImages = [ + 'button_back', + 'button_delete_saved_game', + 'button_resume_game', + 'button_start', + 'game_fail', + 'game_win', + 'placeholder', + ]; + + for (String image in gameImages) { + assets.add('assets/ui/$image.png'); + } + + return assets; + } } diff --git a/lib/match_page.dart b/lib/match_page.dart deleted file mode 100644 index b9a98bd838c379e7becc3ac925b821e5838c7abe..0000000000000000000000000000000000000000 --- a/lib/match_page.dart +++ /dev/null @@ -1,301 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; - -import 'package:puissance4/board.dart'; -import 'package:puissance4/coordinate.dart'; -import 'package:puissance4/cpu.dart'; -import 'package:puissance4/game_chip.dart'; -import 'package:puissance4/hole_painter.dart'; - -enum Color { - yellow, - red, -} - -enum Mode { - pvp, - pvc, - demo, -} - -class MatchPage extends StatefulWidget { - final Mode mode; - final Cpu? cpu; - final Cpu? cpu2; - - const MatchPage({ - super.key, - required this.mode, - required this.cpu, - required this.cpu2, - }); - - @override - MatchPageState createState() => MatchPageState(); -} - -class MatchPageState extends State<MatchPage> with TickerProviderStateMixin { - final board = Board(); - Color? turn; - Color? winner; - - List<List<Animation<double>?>> translations = List.generate( - 7, - (i) => List.generate( - 7, - (i) => null, - ), - ); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - elevation: 0, - ), - backgroundColor: Colors.blue, - body: Padding( - padding: const EdgeInsets.all(16.0), - child: Flex( - direction: Axis.vertical, - mainAxisSize: MainAxisSize.max, - children: <Widget>[ - Flexible( - flex: 2, - child: Container( - constraints: BoxConstraints.loose( - const Size( - 500, - 532, - ), - ), - child: Padding( - padding: const EdgeInsets.only(top: 32.0), - child: Stack( - fit: StackFit.loose, - children: <Widget>[ - Positioned.fill( - child: Container( - color: Colors.white, - ), - ), - buildPieces(), - buildBoard(), - ], - ), - ), - ), - ), - Flexible( - flex: 1, - child: Padding( - padding: const EdgeInsets.all(32.0), - child: winner != null - ? Text( - '${winner == Color.red ? 'RED' : 'YELLOW'} WINS', - textAlign: TextAlign.center, - style: Theme.of(context) - .textTheme - .titleLarge - ?.copyWith(color: Colors.white), - ) - : Column( - children: <Widget>[ - Text( - '${turn == Color.red ? 'RED' : 'YELLOW'} SPEAKS', - textAlign: TextAlign.center, - style: Theme.of(context) - .textTheme - .headlineSmall - ?.copyWith(color: Colors.white), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: GameChip( - color: turn, - ), - ), - _buildPlayerName(context), - ], - ), - ), - ), - ], - ), - ), - ); - } - - Text _buildPlayerName(BuildContext context) { - String name; - - if (widget.mode == Mode.pvc) { - if (turn == widget.cpu?.color) { - name = 'CPU - ${widget.cpu.toString()}'; - } else { - name = 'USER'; - } - } else if (widget.mode == Mode.pvp) { - if (turn == Color.red) { - name = 'PLAYER1'; - } else { - name = 'PLAYER2'; - } - } else { - if (turn == widget.cpu?.color) { - name = 'CPU1 - ${widget.cpu.toString()}'; - } else { - name = 'CPU2 - ${widget.cpu2.toString()}'; - } - } - return Text( - name, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.headlineSmall?.copyWith(color: Colors.white), - ); - } - - @override - void initState() { - super.initState(); - turn = Random().nextBool() ? Color.red : Color.yellow; - if (widget.mode == Mode.pvc && turn == widget.cpu?.color) { - cpuMove(widget.cpu); - } else if (widget.mode == Mode.demo) { - if (turn == widget.cpu?.color) { - cpuMove(widget.cpu); - } else { - cpuMove(widget.cpu2); - } - } - } - - GridView buildPieces() { - return GridView.custom( - padding: const EdgeInsets.all(0), - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 7), - childrenDelegate: SliverChildBuilderDelegate( - (context, i) { - final col = i % 7; - final row = i ~/ 7; - - if (board.getBox(Coordinate(col, row)) == null) { - return const SizedBox(); - } - - return GameChip( - translation: translations[col][row], - color: board.getBox(Coordinate(col, row)), - ); - }, - childCount: 49, - ), - ); - } - - GridView buildBoard() { - return GridView.custom( - padding: const EdgeInsets.all(0), - physics: const NeverScrollableScrollPhysics(), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 7), - shrinkWrap: true, - childrenDelegate: SliverChildBuilderDelegate( - (context, i) { - final col = i % 7; - - return GestureDetector( - onTap: () { - if (winner == null) { - userMove(col); - } - }, - child: CustomPaint( - size: const Size(50, 50), - willChange: false, - painter: HolePainter(), - ), - ); - }, - childCount: 49, - ), - ); - } - - void userMove(int col) { - putChip(col); - if (winner == null && widget.mode == Mode.pvc) { - cpuMove(widget.cpu); - } - } - - void cpuMove(Cpu? cpu) async { - int? col = await cpu?.chooseCol(board); - putChip(col); - - if (winner == null && widget.mode == Mode.demo) { - if (turn == widget.cpu?.color) { - cpuMove(widget.cpu); - } else { - cpuMove(widget.cpu2); - } - } - } - - void putChip(int? col) { - final target = board.getColumnTarget(col); - final player = turn; - - if (target == -1) { - return; - } - - final controller = AnimationController( - vsync: this, - duration: const Duration(seconds: 1), - )..addListener(() { - if (mounted) { - setState(() {}); - } - }); - - if (mounted) { - setState(() { - board.setBox(Coordinate(col, target), turn); - turn = turn == Color.red ? Color.yellow : Color.red; - }); - } - - translations[col ?? 0][target] = Tween( - begin: 0.0, - end: 1.0, - ).animate(CurvedAnimation( - curve: Curves.bounceOut, - parent: controller, - )) - ..addStatusListener((status) { - if (status == AnimationStatus.completed) { - controller.dispose(); - } - }); - - controller.forward().orCancel; - - if (board.checkWinner(Coordinate(col, target), player)) { - showWinnerDialog(context, player); - } - } - - void showWinnerDialog(BuildContext context, Color? player) { - setState(() { - winner = player; - }); - - Future.delayed( - const Duration(seconds: 5), - () => mounted ? Navigator.popUntil(context, (r) => r.isFirst) : null, - ); - } -} diff --git a/lib/models/game/board.dart b/lib/models/game/board.dart new file mode 100644 index 0000000000000000000000000000000000000000..335568aaa71d43319188230be88020f9328ffaaf --- /dev/null +++ b/lib/models/game/board.dart @@ -0,0 +1,62 @@ +import 'package:puissance4/utils/tools.dart'; + +typedef BoardCells = List<List<int>>; + +class Board { + Board({ + required this.cells, + }); + + BoardCells cells = const []; + + factory Board.createNull() { + return Board( + cells: [], + ); + } + + factory Board.createNew({ + required BoardCells cells, + }) { + return Board( + cells: cells, + ); + } + + int getCellValue({required int row, required int col}) { + return cells[row][col]; + } + + void setCellValue({required int row, required int col, required int value}) { + cells[row][col] = value; + } + + void dump() { + printlog(''); + String line = '--'; + for (int i = 0; i < cells[0].length; i++) { + line += '-'; + } + printlog(line); + for (int rowIndex = 0; rowIndex < cells.length; rowIndex++) { + String currentLine = ''; + for (int colIndex = 0; colIndex < cells[rowIndex].length; colIndex++) { + currentLine += getCellValue(row: rowIndex, col: colIndex).toString(); + } + printlog('|$currentLine|'); + } + printlog(line); + printlog(''); + } + + @override + String toString() { + return '$Board(${toJson()})'; + } + + Map<String, dynamic>? toJson() { + return <String, dynamic>{ + 'cells': cells, + }; + } +} diff --git a/lib/models/game/game.dart b/lib/models/game/game.dart new file mode 100644 index 0000000000000000000000000000000000000000..2d60cf380ef789be83b64bdf2b570dbcf0ab14d8 --- /dev/null +++ b/lib/models/game/game.dart @@ -0,0 +1,226 @@ +import 'package:puissance4/config/default_game_settings.dart'; +import 'package:puissance4/models/game/board.dart'; +import 'package:puissance4/models/settings/settings_game.dart'; +import 'package:puissance4/models/settings/settings_global.dart'; +import 'package:puissance4/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 + required this.board, + required this.sizeHorizontal, + required this.sizeVertical, + + // Game data + required this.currentPlayer, + }); + + // Settings + final GameSettings gameSettings; + final GlobalSettings globalSettings; + + // State + bool isRunning; + bool isStarted; + bool isFinished; + bool animationInProgress; + + // Base data + Board board; + final int sizeHorizontal; + final int sizeVertical; + + // Game data + int currentPlayer; + + factory Game.createNull() { + return Game( + // Settings + gameSettings: GameSettings.createDefault(), + globalSettings: GlobalSettings.createDefault(), + // Base data + board: Board.createNull(), + sizeHorizontal: 0, + sizeVertical: 0, + // Game data + currentPlayer: 0, + ); + } + + factory Game.createNew({ + GameSettings? gameSettings, + GlobalSettings? globalSettings, + }) { + final GameSettings newGameSettings = gameSettings ?? GameSettings.createDefault(); + final GlobalSettings newGlobalSettings = globalSettings ?? GlobalSettings.createDefault(); + + final int sizeHorizontal = int.parse(newGameSettings.size.split('x')[0]); + final int sizeVertical = int.parse(newGameSettings.size.split('x')[1]); + + // Create empty board (it will be mined after first hit) + final BoardCells board = []; + for (int rowIndex = 0; rowIndex < sizeVertical; rowIndex++) { + final List<int> row = []; + for (int colIndex = 0; colIndex < sizeHorizontal; colIndex++) { + row.add(0); + } + board.add(row); + } + + return Game( + // Settings + gameSettings: newGameSettings, + globalSettings: newGlobalSettings, + // State + isRunning: true, + // Base data + board: Board.createNew(cells: board), + sizeHorizontal: sizeHorizontal, + sizeVertical: sizeVertical, + // Game data + currentPlayer: 1, + ); + } + + bool get canBeResumed => isStarted && !isFinished; + + // Ensure move is allowed + bool isMoveAllowed({required int col}) { + // ensure first row is empty on selected col + return board.getCellValue(row: 0, col: col) == 0; + } + + bool connected4() { + // vertical + for (int row = 0; row < sizeVertical - 3; row++) { + for (int col = 0; col < sizeHorizontal; col++) { + if ((board.getCellValue(row: row, col: col) == currentPlayer) && + (board.getCellValue(row: row + 1, col: col) == currentPlayer) && + (board.getCellValue(row: row + 2, col: col) == currentPlayer) && + (board.getCellValue(row: row + 3, col: col) == currentPlayer)) { + return true; + } + } + } + + // horizontal + for (int row = 0; row < sizeVertical; row++) { + for (int col = 0; col < sizeHorizontal - 3; col++) { + if ((board.getCellValue(row: row, col: col) == currentPlayer) && + (board.getCellValue(row: row, col: col + 1) == currentPlayer) && + (board.getCellValue(row: row, col: col + 2) == currentPlayer) && + (board.getCellValue(row: row, col: col + 3) == currentPlayer)) { + return true; + } + } + } + + // diagonal down + for (int row = 0; row < sizeVertical - 3; row++) { + for (int col = 0; col < sizeHorizontal - 3; col++) { + if ((board.getCellValue(row: row, col: col) == currentPlayer) && + (board.getCellValue(row: row + 1, col: col + 1) == currentPlayer) && + (board.getCellValue(row: row + 2, col: col + 2) == currentPlayer) && + (board.getCellValue(row: row + 3, col: col + 3) == currentPlayer)) { + return true; + } + } + } + + // diagonal up + for (int row = 0; row < sizeVertical - 3; row++) { + for (int col = 0; col < sizeHorizontal - 3; col++) { + if ((board.getCellValue(row: row + 3, col: col) == currentPlayer) && + (board.getCellValue(row: row + 2, col: col + 1) == currentPlayer) && + (board.getCellValue(row: row + 1, col: col + 2) == currentPlayer) && + (board.getCellValue(row: row, col: col + 3) == currentPlayer)) { + return true; + } + } + } + + return false; + } + + bool canPlay() { + List<int> allowedColumns = []; + + for (int columnIndex = 0; columnIndex < sizeHorizontal; columnIndex++) { + if (isMoveAllowed(col: columnIndex)) { + allowedColumns.add(columnIndex); + } + } + + return allowedColumns.isNotEmpty; + } + + bool isPlayerHuman(int playerIndex) { + switch (gameSettings.gameMode) { + case DefaultGameSettings.gameModeHumanVsHuman: + return true; + case DefaultGameSettings.gameModeHumanVsRobot: + return (playerIndex == 1); + case DefaultGameSettings.gameModeRobotVsHuman: + return (playerIndex == 2); + case DefaultGameSettings.gameModeRobotVsRobot: + return false; + default: + } + return true; + } + + bool isCurrentPlayerHuman() { + return isPlayerHuman(currentPlayer); + } + + 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'); + board.dump(); + 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 + 'board': board.toJson(), + // Game data + 'currentPlayer': currentPlayer, + }; + } +} diff --git a/lib/models/settings/settings_game.dart b/lib/models/settings/settings_game.dart new file mode 100644 index 0000000000000000000000000000000000000000..42a2a2e4b5ea3fb8dffe97742051379601d58980 --- /dev/null +++ b/lib/models/settings/settings_game.dart @@ -0,0 +1,54 @@ +import 'package:puissance4/config/default_game_settings.dart'; +import 'package:puissance4/utils/tools.dart'; + +class GameSettings { + final String gameMode; + final String size; + + GameSettings({ + required this.gameMode, + required this.size, + }); + + static String getGameModeValueFromUnsafe(String gameMode) { + if (DefaultGameSettings.allowedGameModeValues.contains(gameMode)) { + return gameMode; + } + + return DefaultGameSettings.defaultGameModeValue; + } + + static String getSizeValueFromUnsafe(String size) { + if (DefaultGameSettings.allowedSizeValues.contains(size)) { + return size; + } + + return DefaultGameSettings.defaultSizeValue; + } + + factory GameSettings.createDefault() { + return GameSettings( + gameMode: DefaultGameSettings.defaultGameModeValue, + size: DefaultGameSettings.defaultSizeValue, + ); + } + + void dump() { + printlog('$GameSettings:'); + printlog(' ${DefaultGameSettings.parameterCodeGameMode}: $gameMode'); + printlog(' ${DefaultGameSettings.parameterCodeSize}: $size'); + printlog(''); + } + + @override + String toString() { + return '$GameSettings(${toJson()})'; + } + + Map<String, dynamic>? toJson() { + return <String, dynamic>{ + DefaultGameSettings.parameterCodeGameMode: gameMode, + DefaultGameSettings.parameterCodeSize: size, + }; + } +} diff --git a/lib/models/settings/settings_global.dart b/lib/models/settings/settings_global.dart new file mode 100644 index 0000000000000000000000000000000000000000..cd6b2484aa263c3481a625a034bbebdd7423ad5a --- /dev/null +++ b/lib/models/settings/settings_global.dart @@ -0,0 +1,41 @@ +import 'package:puissance4/config/default_global_settings.dart'; +import 'package:puissance4/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/player.dart b/lib/player.dart deleted file mode 100644 index f404ecec61663e88164befae628885101445b222..0000000000000000000000000000000000000000 --- a/lib/player.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:puissance4/match_page.dart'; - -abstract class Player { - final Color player; - - Player(this.player); -} diff --git a/lib/robot/robot_player.dart b/lib/robot/robot_player.dart new file mode 100644 index 0000000000000000000000000000000000000000..89fdc61e42d61ea7a10b941d5d057dbeb3615a3f --- /dev/null +++ b/lib/robot/robot_player.dart @@ -0,0 +1,23 @@ +import 'package:puissance4/models/game/game.dart'; + +class RobotPlayer { + static int? pickColumn(Game currentGame) { + List<int> allowedColumns = []; + + for (int columnIndex = 0; columnIndex < currentGame.sizeHorizontal; columnIndex++) { + if (currentGame.isMoveAllowed(col: columnIndex)) { + allowedColumns.add(columnIndex); + } + } + + if (allowedColumns.isEmpty) { + return null; + } + + allowedColumns.shuffle(); + + final int pickedColumn = allowedColumns[0]; + + return pickedColumn; + } +} 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/styled_button.dart b/lib/ui/helpers/styled_button.dart new file mode 100644 index 0000000000000000000000000000000000000000..3dfe5c8a35d94746458e94e14cdf7bec9f835941 --- /dev/null +++ b/lib/ui/helpers/styled_button.dart @@ -0,0 +1,210 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.dart'; + +import 'package:puissance4/utils/color_extensions.dart'; + +class StyledButton extends StatelessWidget { + const StyledButton({ + super.key, + required this.color, + required this.onPressed, + this.onLongPress, + required this.child, + }); + + final Color color; + final VoidCallback? onPressed; + final VoidCallback? onLongPress; + final Widget child; + + factory StyledButton.text({ + Key? key, + required VoidCallback? onPressed, + VoidCallback? onLongPress, + required String caption, + required Color color, + }) { + final Widget captionWidget = AutoSizeText( + caption, + maxLines: 1, + style: TextStyle( + inherit: true, + fontWeight: FontWeight.w900, + color: color.darken(60), + shadows: [ + Shadow( + blurRadius: 5.0, + color: color.lighten(60), + offset: const Offset(2, 2), + ), + Shadow( + blurRadius: 5.0, + color: color.lighten(60), + offset: const Offset(2, -2), + ), + Shadow( + blurRadius: 5.0, + color: color.lighten(60), + offset: const Offset(-2, 2), + ), + Shadow( + blurRadius: 5.0, + color: color.lighten(60), + offset: const Offset(-2, -2), + ), + ], + ), + ); + + return StyledButton( + color: color, + onPressed: onPressed, + onLongPress: onLongPress, + child: captionWidget, + ); + } + + factory StyledButton.icon({ + Key? key, + required VoidCallback? onPressed, + VoidCallback? onLongPress, + required Icon icon, + required Color color, + required double iconSize, + }) { + return StyledButton( + color: color, + onPressed: onPressed, + onLongPress: onLongPress, + child: Icon( + icon.icon, + color: icon.color ?? color.darken(60), + size: iconSize, + shadows: [ + Shadow( + blurRadius: 5.0, + color: color.lighten(60), + offset: const Offset(2, 2), + ), + Shadow( + blurRadius: 5.0, + color: color.lighten(60), + offset: const Offset(2, -2), + ), + Shadow( + blurRadius: 5.0, + color: color.lighten(60), + offset: const Offset(-2, 2), + ), + Shadow( + blurRadius: 5.0, + color: color.lighten(60), + offset: const Offset(-2, -2), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + const double borderWidth = 4; + final Color borderColor = color.darken(40); + const double borderRadius = 10; + + return Container( + margin: const EdgeInsets.all(2), + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + color: color, + border: Border.all( + color: borderColor, + width: borderWidth, + ), + borderRadius: BorderRadius.circular(borderRadius), + ), + child: CustomPaint( + painter: StyledButtonPainter( + baseColor: color, + ), + child: MaterialButton( + onPressed: onPressed, + onLongPress: onLongPress, + padding: const EdgeInsets.all(8), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + minWidth: 40, + child: child, + ), + ), + ); + } +} + +class StyledButtonPainter extends CustomPainter { + StyledButtonPainter({ + required this.baseColor, + }); + + final Color baseColor; + + @override + void paint(Canvas canvas, Size size) { + final Color lightColor = baseColor.lighten(20); + final Color darkColor = baseColor.darken(20); + + final Paint paint = Paint()..style = PaintingStyle.fill; + + const double cornerRadius = 6; + + Path topPath = Path() + ..moveTo(cornerRadius, 0) + ..lineTo(size.width - cornerRadius, 0) + ..arcToPoint( + Offset(size.width, cornerRadius), + radius: const Radius.circular(cornerRadius), + ) + ..lineTo(size.width, size.height * .35) + ..quadraticBezierTo( + size.width * .4, + size.height * .1, + 0, + size.height * .3, + ) + ..lineTo(0, cornerRadius) + ..arcToPoint( + const Offset(cornerRadius, 0), + radius: const Radius.circular(cornerRadius), + ); + + Path bottomPath = Path() + ..moveTo(cornerRadius, size.height) + ..lineTo(size.width - cornerRadius, size.height) + ..arcToPoint( + Offset(size.width, size.height - cornerRadius), + radius: const Radius.circular(cornerRadius), + clockwise: false, + ) + ..lineTo(size.width, size.height * .7) + ..quadraticBezierTo( + size.width * .6, + size.height * .9, + 0, + size.height * .7, + ) + ..lineTo(0, size.height - cornerRadius) + ..arcToPoint( + Offset(cornerRadius, size.height), + radius: const Radius.circular(cornerRadius), + clockwise: false, + ); + + paint.color = lightColor; + canvas.drawPath(topPath, paint); + + paint.color = darkColor; + canvas.drawPath(bottomPath, paint); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} diff --git a/lib/ui/layouts/game_layout.dart b/lib/ui/layouts/game_layout.dart new file mode 100644 index 0000000000000000000000000000000000000000..10d6660c07649787a68495dafe96dd5b72ea1643 --- /dev/null +++ b/lib/ui/layouts/game_layout.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:puissance4/cubit/game_cubit.dart'; +import 'package:puissance4/models/game/game.dart'; +import 'package:puissance4/ui/widgets/game/game_board.dart'; +import 'package:puissance4/ui/widgets/game/game_bottom.dart'; +import 'package:puissance4/ui/widgets/game/game_end.dart'; + +class GameLayout extends StatelessWidget { + const GameLayout({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder<GameCubit, GameState>( + builder: (BuildContext context, GameState gameState) { + final Game currentGame = gameState.currentGame; + + return Container( + alignment: AlignmentDirectional.topCenter, + padding: const EdgeInsets.all(4), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 48), + const GameBoardWidget(), + const SizedBox(height: 16), + const GameBottomWidget(), + const Expanded(child: SizedBox.shrink()), + currentGame.isFinished ? const GameEndWidget() : const SizedBox.shrink(), + ], + ), + ); + }, + ); + } +} diff --git a/lib/ui/layouts/parameters_layout.dart b/lib/ui/layouts/parameters_layout.dart new file mode 100644 index 0000000000000000000000000000000000000000..7d3be8cb95dbc129a0551fbaab03ef911133f0bf --- /dev/null +++ b/lib/ui/layouts/parameters_layout.dart @@ -0,0 +1,141 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:puissance4/config/default_game_settings.dart'; +import 'package:puissance4/config/default_global_settings.dart'; +import 'package:puissance4/cubit/settings_game_cubit.dart'; +import 'package:puissance4/cubit/settings_global_cubit.dart'; +import 'package:puissance4/ui/parameters/parameter_widget.dart'; +import 'package:puissance4/ui/widgets/actions/button_delete_saved_game.dart'; +import 'package:puissance4/ui/widgets/actions/button_game_start_new.dart'; +import 'package:puissance4/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(Expanded( + child: SizedBox(height: separatorHeight), + )); + + if (canResume == false) { + // Start new game + lines.add( + const AspectRatio( + aspectRatio: 3, + child: StartNewGameButton(), + ), + ); + } else { + // Resume game + lines.add(const AspectRatio( + aspectRatio: 3, + child: ResumeSavedGameButton(), + )); + // Delete saved game + lines.add(SizedBox.square( + dimension: MediaQuery.of(context).size.width / 5, + 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 isSelected = (value == currentValue); + + final double displayWidth = MediaQuery.of(context).size.width; + final double itemWidth = displayWidth / availableValues.length - 4; + + return SizedBox.square( + dimension: itemWidth, + child: ParameterWidget( + code: code, + value: value, + isSelected: isSelected, + size: itemWidth, + gameSettings: gameSettingsState.settings, + globalSettings: globalSettingsState.settings, + onPressed: () { + isGlobal + ? globalSettingsCubit.setParameterValue(code, value) + : gameSettingsCubit.setParameterValue(code, value); + }, + ), + ); + }, + ); + }, + ); + + parameterButtons.add(parameterButton); + } + + return parameterButtons; + } +} diff --git a/lib/ui/parameters/parameter_painter.dart b/lib/ui/parameters/parameter_painter.dart new file mode 100644 index 0000000000000000000000000000000000000000..26d80a114e9a429abad2dee59edbf70c4b5adb56 --- /dev/null +++ b/lib/ui/parameters/parameter_painter.dart @@ -0,0 +1,116 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +import 'package:puissance4/config/default_game_settings.dart'; +import 'package:puissance4/models/settings/settings_game.dart'; +import 'package:puissance4/models/settings/settings_global.dart'; +import 'package:puissance4/utils/tools.dart'; + +class ParameterPainter extends CustomPainter { + const ParameterPainter({ + required this.code, + required this.value, + required this.gameSettings, + required this.globalSettings, + }); + + final String code; + final String value; + final GameSettings gameSettings; + final GlobalSettings globalSettings; + + @override + void paint(Canvas canvas, Size size) { + // force square + final double canvasSize = min(size.width, size.height); + + // content + switch (code) { + case DefaultGameSettings.parameterCodeSize: + paintSizeParameterItem(canvas, canvasSize); + break; + default: + printlog('Unknown parameter: $code/$value'); + paintUnknownParameterItem(canvas, canvasSize); + } + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return false; + } + + // "unknown" parameter -> simple block with text + void paintUnknownParameterItem( + final Canvas canvas, + final double size, + ) { + final paint = Paint(); + paint.strokeJoin = StrokeJoin.round; + paint.strokeWidth = 3; + + final textSpan = TextSpan( + text: '$code\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, + ), + ); + } + + void paintSizeParameterItem( + final Canvas canvas, + final double size, + ) { + int gridWidth = 1; + + switch (value) { + case DefaultGameSettings.sizeValueMedium: + gridWidth = 3; + break; + default: + printlog('Wrong value for boardSize parameter value: $value'); + } + + final paint = Paint(); + paint.strokeJoin = StrokeJoin.round; + paint.strokeWidth = 3 / 100 * size; + + // Mini grid + final squareColor = Colors.grey.shade200; + final borderColor = Colors.grey.shade800; + + final double cellSize = size / 7; + final double origin = (size - gridWidth * cellSize) / 2; + + for (int row = 0; row < gridWidth; row++) { + for (int col = 0; col < gridWidth; col++) { + final Offset topLeft = Offset(origin + col * cellSize, origin + row * cellSize); + final Offset bottomRight = topLeft + Offset(cellSize, cellSize); + + paint.color = squareColor; + paint.style = PaintingStyle.fill; + canvas.drawRect(Rect.fromPoints(topLeft, bottomRight), paint); + + paint.color = borderColor; + paint.style = PaintingStyle.stroke; + canvas.drawRect(Rect.fromPoints(topLeft, bottomRight), paint); + } + } + } +} diff --git a/lib/ui/parameters/parameter_widget.dart b/lib/ui/parameters/parameter_widget.dart new file mode 100644 index 0000000000000000000000000000000000000000..0e22ad2ac3a64ed4c134212864920f9bbc2e4655 --- /dev/null +++ b/lib/ui/parameters/parameter_widget.dart @@ -0,0 +1,133 @@ +import 'package:flutter/material.dart'; + +import 'package:puissance4/config/default_game_settings.dart'; +import 'package:puissance4/models/settings/settings_game.dart'; +import 'package:puissance4/models/settings/settings_global.dart'; +import 'package:puissance4/ui/helpers/styled_button.dart'; +import 'package:puissance4/ui/parameters/parameter_painter.dart'; +import 'package:puissance4/utils/tools.dart'; + +class ParameterWidget extends StatelessWidget { + const ParameterWidget({ + super.key, + required this.code, + required this.value, + required this.isSelected, + required this.size, + required this.gameSettings, + required this.globalSettings, + required this.onPressed, + }); + + final String code; + final String value; + final bool isSelected; + final double size; + final GameSettings gameSettings; + final GlobalSettings globalSettings; + final VoidCallback onPressed; + + static const Color buttonColorActive = Colors.blue; + static const Color buttonColorInactive = Colors.white; + static const double buttonBorderWidth = 4.0; + static const double buttonBorderRadius = 12.0; + + @override + Widget build(BuildContext context) { + Widget content = const SizedBox.shrink(); + + switch (code) { + case DefaultGameSettings.parameterCodeGameMode: + content = getGameModeParameterItem(); + break; + case DefaultGameSettings.parameterCodeSize: + content = getSizeParameterItem(value); + break; + default: + printlog('Unknown parameter: $code/$value'); + content = getUnknownParameterItem(); + } + + final Color buttonColor = isSelected ? buttonColorActive : buttonColorInactive; + + return Container( + decoration: BoxDecoration( + color: buttonColor, + borderRadius: BorderRadius.circular(buttonBorderRadius), + border: Border.all( + color: buttonColor, + width: buttonBorderWidth, + ), + ), + child: content, + ); + } + + // "unknown" parameter -> simple block with text + Widget getUnknownParameterItem() { + return StyledButton.text( + caption: '$code / $value', + color: Colors.grey, + onPressed: null, + ); + } + + Widget getGameModeParameterItem() { + String text = ''; + Color baseColor = Colors.grey; + + switch (value) { + case DefaultGameSettings.gameModeHumanVsHuman: + text = '🧑 🧑'; + baseColor = Colors.green; + break; + case DefaultGameSettings.gameModeHumanVsRobot: + text = '🧑 🤖'; + baseColor = Colors.pink; + break; + case DefaultGameSettings.gameModeRobotVsHuman: + text = '🤖 🧑'; + baseColor = Colors.pink; + break; + case DefaultGameSettings.gameModeRobotVsRobot: + text = '🤖 🤖'; + baseColor = Colors.brown; + break; + default: + } + + return StyledButton.text( + caption: text, + color: baseColor, + onPressed: onPressed, + ); + } + + Widget getSizeParameterItem(final String value) { + Color backgroundColor = Colors.grey; + + switch (value) { + case DefaultGameSettings.sizeValueMedium: + backgroundColor = Colors.orange; + break; + default: + printlog('Wrong value for boardSize parameter value: $value'); + } + + return StyledButton( + color: backgroundColor, + onPressed: onPressed, + child: CustomPaint( + size: Size(size, size), + willChange: false, + painter: ParameterPainter( + code: code, + value: value, + gameSettings: gameSettings, + globalSettings: globalSettings, + ), + isComplex: true, + ), + ); + } +} diff --git a/lib/ui/screens/page_about.dart b/lib/ui/screens/page_about.dart new file mode 100644 index 0000000000000000000000000000000000000000..6bb8b3077fd60d52678ff5632bd4c7a56dbc71db --- /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:puissance4/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..b183a162d6519dc16b24bd55853ae10e96b3dd36 --- /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:puissance4/cubit/game_cubit.dart'; +import 'package:puissance4/models/game/game.dart'; +import 'package:puissance4/ui/layouts/parameters_layout.dart'; +import 'package:puissance4/ui/layouts/game_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..5fefd76be159ccf8c95869ae563f01847e36bcf6 --- /dev/null +++ b/lib/ui/screens/page_settings.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +import 'package:puissance4/ui/helpers/app_titles.dart'; +import 'package:puissance4/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..cbd1e496f8deacbaffd98a33db98a3f848db6688 --- /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:puissance4/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..70247310f825ab5d95095eb426ebc55d5a451308 --- /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:puissance4/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..4a806ca2593550f205fde10a56cd47b39186e249 --- /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:puissance4/config/menu.dart'; +import 'package:puissance4/cubit/nav_cubit.dart'; +import 'package:puissance4/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..58e217ace6f0c2224277f685e695195ecd4555f7 --- /dev/null +++ b/lib/ui/widgets/actions/button_delete_saved_game.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:puissance4/cubit/game_cubit.dart'; +import 'package:puissance4/ui/helpers/styled_button.dart'; + +class DeleteSavedGameButton extends StatelessWidget { + const DeleteSavedGameButton({super.key}); + + @override + Widget build(BuildContext context) { + return StyledButton( + color: Colors.grey, + onPressed: () { + BlocProvider.of<GameCubit>(context).deleteSavedGame(); + }, + child: const Image( + image: AssetImage('assets/ui/button_delete_saved_game.png'), + fit: BoxFit.fill, + ), + ); + } +} 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..1b955e6e6e598710ff2f34dcbaace8b28986f5fa --- /dev/null +++ b/lib/ui/widgets/actions/button_game_quit.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:puissance4/cubit/game_cubit.dart'; +import 'package:puissance4/ui/helpers/styled_button.dart'; + +class QuitGameButton extends StatelessWidget { + const QuitGameButton({super.key}); + + @override + Widget build(BuildContext context) { + return StyledButton( + color: Colors.red, + onPressed: () { + BlocProvider.of<GameCubit>(context).quitGame(); + }, + child: const Image( + image: AssetImage('assets/ui/button_back.png'), + fit: BoxFit.fill, + ), + ); + } +} 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..d66bd548bb4e9347d0bb129b61d7f1c3a1901653 --- /dev/null +++ b/lib/ui/widgets/actions/button_game_start_new.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:puissance4/cubit/game_cubit.dart'; +import 'package:puissance4/cubit/settings_game_cubit.dart'; +import 'package:puissance4/cubit/settings_global_cubit.dart'; +import 'package:puissance4/ui/helpers/styled_button.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 StyledButton( + color: Colors.blue, + onPressed: () { + BlocProvider.of<GameCubit>(context).startNewGame( + gameSettings: gameSettingsState.settings, + globalSettings: globalSettingsState.settings, + ); + }, + child: const Image( + image: AssetImage('assets/ui/button_start.png'), + fit: BoxFit.fill, + ), + ); + }, + ); + }, + ); + } +} 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..d144b1d266382f18a16d6a45e1658eb22dd27411 --- /dev/null +++ b/lib/ui/widgets/actions/button_resume_saved_game.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:puissance4/cubit/game_cubit.dart'; +import 'package:puissance4/ui/helpers/styled_button.dart'; + +class ResumeSavedGameButton extends StatelessWidget { + const ResumeSavedGameButton({super.key}); + + @override + Widget build(BuildContext context) { + return StyledButton( + color: Colors.blue, + onPressed: () { + BlocProvider.of<GameCubit>(context).resumeSavedGame(); + }, + child: const Image( + image: AssetImage('assets/ui/button_resume_game.png'), + fit: BoxFit.fill, + ), + ); + } +} diff --git a/lib/ui/widgets/game/game_board.dart b/lib/ui/widgets/game/game_board.dart new file mode 100644 index 0000000000000000000000000000000000000000..2ca486f089836e7261e54e5153d9c15831af21f5 --- /dev/null +++ b/lib/ui/widgets/game/game_board.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:puissance4/cubit/game_cubit.dart'; +import 'package:puissance4/ui/helpers/styled_button.dart'; + +class GameBoardWidget extends StatelessWidget { + const GameBoardWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Center( + child: BlocBuilder<GameCubit, GameState>( + builder: (BuildContext context, GameState gameState) { + final Color borderColor = Theme.of(context).colorScheme.onSurface; + + Widget cellContent({required int row, required int col}) { + final cellValue = gameState.currentGame.board.getCellValue(row: row, col: col); + final Color color = + cellValue == 0 ? Colors.grey : (cellValue == 1 ? Colors.yellow : Colors.red); + + return AspectRatio( + aspectRatio: 1, + child: StyledButton( + color: color, + onPressed: () { + BlocProvider.of<GameCubit>(context).tapOnColumn(col: col); + }, + child: SizedBox.shrink(), + ), + ); + } + + return Container( + margin: const EdgeInsets.all(2), + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + color: borderColor, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: borderColor, + width: 2, + ), + ), + child: Table( + defaultColumnWidth: const IntrinsicColumnWidth(), + children: [ + for (int row = 0; row < gameState.currentGame.sizeVertical; row++) + TableRow( + children: [ + for (int col = 0; col < gameState.currentGame.sizeHorizontal; col++) + Column( + children: [cellContent(row: row, col: col)], + ), + ], + ), + ], + ), + ); + }, + ), + ); + } +} diff --git a/lib/ui/widgets/game/game_bottom.dart b/lib/ui/widgets/game/game_bottom.dart new file mode 100644 index 0000000000000000000000000000000000000000..10574ea26226db727d4a3bd32737e7603bcba030 --- /dev/null +++ b/lib/ui/widgets/game/game_bottom.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:puissance4/cubit/game_cubit.dart'; +import 'package:puissance4/models/game/game.dart'; +import 'package:puissance4/ui/helpers/styled_button.dart'; + +class GameBottomWidget extends StatelessWidget { + const GameBottomWidget({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder<GameCubit, GameState>( + builder: (BuildContext context, GameState gameState) { + final Game currentGame = gameState.currentGame; + + final Color color = currentGame.currentPlayer == 0 + ? Colors.grey + : (currentGame.currentPlayer == 1 ? Colors.yellow : Colors.red); + + return StyledButton( + color: color, + onPressed: () {}, + child: SizedBox.shrink(), + ); + }, + ); + } +} diff --git a/lib/ui/widgets/game/game_end.dart b/lib/ui/widgets/game/game_end.dart new file mode 100644 index 0000000000000000000000000000000000000000..053e6043d404a8d2d8b81fa2c6e8d67ed875653c --- /dev/null +++ b/lib/ui/widgets/game/game_end.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:puissance4/cubit/game_cubit.dart'; +import 'package:puissance4/models/game/game.dart'; +import 'package:puissance4/ui/widgets/actions/button_game_quit.dart'; + +class GameEndWidget extends StatelessWidget { + const GameEndWidget({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder<GameCubit, GameState>( + builder: (BuildContext context, GameState gameState) { + final Game currentGame = gameState.currentGame; + + final bool connected4 = currentGame.connected4(); + + final String player1ImageAsset = (currentGame.currentPlayer == 1 && connected4) + ? 'assets/ui/game_win.png' + : (currentGame.currentPlayer == 2 && connected4) + ? 'assets/ui/game_fail.png' + : 'assets/ui/game_draw.png'; + final String player2ImageAsset = (currentGame.currentPlayer == 2 && connected4) + ? 'assets/ui/game_win.png' + : (currentGame.currentPlayer == 1 && connected4) + ? 'assets/ui/game_fail.png' + : 'assets/ui/game_draw.png'; + + final Image player1Image = Image( + image: AssetImage(player1ImageAsset), + fit: BoxFit.fill, + ); + + final Image player2Image = Image( + image: AssetImage(player2ImageAsset), + fit: BoxFit.fill, + ); + + return Container( + margin: const EdgeInsets.all(2), + padding: const EdgeInsets.all(2), + child: Table( + defaultColumnWidth: const IntrinsicColumnWidth(), + defaultVerticalAlignment: TableCellVerticalAlignment.bottom, + children: [ + TableRow( + children: [ + Column(children: [player1Image]), + Column(children: [player1Image]), + Column( + children: [ + currentGame.animationInProgress + ? Image( + image: const AssetImage('assets/ui/placeholder.png'), + fit: BoxFit.fill, + ) + : SizedBox.square( + dimension: 70, + child: QuitGameButton(), + ), + ], + ), + Column(children: [player2Image]), + Column(children: [player2Image]), + ], + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/ui/widgets/global_app_bar.dart b/lib/ui/widgets/global_app_bar.dart new file mode 100644 index 0000000000000000000000000000000000000000..3782f87ecefecef3b8b989d2383d57b844971054 --- /dev/null +++ b/lib/ui/widgets/global_app_bar.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:puissance4/config/menu.dart'; +import 'package:puissance4/cubit/game_cubit.dart'; +import 'package:puissance4/cubit/nav_cubit.dart'; +import 'package:puissance4/models/game/game.dart'; +import 'package:puissance4/ui/helpers/app_titles.dart'; +import 'package:puissance4/ui/helpers/styled_button.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(StyledButton( + color: Colors.red, + onPressed: () {}, + onLongPress: () { + BlocProvider.of<GameCubit>(context).quitGame(); + }, + child: const Image( + image: AssetImage('assets/ui/button_back.png'), + fit: BoxFit.fill, + ), + )); + } 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/pubspec.lock b/pubspec.lock index f6caeef09201deb0f5cb2a81c1d6dd62fdb9c724..229631c1f86719287b9acdfad6debc0e9f46254d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,38 @@ # 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: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + auto_size_text: + dependency: "direct main" + description: + name: auto_size_text + sha256: "3f5261cd3fb5f2a9ab4e2fc3fba84fd9fcaac8821f20a1d4e71f557521b22599" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + bloc: + dependency: transitive + description: + name: bloc + sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" + url: "https://pub.dev" + source: hosted + version: "8.1.4" characters: dependency: transitive description: @@ -9,6 +41,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: @@ -17,48 +57,362 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + crypto: + dependency: transitive + description: + name: crypto + sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 + url: "https://pub.dev" + source: hosted + version: "3.0.5" + 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: + name: ffi + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" flutter: dependency: "direct main" 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: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "5.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: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + url: "https://pub.dev" + source: hosted + version: "1.2.2" + 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: "3315600f3fb3b135be672bf4a178c55f274bebe368325ae18462c89ac1e3b413" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "5.0.0" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + url: "https://pub.dev" + source: hosted + version: "1.15.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: "894f37107424311bdae3e476552229476777b8752c5a2a2369c0cb9a2d5442ef" + url: "https://pub.dev" + source: hosted + version: "8.0.3" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: ac1f4a4847f1ade8e6a87d1f39f5d7c67490738642e2542f559ec38c37489a66 + url: "https://pub.dev" + source: hosted + version: "3.0.1" + path: + dependency: transitive + 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: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "2.1.4" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a + url: "https://pub.dev" + source: hosted + version: "2.2.12" + 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: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + url: "https://pub.dev" + source: hosted + version: "3.1.5" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + provider: + dependency: transitive + description: + name: provider + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + url: "https://pub.dev" + source: hosted + version: "6.1.2" + shared_preferences: + dependency: transitive + description: + name: shared_preferences + sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "3b9febd815c9ca29c9e3520d50ec32f49157711e143b7a4ca039eb87e8ade5ab" + url: "https://pub.dev" + source: hosted + version: "2.3.3" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e + url: "https://pub.dev" + source: hosted + version: "2.4.2" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" sky_engine: dependency: transitive 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: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" + url: "https://pub.dev" + source: hosted + version: "3.3.0+3" + 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: f3eab9d87c226415ef857cfd2167e1d12ad81ea1f5783b46cf644224fea4eab7 + url: "https://pub.dev" + source: hosted + version: "3.0.0" vector_math: dependency: transitive description: @@ -67,5 +421,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + web: + dependency: transitive + description: + name: web + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + url: "https://pub.dev" + source: hosted + version: "1.1.0" + win32: + dependency: transitive + description: + name: win32 + sha256: "4d45dc9069dba4619dc0ebd93c7cec5e66d8482cb625a370ac806dcc8165f2ec" + url: "https://pub.dev" + source: hosted + version: "5.5.5" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" sdks: - dart: ">=3.3.0-0 <4.0.0" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/pubspec.yaml b/pubspec.yaml index 55712aa189e5a9257a161b0ae6e35df0a5cc304f..f4ab7adb621b861e688ccb18170e0afc43cc5593 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,17 +1,48 @@ name: puissance4 description: puissance4 -publish_to: 'none' -version: 1.0.21+22 + +publish_to: "none" + +version: 1.1.0+23 environment: - sdk: '^3.0.0' + sdk: "^3.0.0" dependencies: flutter: sdk: flutter + # base + auto_size_text: ^3.0.0 + 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: ^3.0.0 + + # specific + # (none) + dev_dependencies: - flutter_lints: ^3.0.1 + flutter_lints: ^5.0.0 flutter: uses-material-design: true + assets: + - assets/translations/ + - assets/ui/ + + 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 diff --git a/icons/build_application_icons.sh b/resources/app/build_application_resources.sh similarity index 56% rename from icons/build_application_icons.sh rename to resources/app/build_application_resources.sh index 27dbe2647fe4e6d562fbd99451716d1b7d448570..1ace90d0e0029bf1704122d2b60bced59d5ed348 100755 --- a/icons/build_application_icons.sh +++ b/resources/app/build_application_resources.sh @@ -1,12 +1,21 @@ #! /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; } +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}")" +BASE_DIR="$(dirname "$(dirname "${CURRENT_DIR}")")" SOURCE_ICON="${CURRENT_DIR}/icon.svg" SOURCE_FASTLANE="${CURRENT_DIR}/featureGraphic.svg" @@ -31,14 +40,14 @@ function optimize_svg() { cp ${SVG} ${SVG}.tmp scour \ - --remove-descriptive-elements \ - --enable-id-stripping \ - --enable-viewboxing \ - --enable-comment-stripping \ - --nindent=4 \ - --quiet \ - -i ${SVG}.tmp \ - -o ${SVG} + --remove-descriptive-elements \ + --enable-id-stripping \ + --enable-viewboxing \ + --enable-comment-stripping \ + --nindent=4 \ + --quiet \ + -i ${SVG}.tmp \ + -o ${SVG} rm ${SVG}.tmp } @@ -57,10 +66,10 @@ function build_application_icon() { TARGET_PNG="${TARGET}.png" inkscape \ - --export-width=${ICON_SIZE} \ - --export-height=${ICON_SIZE} \ - --export-filename=${TARGET_PNG} \ - ${SOURCE_ICON} + --export-width=${ICON_SIZE} \ + --export-height=${ICON_SIZE} \ + --export-filename=${TARGET_PNG} \ + ${SOURCE_ICON} optipng ${OPTIPNG_OPTIONS} ${TARGET_PNG} } @@ -76,10 +85,10 @@ function build_fastlane_image() { TARGET_PNG="${TARGET}.png" inkscape \ - --export-width=${WIDTH} \ - --export-height=${HEIGHT} \ - --export-filename=${TARGET_PNG} \ - ${SOURCE_FASTLANE} + --export-width=${WIDTH} \ + --export-height=${HEIGHT} \ + --export-filename=${TARGET_PNG} \ + ${SOURCE_FASTLANE} optipng ${OPTIPNG_OPTIONS} ${TARGET_PNG} } @@ -94,24 +103,24 @@ function build_launch_image() { TARGET_PNG="${TARGET}.png" inkscape \ - --export-width=${ICON_SIZE} \ - --export-height=${ICON_SIZE} \ - --export-filename=${TARGET_PNG} \ - ${SOURCE_LAUNCH_IMAGE} + --export-width=${ICON_SIZE} \ + --export-height=${ICON_SIZE} \ + --export-filename=${TARGET_PNG} \ + ${SOURCE_LAUNCH_IMAGE} optipng ${OPTIPNG_OPTIONS} ${TARGET_PNG} } -build_application_icon 72 ${BASE_DIR}/android/app/src/main/res/mipmap-hdpi/ic_launcher -build_application_icon 48 ${BASE_DIR}/android/app/src/main/res/mipmap-mdpi/ic_launcher -build_application_icon 96 ${BASE_DIR}/android/app/src/main/res/mipmap-xhdpi/ic_launcher +build_application_icon 72 ${BASE_DIR}/android/app/src/main/res/mipmap-hdpi/ic_launcher +build_application_icon 48 ${BASE_DIR}/android/app/src/main/res/mipmap-mdpi/ic_launcher +build_application_icon 96 ${BASE_DIR}/android/app/src/main/res/mipmap-xhdpi/ic_launcher build_application_icon 144 ${BASE_DIR}/android/app/src/main/res/mipmap-xxhdpi/ic_launcher build_application_icon 192 ${BASE_DIR}/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher build_application_icon 512 ${BASE_DIR}/fastlane/metadata/android/en-US/images/icon -build_launch_image 72 ${BASE_DIR}/android/app/src/main/res/mipmap-hdpi/launch_image -build_launch_image 48 ${BASE_DIR}/android/app/src/main/res/mipmap-mdpi/launch_image -build_launch_image 96 ${BASE_DIR}/android/app/src/main/res/mipmap-xhdpi/launch_image +build_launch_image 72 ${BASE_DIR}/android/app/src/main/res/mipmap-hdpi/launch_image +build_launch_image 48 ${BASE_DIR}/android/app/src/main/res/mipmap-mdpi/launch_image +build_launch_image 96 ${BASE_DIR}/android/app/src/main/res/mipmap-xhdpi/launch_image build_launch_image 144 ${BASE_DIR}/android/app/src/main/res/mipmap-xxhdpi/launch_image build_launch_image 192 ${BASE_DIR}/android/app/src/main/res/mipmap-xxxhdpi/launch_image 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/resources/build_resources.sh b/resources/build_resources.sh new file mode 100755 index 0000000000000000000000000000000000000000..774953c5b885aae73f710aaa9d8b55a0d8dcc2c0 --- /dev/null +++ b/resources/build_resources.sh @@ -0,0 +1,6 @@ +#! /bin/bash + +CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" + +${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..f6c3f33201ae81b1f43cf677bb76c91e04727f33 --- /dev/null +++ b/resources/ui/build_ui_resources.sh @@ -0,0 +1,144 @@ +#! /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" +IMAGE_SIZE=192 + +####################################################### + +# Game images (svg files found in `images` folder) +AVAILABLE_GAME_SVG_IMAGES="" +AVAILABLE_GAME_PNG_IMAGES="" +if [ -d "${CURRENT_DIR}/images" ]; then + AVAILABLE_GAME_SVG_IMAGES="$(find "${CURRENT_DIR}/images" -type f -name "*.svg" | awk -F/ '{print $NF}' | cut -d"." -f1 | sort)" + AVAILABLE_GAME_PNG_IMAGES="$(find "${CURRENT_DIR}/images" -type f -name "*.png" | 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_SVG_IMAGES="" +SKIN_PNG_IMAGES="" +if [ -d "${CURRENT_DIR}/skins" ]; then + SKIN_SVG_IMAGES="$(find "${CURRENT_DIR}/skins" -type f -name "*.svg" | awk -F/ '{print $NF}' | cut -d"." -f1 | sort | uniq)" + SKIN_PNG_IMAGES="$(find "${CURRENT_DIR}/skins" -type f -name "*.png" | 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 png from svg +function build_svg_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=${IMAGE_SIZE} \ + --export-height=${IMAGE_SIZE} \ + --export-filename=${TARGET} \ + "${SOURCE}" + + optipng ${OPTIPNG_OPTIONS} "${TARGET}" +} + +# build png from png +function build_png_image() { + SOURCE="$1" + TARGET="$2" + + echo "Building ${TARGET}" + + if [ ! -f "${SOURCE}" ]; then + echo "Missing file: ${SOURCE}" + exit 1 + fi + + mkdir -p "$(dirname "${TARGET}")" + + convert -resize ${IMAGE_SIZE}x${IMAGE_SIZE} "${SOURCE}" "${TARGET}" + + optipng ${OPTIPNG_OPTIONS} "${TARGET}" +} + +function build_images_for_skin() { + SKIN_CODE="$1" + + # skin images + for SKIN_SVG_IMAGE in ${SKIN_SVG_IMAGES}; do + build_svg_image ${CURRENT_DIR}/skins/${SKIN_CODE}/${SKIN_SVG_IMAGE}.svg ${ASSETS_DIR}/skins/${SKIN_CODE}_${SKIN_SVG_IMAGE}.png + done + for SKIN_PNG_IMAGE in ${SKIN_PNG_IMAGES}; do + build_png_image ${CURRENT_DIR}/skins/${SKIN_CODE}/${SKIN_PNG_IMAGE}.png ${ASSETS_DIR}/skins/${SKIN_CODE}_${SKIN_PNG_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_SVG_IMAGE in ${AVAILABLE_GAME_SVG_IMAGES}; do + build_svg_image ${CURRENT_DIR}/images/${GAME_SVG_IMAGE}.svg ${ASSETS_DIR}/ui/${GAME_SVG_IMAGE}.png +done +for GAME_PNG_IMAGE in ${AVAILABLE_GAME_PNG_IMAGES}; do + build_png_image ${CURRENT_DIR}/images/${GAME_PNG_IMAGE}.png ${ASSETS_DIR}/ui/${GAME_PNG_IMAGE}.png +done + +# build skins images +for SKIN in ${AVAILABLE_SKINS}; do + build_images_for_skin "${SKIN}" +done diff --git a/resources/ui/images/button_back.svg b/resources/ui/images/button_back.svg new file mode 100644 index 0000000000000000000000000000000000000000..018d8b734d2932028fbfce1643c4e888ff1b45b1 --- /dev/null +++ b/resources/ui/images/button_back.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"><path transform="matrix(1.3783 .61747 -.61747 1.3783 45.198 93.762)" d="m11.645-14.603-44.77-4.6003 26.369-36.472z" fill="#fff" stroke="#950e4f" stroke-linecap="round" stroke-linejoin="round" stroke-width="7.2832"/></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..c3f872e434052a6b4e7036b530ced8e6233508e4 --- /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"><path d="m76.652 23.303-3.6441 58.302c-0.28153 4.5103-4.0223 8.0241-8.5413 8.0241h-35.27c-4.5189 0-8.2598-3.5138-8.5413-8.0241l-3.6441-58.302h-5.4824c-1.7723 0-3.2093-1.437-3.2093-3.2093 0-1.773 1.437-3.2093 3.2093-3.2093h70.605c1.7723 0 3.2093 1.4363 3.2093 3.2093 0 1.7723-1.437 3.2093-3.2093 3.2093zm-6.8314 0h-45.979l3.0819 55.867c0.12535 2.268 2.0008 4.0433 4.2732 4.0433h31.268c2.2724 0 4.1478-1.7752 4.2732-4.0433zm-22.99 6.4188c1.6541 0 2.9952 1.3411 2.9952 2.9952v41.08c0 1.6541-1.3411 2.9952-2.9952 2.9952-1.6542 0-2.9952-1.3411-2.9952-2.9952v-41.08c0-1.6541 1.3411-2.9952 2.9952-2.9952zm-12.837 0c1.6756 0 3.0553 1.3181 3.1312 2.9921l1.8776 41.3c0.06665 1.4664-1.0681 2.7087-2.5345 2.7762-0.04011 0.0015-0.08024 0.0021-0.12108 0.0021-1.5595 0-2.8476-1.2193-2.9328-2.7774l-2.253-41.3c-0.08524-1.5646 1.114-2.9012 2.6779-2.9864 0.05157-0.0029 0.10317-0.0042 0.15474-0.0042zm25.675 0c1.5667 0 2.8361 1.2694 2.8361 2.8361 0 0.05156-6.87e-4 0.10317-0.0036 0.15474l-2.2416 41.088c-0.09171 1.6778-1.4786 2.991-3.1586 2.991-1.5667 0-2.8361-1.2694-2.8361-2.8361 0-0.05156 7.31e-4 -0.10315 0.0036-0.15474l2.2417-41.088c0.09172-1.6778 1.4786-2.991 3.1586-2.991zm-21.397-25.675h17.117c4.7265 0 8.5578 3.8313 8.5578 8.5578v4.2795h-34.231v-4.2795c0-4.7265 3.8313-8.5578 8.5578-8.5578zm0.42837 6.4188c-1.4184 0-2.5675 1.1491-2.5675 2.5675v3.8512h21.394v-3.8512c0-1.4184-1.1491-2.5675-2.5675-2.5675z" fill="#fff" fill-rule="evenodd" stroke="#050200"/></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..2bf973276aefa564ecff7d6149899298344819f9 --- /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"><g transform="translate(-5.618)" fill="#fff" stroke="#105ea2" stroke-linecap="round" stroke-linejoin="round"><path transform="matrix(-1.3783 -.61747 .61747 -1.3783 55.567 -.086035)" d="m11.645-14.603-44.77-4.6003 26.369-36.472z" stroke-width="7.2832"/><path d="m15.535 12.852 2e-3 67.973z" stroke-width="11"/></g></svg> diff --git a/resources/ui/images/button_start.svg b/resources/ui/images/button_start.svg new file mode 100644 index 0000000000000000000000000000000000000000..4d7634a9f3fb559e590ee965e1341ae2634bf80f --- /dev/null +++ b/resources/ui/images/button_start.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"><path transform="matrix(-1.3783 -.61747 .61747 -1.3783 46.954 -.086035)" d="m11.645-14.603-44.77-4.6003 26.369-36.472z" fill="#fff" stroke="#105ea2" stroke-linecap="round" stroke-linejoin="round" stroke-width="7.2832"/></svg> diff --git a/resources/ui/images/game_draw.svg b/resources/ui/images/game_draw.svg new file mode 100644 index 0000000000000000000000000000000000000000..d988c4937ec1d00ce581b412ff4a859a0fd1862d --- /dev/null +++ b/resources/ui/images/game_draw.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"/> diff --git a/resources/ui/images/game_fail.svg b/resources/ui/images/game_fail.svg new file mode 100644 index 0000000000000000000000000000000000000000..d988c4937ec1d00ce581b412ff4a859a0fd1862d --- /dev/null +++ b/resources/ui/images/game_fail.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"/> diff --git a/resources/ui/images/game_win.svg b/resources/ui/images/game_win.svg new file mode 100644 index 0000000000000000000000000000000000000000..fe20923864d0c5d39168eced03038b65106a596b --- /dev/null +++ b/resources/ui/images/game_win.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"><g transform="matrix(.17604 0 0 .17604 7.9341 1.7716)"><path d="m101.92 496.35c-1.8555 0-3.7109-0.69532-5.1484-2.0898-2.9297-2.8438-3-7.5234-0.15234-10.453l9.1875-9.4648c2.8438-2.9297 7.5234-3 10.453-0.15625s3 7.5234 0.15625 10.453l-9.1914 9.4648c-1.4492 1.4961-3.375 2.2461-5.3047 2.2461z" fill="#ff4e61"/><path d="m201.65 133.26c-1.8516 0-3.7109-0.69531-5.1445-2.0898-2.9297-2.8438-3-7.5234-0.15625-10.449l9.1914-9.4688c2.8438-2.9297 7.5195-3 10.449-0.15625s3 7.5234 0.15625 10.453l-9.1914 9.4688c-1.4492 1.4922-3.375 2.2422-5.3047 2.2422z" fill="#ff4e61"/><path d="m413.8 100.39c-1.8555 0-3.7109-0.69141-5.1484-2.0859-2.9297-2.8438-3-7.5234-0.15625-10.453l9.1914-9.4688c2.8438-2.9258 7.5234-2.9961 10.453-0.15234 2.9297 2.8398 3 7.5234 0.15625 10.449l-9.1914 9.4688c-1.4492 1.4922-3.375 2.2422-5.3047 2.2422z" fill="#5c73bc"/><path d="m413.8 463.77c-1.8555 0-3.7109-0.69532-5.1484-2.0859-2.9297-2.8438-3-7.5234-0.15625-10.453l9.1914-9.4688c2.8438-2.9258 7.5234-3 10.453-0.15625s3 7.5234 0.15625 10.453l-9.1914 9.4688c-1.4492 1.4922-3.375 2.2422-5.3047 2.2422z" fill="#fa0"/><path d="m63.07 112.91c-1.8516 0-3.7109-0.69141-5.1445-2.0859-2.9297-2.8438-3-7.5234-0.15625-10.453l9.1914-9.4687c2.8438-2.9258 7.5234-2.9961 10.453-0.15234 2.9258 2.8438 2.9961 7.5234 0.15234 10.449l-9.1914 9.4688c-1.4492 1.4922-3.375 2.2422-5.3047 2.2422z" fill="#fa0"/><path d="m12.309 278.82c-1.8516 0-3.7109-0.69141-5.1445-2.0859-2.9297-2.8438-3-7.5234-0.15625-10.453l9.1875-9.4688c2.8438-2.9297 7.5234-3 10.453-0.15625 2.9297 2.8438 3 7.5234 0.15625 10.453l-9.1914 9.4688c-1.4453 1.4922-3.375 2.2422-5.3047 2.2422z" fill="#2dc471"/><path d="m216.29 278.49-23.996 12.996c-6.2226 3.3711-13.496-2.0742-12.309-9.2148l4.582-27.523c0.47266-2.8359-0.4375-5.7266-2.4375-7.7344l-19.414-19.496c-5.0352-5.0547-2.2578-13.863 4.7031-14.906l26.824-4.0156c2.7656-0.41407 5.1524-2.1992 6.3867-4.7812l12-25.043c3.1133-6.4922 12.102-6.4922 15.215 0l11.996 25.043c1.2383 2.582 3.625 4.3672 6.3867 4.7812l26.828 4.0156c6.957 1.043 9.7344 9.8516 4.6992 14.906l-19.41 19.496c-2 2.0078-2.9141 4.8984-2.4414 7.7344l4.582 27.523c1.1914 7.1406-6.082 12.586-12.305 9.2148l-23.996-12.996c-2.4727-1.3398-5.4258-1.3398-7.8945 0z" fill="#ffd02f"/><path d="m220.24 512c-4.082 0-7.3906-3.3086-7.3906-7.3906v-115.59c0-4.082 3.3086-7.3945 7.3906-7.3945s7.3906 3.3125 7.3906 7.3945v115.59c0 4.082-3.3086 7.3906-7.3906 7.3906z" fill="#5c73bc"/><path d="m220.3 357.42h-0.11328c-4.082 0-7.3945-3.3125-7.3945-7.3945s3.3086-7.3906 7.3945-7.3906h0.11328c4.082 0 7.3906 3.3086 7.3906 7.3906s-3.3086 7.3945-7.3906 7.3945z" fill="#5c73bc"/><path d="m220.3 332h-0.14838c-4.082-0.0156-7.375-3.3398-7.3594-7.4219 0.0195-4.0742 3.3242-7.3594 7.3906-7.3594h0.14848c4.082 0.0156 7.375 3.3398 7.3594 7.4219-0.0156 4.0703-3.3242 7.3594-7.3906 7.3594z" fill="#fa0"/><path d="m87.234 230.89c-1.9297 0-3.8555-0.75-5.3047-2.2422l-79.34-81.738c-2.8438-2.9297-2.7773-7.6094 0.15234-10.449 2.9297-2.8438 7.6094-2.7734 10.453 0.15235l79.344 81.738c2.8438 2.9258 2.7734 7.6094-0.15625 10.449-1.4375 1.3945-3.293 2.0898-5.1484 2.0898z" fill="#ff4e61"/><path d="m113.95 258.5c-1.8633 0-3.7266-0.69922-5.1641-2.1055-2.9219-2.8516-2.9766-7.5312-0.125-10.453l0.082-0.082c2.8516-2.918 7.5312-2.9766 10.453-0.12109 2.9219 2.8516 2.9766 7.5312 0.12109 10.453l-0.0781 0.082c-1.4492 1.4805-3.3672 2.2266-5.2891 2.2266z" fill="#fa0"/><path d="m131.4 276.48c-1.8555 0-3.7109-0.69531-5.1484-2.0898-2.9258-2.8438-2.9961-7.5234-0.15235-10.449l0.0781-0.0859c2.8476-2.9297 7.5273-2.9961 10.453-0.15235 2.9297 2.8438 3 7.5234 0.15625 10.453l-0.082 0.082c-1.4492 1.4922-3.375 2.2422-5.3047 2.2422z" fill="#5c73bc"/><path d="m353.24 227.99c-1.8555 0-3.7109-0.69141-5.1445-2.0859-2.9297-2.8438-3-7.5234-0.15625-10.453l79.34-81.734c2.8438-2.9297 7.5234-3 10.453-0.15625 2.9297 2.8438 3 7.5234 0.15625 10.453l-79.344 81.734c-1.4492 1.4922-3.375 2.2422-5.3047 2.2422z" fill="#fa0"/><path d="m326.52 255.6c-1.9141 0-3.8242-0.73828-5.2695-2.2109l-0.082-0.082c-2.8633-2.9141-2.8203-7.5938 0.0899-10.453 2.9141-2.8633 7.5938-2.8203 10.453 0.0898l0.082 0.082c2.8594 2.9141 2.8203 7.5938-0.0937 10.453-1.4375 1.4141-3.3086 2.1211-5.1797 2.1211z" fill="#ff4e61"/><path d="m309.07 273.58c-1.9297 0-3.8555-0.75-5.3047-2.2422l-0.082-0.082c-2.8398-2.9297-2.7734-7.6094 0.15625-10.453s7.6094-2.7734 10.453 0.15234l0.082 0.082c2.8398 2.9297 2.7734 7.6094-0.15625 10.453-1.4375 1.3945-3.293 2.0898-5.1484 2.0898z" fill="#fa0"/><path d="m300.65 116.69c-1.2422 0-2.5-0.3125-3.6523-0.97266-3.5469-2.0234-4.7812-6.5391-2.7578-10.082l56.863-99.652c2.0234-3.543 6.5352-4.7773 10.082-2.7539 3.5469 2.0234 4.7812 6.5391 2.7578 10.082l-56.863 99.652c-1.3633 2.3867-3.8594 3.7266-6.4297 3.7266z" fill="#62d38f"/><path d="m281.52 150.33c-1.293 0-2.5977-0.33593-3.7891-1.0469l-0.0976-0.0586c-3.5-2.0938-4.6445-6.6328-2.5469-10.137 2.0938-3.5078 6.6328-4.6445 10.137-2.5508l0.0977 0.0586c3.5039 2.0938 4.6445 6.6328 2.5508 10.137-1.3867 2.3164-3.8359 3.5976-6.3516 3.5976z" fill="#fa0"/><path d="m269.02 172.25c-1.3008 0-2.6172-0.34375-3.8086-1.0625l-0.0977-0.0586c-3.4961-2.1094-4.6211-6.6523-2.5156-10.148 2.1094-3.4961 6.6523-4.6172 10.148-2.5117l0.0976 0.0586c3.4961 2.1094 4.6211 6.6523 2.5117 10.148-1.3867 2.3008-3.832 3.5742-6.3359 3.5742z" fill="#2dc471"/><path d="m139.96 116.69c-2.5703 0-5.0664-1.3398-6.4297-3.7305l-56.863-99.648c-2.0234-3.5469-0.78906-8.0586 2.7539-10.082 3.5469-2.0234 8.0625-0.79297 10.086 2.7539l56.863 99.648c2.0234 3.5469 0.78906 8.0625-2.7539 10.086-1.1562 0.66016-2.4141 0.97266-3.6562 0.97266z" fill="#5c73bc"/><path d="m159.09 150.33c-2.5078 0-4.957-1.2773-6.3438-3.582-2.1016-3.5-0.96875-8.043 2.5273-10.145l0.10157-0.0586c3.5-2.1016 8.0391-0.97266 10.141 2.5273 2.1055 3.5 0.97266 8.0391-2.5273 10.145l-0.0977 0.0586c-1.1914 0.71484-2.5039 1.0547-3.8008 1.0547z" fill="#ff4e61"/><path d="m171.6 172.25c-2.5 0-4.9375-1.2656-6.3281-3.5625-2.1172-3.4922-1-8.0352 2.4883-10.152l0.0977-0.0586c3.4961-2.1133 8.0391-1 10.156 2.4922 2.1133 3.4922 1 8.0352-2.4922 10.152l-0.0977 0.0586c-1.1992 0.72656-2.5195 1.0703-3.8242 1.0703z" fill="#fa0"/><path d="m402.14 357.28-15.523 11.602c-4.0234 3.0117-9.6523-0.043-9.5234-5.1641l0.5039-19.75c0.0508-2.0352-0.87109-3.9648-2.4688-5.1641l-15.508-11.621c-4.0234-3.0156-2.9453-9.4726 1.8242-10.93l18.391-5.6094c1.8906-0.57812 3.3906-2.082 4-4.0156l5.9375-18.785c1.5391-4.875 7.8359-5.8125 10.652-1.5898l10.863 16.285c1.1211 1.6758 2.9688 2.6797 4.9414 2.6797l19.18 0.0117c4.9766 4e-3 7.7891 5.8828 4.7578 9.9492l-11.676 15.672c-1.2031 1.6172-1.5586 3.7383-0.94922 5.6719l5.918 18.797c1.5312 4.875-3.0273 9.4453-7.7148 7.7344l-18.078-6.5977c-1.8594-0.67969-3.9258-0.37109-5.5273 0.82422z" fill="#ffd02f"/><path d="m261.51 512c-4.082 0-7.3906-3.3086-7.3906-7.3906 0-57.23 22.832-95.922 41.984-118.3 20.828-24.332 41.613-35.023 42.488-35.469 3.6406-1.8477 8.0898-0.39063 9.9336 3.2539 1.8438 3.6367 0.39453 8.0781-3.2422 9.9297-0.3125 0.16016-19.5 10.164-38.367 32.395-25.227 29.719-38.016 66.121-38.016 108.2 0 4.082-3.3086 7.3906-7.3906 7.3906z" fill="#ff4e61"/><path d="m102.86 397.35 11.766 15.605c3.0547 4.0469 9.2852 2.7305 10.547-2.2266l4.8633-19.113c0.5-1.9648 1.9102-3.5547 3.7695-4.2461l18.039-6.707c4.6797-1.7383 5.3906-8.25 1.207-11.016l-16.141-10.672c-1.6602-1.1016-2.6914-2.9726-2.7578-5.0039l-0.61719-19.75c-0.15625-5.1211-5.9492-7.832-9.7969-4.5859l-14.84 12.516c-1.5312 1.2891-3.5781 1.7227-5.4726 1.1562l-18.422-5.5c-4.7773-1.4258-9.0703 3.4102-7.2617 8.1836l6.9688 18.41c0.71875 1.8945 0.48438 4.0352-0.625 5.7188l-10.77 16.348c-2.793 4.2422 0.34375 9.9375 5.3125 9.6445l19.145-1.1406c1.9727-0.11719 3.875 0.77343 5.0859 2.3789z" fill="#ffd02f"/><path d="m179.02 512c-4.082 0-7.3906-3.3086-7.3906-7.3906 0-30.059-6.6797-57.559-19.852-81.734-1.9531-3.5859-0.62891-8.0742 2.957-10.027 3.5859-1.9531 8.0742-0.62891 10.027 2.9531 14.363 26.375 21.648 56.254 21.648 88.809 0 4.082-3.3086 7.3906-7.3906 7.3906z" fill="#fa0"/><path d="m268.93 55.898c0-11.285-8.8828-20.434-19.836-20.434-10.957 0-19.836 9.1484-19.836 20.434 0 11.285 8.8789 20.438 19.836 20.438 10.953 0 19.836-9.1523 19.836-20.438z" fill="#ffd02f"/><path d="m373.08 446.81c0-11.285-8.8789-20.434-19.832-20.434-10.957 0-19.836 9.1484-19.836 20.434s8.8789 20.434 19.836 20.434c10.953 0 19.832-9.1484 19.832-20.434z" fill="#5c73bc"/><path d="m44.129 450.86c0-9.0508-7.1211-16.387-15.91-16.387-8.7852 0-15.906 7.3359-15.906 16.387 0 9.0547 7.1211 16.391 15.906 16.391 8.7891 0 15.91-7.3359 15.91-16.391z" fill="#62d38f"/><path d="m88.172 288.35c0-9.0508-7.1211-16.387-15.91-16.387-8.7852 0-15.906 7.3359-15.906 16.387s7.1211 16.391 15.906 16.391c8.7891 0 15.91-7.3398 15.91-16.391z" fill="#5c73bc"/><g fill="#ff4e61"><path d="m210.84 16.391c0-9.0547-7.1211-16.391-15.906-16.391-8.7891 0-15.91 7.3359-15.91 16.391 0 9.0508 7.1211 16.387 15.91 16.387 8.7852 0 15.906-7.3359 15.906-16.387z"/><path d="m365.23 152.88c0-9.0508-7.125-16.391-15.91-16.391-8.7852 0-15.91 7.3398-15.91 16.391s7.125 16.387 15.91 16.387c8.7852 0 15.91-7.3359 15.91-16.387z"/><path d="m139.96 32.746c-1.8555 0-3.7109-0.69141-5.1484-2.0898-2.9297-2.8438-3-7.5195-0.15625-10.449l9.1914-9.4688c2.8438-2.9297 7.5234-3 10.449-0.15625 2.9297 2.8438 3 7.5234 0.15625 10.453l-9.1875 9.4688c-1.4492 1.4922-3.3789 2.2422-5.3047 2.2422z"/></g></g></svg> diff --git a/resources/ui/images/placeholder.svg b/resources/ui/images/placeholder.svg new file mode 100644 index 0000000000000000000000000000000000000000..23ace81fbb82a8409cc0710c0f7bddd6381f7256 --- /dev/null +++ b/resources/ui/images/placeholder.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 102 102" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"/>