diff --git a/android/app/build.gradle b/android/app/build.gradle index 92dfdf95cca2b5d45bc09ff6fa27da0762ec3e1d..300b8347137fac66851a5f61f9a0e5d08109a59b 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.sortgame" defaultConfig { diff --git a/android/gradle.properties b/android/gradle.properties index cd2d833ca96b3d1ada4a39df51dc5f5ee67665b7..30298b3b3f04073678e48519b8c043edba635df8 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,5 +1,5 @@ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true -app.versionName=0.0.17 -app.versionCode=17 +app.versionName=0.0.18 +app.versionCode=18 diff --git a/assets/files/data.json b/assets/files/data.json deleted file mode 100644 index 0a37920d3c39be20abe6ba1159c8e111d992abce..0000000000000000000000000000000000000000 --- a/assets/files/data.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - data: [] -} 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/icons/button_back.png b/assets/icons/button_back.png new file mode 100644 index 0000000000000000000000000000000000000000..cc48ffb1dbb653d9a996f139dfbe02969724bfa5 Binary files /dev/null and b/assets/icons/button_back.png differ diff --git a/assets/icons/button_start.png b/assets/icons/button_start.png new file mode 100644 index 0000000000000000000000000000000000000000..6845e2f5c21598ab61f1684d2075aeec0334bf23 Binary files /dev/null and b/assets/icons/button_start.png differ diff --git a/assets/icons/placeholder.png b/assets/icons/placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..814df31be6ddc4275ebe4490c79365578dbef1f0 Binary files /dev/null and b/assets/icons/placeholder.png differ diff --git a/assets/translations/en.json b/assets/translations/en.json new file mode 100644 index 0000000000000000000000000000000000000000..5f4285607dd81c5e4a64c24734cb40530e4ac12d --- /dev/null +++ b/assets/translations/en.json @@ -0,0 +1,3 @@ +{ + "app_name": "SortGame" +} diff --git a/assets/translations/fr.json b/assets/translations/fr.json new file mode 100644 index 0000000000000000000000000000000000000000..5f4285607dd81c5e4a64c24734cb40530e4ac12d --- /dev/null +++ b/assets/translations/fr.json @@ -0,0 +1,3 @@ +{ + "app_name": "SortGame" +} diff --git a/fastlane/metadata/android/en-US/changelogs/18.txt b/fastlane/metadata/android/en-US/changelogs/18.txt new file mode 100644 index 0000000000000000000000000000000000000000..c4767c31cd60be21ad424c3e0b8357a7a045e229 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/18.txt @@ -0,0 +1 @@ +Add minimal playable game. diff --git a/fastlane/metadata/android/fr-FR/changelogs/18.txt b/fastlane/metadata/android/fr-FR/changelogs/18.txt new file mode 100644 index 0000000000000000000000000000000000000000..fa0c74f6946c75534baca4f6411a9e3a690c3dfe --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/18.txt @@ -0,0 +1 @@ +Ajout du jeu minimal. diff --git a/icons/build_game_icons.sh b/icons/build_game_icons.sh index 218080d1eb12952690ba1e182a468f6e3cf03d65..7368dc4543ff5b92568d683f3d0fb144364bbe04 100755 --- a/icons/build_game_icons.sh +++ b/icons/build_game_icons.sh @@ -16,10 +16,9 @@ ICON_SIZE=192 # Game images AVAILABLE_GAME_IMAGES=" -" - -# Settings images -AVAILABLES_GAME_SETTINGS=" + button_back + button_start + placeholder " ####################################################### @@ -64,19 +63,6 @@ function build_icon() { 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 @@ -90,9 +76,3 @@ 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/icons/build_icons.sh b/icons/build_icons.sh deleted file mode 100755 index fefc393e2f601cd671938068d23247d6bfb1682b..0000000000000000000000000000000000000000 --- a/icons/build_icons.sh +++ /dev/null @@ -1,48 +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; } -command -v convert >/dev/null 2>&1 || { echo >&2 "I require convert (imagemagick) 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}")" - -SOURCE="${CURRENT_DIR}/icon.svg" -OPTIPNG_OPTIONS="-preserve -quiet -o7" - -# optimize svg -cp ${SOURCE} ${SOURCE}.tmp -scour \ - --remove-descriptive-elements \ - --enable-id-stripping \ - --enable-viewboxing \ - --enable-comment-stripping \ - --nindent=4 \ - -i ${SOURCE}.tmp \ - -o ${SOURCE} -rm ${SOURCE}.tmp - -# build icons -function build_icon() { - ICON_SIZE="$1" - TARGET="$2" - - TARGET_PNG="${TARGET}.png" - - inkscape \ - --export-width=${ICON_SIZE} \ - --export-height=${ICON_SIZE} \ - --export-filename=${TARGET_PNG} \ - ${SOURCE} - - optipng ${OPTIPNG_OPTIONS} ${TARGET_PNG} -} - - -build_icon 72 ${BASE_DIR}/android/app/src/main/res/mipmap-hdpi/ic_launcher -build_icon 48 ${BASE_DIR}/android/app/src/main/res/mipmap-mdpi/ic_launcher -build_icon 96 ${BASE_DIR}/android/app/src/main/res/mipmap-xhdpi/ic_launcher -build_icon 144 ${BASE_DIR}/android/app/src/main/res/mipmap-xxhdpi/ic_launcher -build_icon 192 ${BASE_DIR}/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher diff --git a/icons/button_back.svg b/icons/button_back.svg new file mode 100644 index 0000000000000000000000000000000000000000..2622a578dba53ce582afabfc587c2a85a1fb6eaa --- /dev/null +++ b/icons/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"><rect x=".44662" y=".89101" width="92.772" height="91.894" ry="11.689" fill="#e41578" stroke="#fff" stroke-width=".238"/><path d="m59.387 71.362c1.1248 1.1302 4.0012 1.1302 4.0012 0v-45.921c0-1.1316-2.8832-1.1316-4.0121 0l-37.693 20.918c-1.1289 1.1248-1.1479 2.9551-0.02171 4.084z" fill="#fefeff" stroke="#930e4e" stroke-linecap="round" stroke-linejoin="round" stroke-width="8.257"/><path d="m57.857 68.048c0.96243 0.96706 3.4236 0.96706 3.4236 0v-39.292c0-0.96825-2.467-0.96825-3.4329 0l-32.252 17.898c-0.96594 0.96243-0.9822 2.5285-0.01858 3.4945z" fill="#fefeff" stroke="#feffff" stroke-linecap="round" stroke-linejoin="round" stroke-width="4.314"/></svg> diff --git a/icons/button_start.svg b/icons/button_start.svg new file mode 100644 index 0000000000000000000000000000000000000000..e9d49d2172b9a0305db82779971e3c1e12f34a70 --- /dev/null +++ b/icons/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"><rect x=".44662" y=".89101" width="92.772" height="91.894" ry="11.689" fill="#49a1ee" stroke="#fff" stroke-width=".238"/><path d="m34.852 25.44c-1.1248-1.1302-4.0012-1.1302-4.0012 0v45.921c0 1.1316 2.8832 1.1316 4.0121 0l37.693-20.918c1.1289-1.1248 1.1479-2.9551 0.02171-4.084z" fill="#fefeff" stroke="#105ca1" stroke-linecap="round" stroke-linejoin="round" stroke-width="8.257"/><path d="m36.382 28.754c-0.96243-0.96706-3.4236-0.96706-3.4236 0v39.292c0 0.96825 2.467 0.96825 3.4329 0l32.252-17.898c0.96594-0.96243 0.9822-2.5285 0.01858-3.4945z" fill="#fefeff" stroke="#feffff" stroke-linecap="round" stroke-linejoin="round" stroke-width="4.314"/></svg> diff --git a/icons/placeholder.svg b/icons/placeholder.svg new file mode 100644 index 0000000000000000000000000000000000000000..23ace81fbb82a8409cc0710c0f7bddd6381f7256 --- /dev/null +++ b/icons/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"/> diff --git a/lib/config/default_game_settings.dart b/lib/config/default_game_settings.dart new file mode 100644 index 0000000000000000000000000000000000000000..c4486031cf4e7ed40d95822ce17ee826fe84e4b2 --- /dev/null +++ b/lib/config/default_game_settings.dart @@ -0,0 +1,30 @@ +import 'package:sortgame/utils/tools.dart'; + +class DefaultGameSettings { + static const List<String> availableParameters = [ + 'itemsCount', + ]; + + static const int itemsCountValueLow = 5; + static const int itemsCountValueMedium = 10; + static const int itemsCountValueHigh = 15; + static const int itemsCountValueVeryHigh = 20; + + static const int defaultItemsCountValue = itemsCountValueMedium; + static const List<int> allowedItemsCountValues = [ + itemsCountValueLow, + itemsCountValueMedium, + itemsCountValueHigh, + itemsCountValueVeryHigh, + ]; + + static List<int> getAvailableValues(String parameterCode) { + switch (parameterCode) { + case 'itemsCount': + return DefaultGameSettings.allowedItemsCountValues; + } + + 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..8b0d15cae4ca4f0dbaa902ac35fdd0c865b91e5a --- /dev/null +++ b/lib/config/default_global_settings.dart @@ -0,0 +1,10 @@ +import 'package:sortgame/utils/tools.dart'; + +class DefaultGlobalSettings { + static const List<String> availableParameters = []; + + static List<int> getAvailableValues(String parameterCode) { + printlog('Did not find any available value for global parameter "$parameterCode".'); + return []; + } +} diff --git a/lib/config/theme.dart b/lib/config/theme.dart new file mode 100644 index 0000000000000000000000000000000000000000..be390348c7868e7c63387df13e13c46de43f8a23 --- /dev/null +++ b/lib/config/theme.dart @@ -0,0 +1,196 @@ +import 'package:flutter/material.dart'; + +/// Colors from Tailwind CSS (v3.0) - June 2022 +/// +/// https://tailwindcss.com/docs/customizing-colors + +const int _primaryColor = 0xFF6366F1; +const MaterialColor primarySwatch = MaterialColor(_primaryColor, <int, Color>{ + 50: Color(0xFFEEF2FF), // indigo-50 + 100: Color(0xFFE0E7FF), // indigo-100 + 200: Color(0xFFC7D2FE), // indigo-200 + 300: Color(0xFFA5B4FC), // indigo-300 + 400: Color(0xFF818CF8), // indigo-400 + 500: Color(_primaryColor), // indigo-500 + 600: Color(0xFF4F46E5), // indigo-600 + 700: Color(0xFF4338CA), // indigo-700 + 800: Color(0xFF3730A3), // indigo-800 + 900: Color(0xFF312E81), // indigo-900 +}); + +const int _textColor = 0xFF64748B; +const MaterialColor textSwatch = MaterialColor(_textColor, <int, Color>{ + 50: Color(0xFFF8FAFC), // slate-50 + 100: Color(0xFFF1F5F9), // slate-100 + 200: Color(0xFFE2E8F0), // slate-200 + 300: Color(0xFFCBD5E1), // slate-300 + 400: Color(0xFF94A3B8), // slate-400 + 500: Color(_textColor), // slate-500 + 600: Color(0xFF475569), // slate-600 + 700: Color(0xFF334155), // slate-700 + 800: Color(0xFF1E293B), // slate-800 + 900: Color(0xFF0F172A), // slate-900 +}); + +const Color errorColor = Color(0xFFDC2626); // red-600 + +final ColorScheme lightColorScheme = ColorScheme.light( + primary: primarySwatch.shade500, + secondary: primarySwatch.shade500, + onSecondary: Colors.white, + error: errorColor, + background: textSwatch.shade200, + onBackground: textSwatch.shade500, + onSurface: textSwatch.shade500, + surface: textSwatch.shade50, + surfaceVariant: Colors.white, + shadow: textSwatch.shade900.withOpacity(.1), +); + +final ColorScheme darkColorScheme = ColorScheme.dark( + primary: primarySwatch.shade500, + secondary: primarySwatch.shade500, + onSecondary: Colors.white, + error: errorColor, + background: const Color(0xFF171724), + onBackground: textSwatch.shade400, + onSurface: textSwatch.shade300, + surface: const Color(0xFF262630), + surfaceVariant: const Color(0xFF282832), + shadow: textSwatch.shade900.withOpacity(.2), +); + +final ThemeData lightTheme = ThemeData( + colorScheme: lightColorScheme, + fontFamily: 'Nunito', + textTheme: TextTheme( + displayLarge: TextStyle( + color: textSwatch.shade700, + fontFamily: 'Nunito', + ), + displayMedium: TextStyle( + color: textSwatch.shade600, + fontFamily: 'Nunito', + ), + displaySmall: TextStyle( + color: textSwatch.shade500, + fontFamily: 'Nunito', + ), + headlineLarge: TextStyle( + color: textSwatch.shade700, + fontFamily: 'Nunito', + ), + headlineMedium: TextStyle( + color: textSwatch.shade600, + fontFamily: 'Nunito', + ), + headlineSmall: TextStyle( + color: textSwatch.shade500, + fontFamily: 'Nunito', + ), + titleLarge: TextStyle( + color: textSwatch.shade700, + fontFamily: 'Nunito', + ), + titleMedium: TextStyle( + color: textSwatch.shade600, + fontFamily: 'Nunito', + ), + titleSmall: TextStyle( + color: textSwatch.shade500, + fontFamily: 'Nunito', + ), + bodyLarge: TextStyle( + color: textSwatch.shade700, + fontFamily: 'Nunito', + ), + bodyMedium: TextStyle( + color: textSwatch.shade600, + fontFamily: 'Nunito', + ), + bodySmall: TextStyle( + color: textSwatch.shade500, + fontFamily: 'Nunito', + ), + labelLarge: TextStyle( + color: textSwatch.shade700, + fontFamily: 'Nunito', + ), + labelMedium: TextStyle( + color: textSwatch.shade600, + fontFamily: 'Nunito', + ), + labelSmall: TextStyle( + color: textSwatch.shade500, + fontFamily: 'Nunito', + ), + ), +); + +final ThemeData darkTheme = lightTheme.copyWith( + colorScheme: darkColorScheme, + textTheme: TextTheme( + displayLarge: TextStyle( + color: textSwatch.shade200, + fontFamily: 'Nunito', + ), + displayMedium: TextStyle( + color: textSwatch.shade300, + fontFamily: 'Nunito', + ), + displaySmall: TextStyle( + color: textSwatch.shade400, + fontFamily: 'Nunito', + ), + headlineLarge: TextStyle( + color: textSwatch.shade200, + fontFamily: 'Nunito', + ), + headlineMedium: TextStyle( + color: textSwatch.shade300, + fontFamily: 'Nunito', + ), + headlineSmall: TextStyle( + color: textSwatch.shade400, + fontFamily: 'Nunito', + ), + titleLarge: TextStyle( + color: textSwatch.shade200, + fontFamily: 'Nunito', + ), + titleMedium: TextStyle( + color: textSwatch.shade300, + fontFamily: 'Nunito', + ), + titleSmall: TextStyle( + color: textSwatch.shade400, + fontFamily: 'Nunito', + ), + bodyLarge: TextStyle( + color: textSwatch.shade200, + fontFamily: 'Nunito', + ), + bodyMedium: TextStyle( + color: textSwatch.shade300, + fontFamily: 'Nunito', + ), + bodySmall: TextStyle( + color: textSwatch.shade400, + fontFamily: 'Nunito', + ), + labelLarge: TextStyle( + color: textSwatch.shade200, + fontFamily: 'Nunito', + ), + labelMedium: TextStyle( + color: textSwatch.shade300, + fontFamily: 'Nunito', + ), + labelSmall: TextStyle( + color: textSwatch.shade400, + fontFamily: 'Nunito', + ), + ), +); + +final ThemeData appTheme = darkTheme; diff --git a/lib/cubit/game_cubit.dart b/lib/cubit/game_cubit.dart new file mode 100644 index 0000000000000000000000000000000000000000..8a2551c889ff95a9c6ae8d42389f6b1fc9a931df --- /dev/null +++ b/lib/cubit/game_cubit.dart @@ -0,0 +1,87 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; + +import 'package:sortgame/models/game.dart'; +import 'package:sortgame/models/settings_game.dart'; +import 'package:sortgame/models/settings_global.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( + items: state.currentGame.items, + gameSettings: state.currentGame.gameSettings, + globalSettings: state.currentGame.globalSettings, + isRunning: state.currentGame.isRunning, + isFinished: state.currentGame.isFinished, + position: state.currentGame.position, + score: state.currentGame.score, + ); + game.dump(); + + updateState(game); + } + + void quitGame() { + state.currentGame.updateGameIsRunning(false); + refresh(); + } + + void increasePosition() { + if (state.currentGame.position < state.currentGame.items.length) { + state.currentGame.increasePosition(); + } else { + state.currentGame.updateGameIsFinished(true); + } + refresh(); + } + + void increaseScore(int increment) { + state.currentGame.increaseScore(increment); + refresh(); + } + + void startNewGame({ + required GameSettings gameSettings, + required GlobalSettings globalSettings, + }) { + Game newGame = Game.createNew( + gameSettings: gameSettings, + globalSettings: globalSettings, + ); + + newGame.dump(); + + updateState(newGame); + refresh(); + } + + @override + GameState? fromJson(Map<String, dynamic> json) { + 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..3fd161a0915313722b7a15c55c7cf538a7e3b6e1 --- /dev/null +++ b/lib/cubit/game_state.dart @@ -0,0 +1,19 @@ +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, + ]; + + Map<String, dynamic> get values => <String, dynamic>{ + 'currentGame': currentGame, + }; +} diff --git a/lib/cubit/settings_game_cubit.dart b/lib/cubit/settings_game_cubit.dart new file mode 100644 index 0000000000000000000000000000000000000000..a2c04ea96f915f709c25b31942c277bb742ade54 --- /dev/null +++ b/lib/cubit/settings_game_cubit.dart @@ -0,0 +1,61 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; + +import 'package:sortgame/models/settings_game.dart'; +import 'package:sortgame/utils/tools.dart'; + +part 'settings_game_state.dart'; + +class GameSettingsCubit extends HydratedCubit<GameSettingsState> { + GameSettingsCubit() : super(GameSettingsState(settings: GameSettings.createDefault())); + + void setValues({ + int? itemsCount, + }) { + emit( + GameSettingsState( + settings: GameSettings( + itemsCount: itemsCount ?? state.settings.itemsCount, + ), + ), + ); + } + + int getParameterValue(String code) { + switch (code) { + case 'itemsCount': + return GameSettings.getItemsCountValueFromUnsafe(state.settings.itemsCount); + } + return 0; + } + + void setParameterValue(String code, int value) { + printlog('GameSettingsCubit.setParameterValue'); + printlog('code: $code / value: $value'); + + int itemsCount = code == 'itemsCount' ? value : getParameterValue('itemsCount'); + + setValues( + itemsCount: itemsCount, + ); + } + + @override + GameSettingsState? fromJson(Map<String, dynamic> json) { + int itemsCount = json['itemsCount'] as int; + + return GameSettingsState( + settings: GameSettings( + itemsCount: itemsCount, + ), + ); + } + + @override + Map<String, dynamic>? toJson(GameSettingsState state) { + return <String, dynamic>{ + 'itemsCount': state.settings.itemsCount, + }; + } +} diff --git a/lib/cubit/settings_game_state.dart b/lib/cubit/settings_game_state.dart new file mode 100644 index 0000000000000000000000000000000000000000..b773dc69be12673b158e880e2d7e6e7bec465506 --- /dev/null +++ b/lib/cubit/settings_game_state.dart @@ -0,0 +1,19 @@ +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, + ]; + + Map<String, dynamic> get values => <String, dynamic>{ + 'settings': settings, + }; +} diff --git a/lib/cubit/settings_global_cubit.dart b/lib/cubit/settings_global_cubit.dart new file mode 100644 index 0000000000000000000000000000000000000000..36b4b20ed5e0c18f5509e0214986fecb86df4d3a --- /dev/null +++ b/lib/cubit/settings_global_cubit.dart @@ -0,0 +1,44 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; + +import 'package:sortgame/models/settings_global.dart'; +import 'package:sortgame/utils/tools.dart'; + +part 'settings_global_state.dart'; + +class GlobalSettingsCubit extends HydratedCubit<GlobalSettingsState> { + GlobalSettingsCubit() : super(GlobalSettingsState(settings: GlobalSettings.createDefault())); + + void setValues() { + emit( + GlobalSettingsState( + settings: GlobalSettings(), + ), + ); + } + + int getParameterValue(String code) { + switch (code) {} + return 0; + } + + void setParameterValue(String code, int value) { + printlog('GlobalSettingsCubit.setParameterValue'); + printlog('code: $code / value: $value'); + + setValues(); + } + + @override + GlobalSettingsState? fromJson(Map<String, dynamic> json) { + return GlobalSettingsState( + settings: GlobalSettings(), + ); + } + + @override + Map<String, dynamic>? toJson(GlobalSettingsState state) { + return <String, dynamic>{}; + } +} diff --git a/lib/cubit/settings_global_state.dart b/lib/cubit/settings_global_state.dart new file mode 100644 index 0000000000000000000000000000000000000000..4e4fbdf707b4e805f2092d0ca6a68a2de1c957c6 --- /dev/null +++ b/lib/cubit/settings_global_state.dart @@ -0,0 +1,19 @@ +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, + ]; + + Map<String, dynamic> get values => <String, dynamic>{ + 'settings': settings, + }; +} diff --git a/lib/data/fetch_data_helper.dart b/lib/data/fetch_data_helper.dart new file mode 100644 index 0000000000000000000000000000000000000000..2b5148c194338d497a9b2ef79d10ecddf0d0395f --- /dev/null +++ b/lib/data/fetch_data_helper.dart @@ -0,0 +1,75 @@ +import 'package:sortgame/data/game_data.dart'; +import 'package:sortgame/models/data/category.dart'; +import 'package:sortgame/models/data/game_item.dart'; +import 'package:sortgame/models/data/item.dart'; +import 'package:sortgame/utils/tools.dart'; + +class FetchDataHelper { + FetchDataHelper(); + + final List<Category> _categories = []; + List<Category> get categories => _categories; + + final List<Item> _items = []; + List<Item> get items => _items; + + final List<GameItem> _mapping = []; + + void init() { + try { + const gameData = GameData.data; + + final List<dynamic> rawCategories = gameData['categories'] as List<dynamic>; + for (var rawElement in rawCategories) { + final element = rawElement.toString(); + _categories.add(Category(key: element, text: element)); + } + + final List<dynamic> rawItems = gameData['items'] as List<dynamic>; + for (var rawElement in rawItems) { + final element = rawElement.toString(); + _items.add(Item(key: element, text: element)); + } + + final Map<String, dynamic> rawMapping = gameData['mapping'] as Map<String, dynamic>; + final Map<String, dynamic> rawMappingItems = rawMapping['items'] as Map<String, dynamic>; + rawMappingItems.forEach( + (String itemName, itemMappings) { + List<String> rawIsCategories = itemMappings['is'] as List<String>; + List<String> rawIsNotCategories = itemMappings['isnot'] as List<String>; + + _mapping.add(GameItem( + item: Item( + key: itemName, + text: itemName, + ), + isCategory: rawIsCategories + .map((String category) => Category(key: category, text: category)) + .toList(), + isNotCategory: rawIsNotCategories + .map((String category) => Category(key: category, text: category)) + .toList(), + )); + }, + ); + } catch (e) { + printlog("$e"); + } + } + + List<GameItem> getItems(int count) { + if (_mapping.isEmpty) { + init(); + } + + List<GameItem> items = _mapping; + + // Remove items without enough data + items.removeWhere((GameItem question) => + (question.isCategory.isEmpty || question.isNotCategory.isEmpty)); + + items.shuffle(); + + return items.take(count).toList(); + } +} diff --git a/lib/data/game_data.dart b/lib/data/game_data.dart new file mode 100644 index 0000000000000000000000000000000000000000..4566cbfcbdb18652785fc73e67541f454674a9d0 --- /dev/null +++ b/lib/data/game_data.dart @@ -0,0 +1,376 @@ +class GameData { + static const Map<String, dynamic> data = { + "categories": ["artificiel", "gazeux", "inerte", "liquide", "naturel", "solide", "vivant"], + "exclusions": [ + ["liquide", "solide", "gazeux"], + ["inerte", "vivant"], + ["naturel", "artificiel"] + ], + "items": [ + "ABEILLE", + "AMPOULE", + "ARBRE", + "ASPIRATEUR", + "BALAI", + "BANANE", + "BERCEAU", + "BONBON", + "BOUGIE", + "BROUETTE", + "CAFETIÈRE", + "CANARD", + "CASQUETTE", + "CHAISE", + "CHAUSSURE", + "CINTRE", + "COCCINELLE", + "CONFITURE", + "CORDE", + "CROCODILE", + "DROMADAIRE", + "FENÊTRE", + "FOURMI", + "FUSÉE", + "GOURDE", + "GUÊPE", + "HÉLICOPTÈRE", + "KANGOUROU", + "LAMPE", + "LICORNE", + "LIVRE", + "MAISON", + "MOUCHE", + "NOEUD", + "ORAGE", + "OURS", + "PAPILLON", + "PEIGNE", + "PENDULE", + "PIANO", + "PIZZA", + "POMME", + "POULET", + "PUZZLE", + "PÊCHE", + "REQUIN", + "ROSE", + "SAC À DOS", + "SANGLIER", + "SAVON", + "SINGE", + "STADE", + "TABOURET", + "TARTINE", + "TIROIR", + "TORTUE", + "TROMPETTE", + "VACHE", + "VOILIER", + "ZÈBRE" + ], + "mapping": { + "items": { + "ABEILLE": { + "is": ["naturel", "vivant"], + "isnot": ["artificiel", "gazeux", "inerte"], + "na": ["liquide"] + }, + "AMPOULE": { + "is": ["artificiel", "inerte", "solide"], + "isnot": ["gazeux", "liquide", "naturel", "vivant"], + "na": [] + }, + "ARBRE": { + "is": ["naturel", "vivant"], + "isnot": ["artificiel", "inerte", "liquide"], + "na": [] + }, + "ASPIRATEUR": { + "is": ["artificiel", "inerte", "solide"], + "isnot": ["gazeux", "liquide", "naturel", "vivant"], + "na": [] + }, + "BALAI": { + "is": ["artificiel", "inerte", "solide"], + "isnot": ["gazeux", "liquide", "naturel", "vivant"], + "na": [] + }, + "BANANE": { + "is": ["solide", "vivant"], + "isnot": ["gazeux", "inerte", "liquide"], + "na": [] + }, + "BERCEAU": { + "is": ["artificiel", "inerte"], + "isnot": ["liquide", "naturel", "vivant"], + "na": [] + }, + "BONBON": { + "is": ["artificiel", "inerte", "solide"], + "isnot": ["gazeux", "liquide", "naturel", "vivant"], + "na": [] + }, + "BOUGIE": { + "is": ["artificiel", "inerte", "solide"], + "isnot": ["gazeux", "liquide", "naturel", "vivant"], + "na": [] + }, + "BROUETTE": { + "is": ["artificiel", "inerte"], + "isnot": ["liquide", "naturel", "vivant"], + "na": [] + }, + "CAFETIÈRE": { + "is": ["inerte"], + "isnot": ["liquide", "vivant"], + "na": [] + }, + "CANARD": { + "is": ["naturel", "solide", "vivant"], + "isnot": ["artificiel", "gazeux", "inerte", "liquide"], + "na": ["liquide"] + }, + "CASQUETTE": { + "is": ["artificiel", "inerte"], + "isnot": ["liquide", "naturel", "vivant"], + "na": [] + }, + "CHAISE": { + "is": ["artificiel", "inerte", "solide"], + "isnot": ["gazeux", "liquide", "naturel", "vivant"], + "na": [] + }, + "CHAUSSURE": { + "is": ["artificiel", "inerte", "solide"], + "isnot": ["gazeux", "liquide", "naturel", "vivant"], + "na": [] + }, + "CINTRE": { + "is": ["artificiel", "inerte", "solide"], + "isnot": ["gazeux", "liquide", "naturel", "vivant"], + "na": [] + }, + "COCCINELLE": { + "is": ["naturel", "vivant"], + "isnot": ["artificiel", "gazeux", "inerte"], + "na": ["liquide", "solide"] + }, + "CONFITURE": { + "is": ["inerte", "solide"], + "isnot": ["gazeux", "liquide", "vivant"], + "na": ["liquide"] + }, + "CORDE": { + "is": ["artificiel", "inerte", "solide"], + "isnot": ["gazeux", "liquide", "naturel", "vivant"], + "na": [] + }, + "CROCODILE": { + "is": ["naturel", "vivant"], + "isnot": ["artificiel", "gazeux", "inerte"], + "na": ["liquide", "solide"] + }, + "DROMADAIRE": { + "is": ["naturel", "solide", "vivant"], + "isnot": ["artificiel", "gazeux", "inerte", "liquide"], + "na": [] + }, + "FENÊTRE": { + "is": ["artificiel", "inerte"], + "isnot": ["naturel", "vivant"], + "na": ["liquide"] + }, + "FOURMI": { + "is": ["naturel", "solide", "vivant"], + "isnot": ["artificiel", "gazeux", "inerte", "liquide"], + "na": ["liquide"] + }, + "FUSÉE": { + "is": ["artificiel", "inerte", "solide"], + "isnot": ["gazeux", "liquide", "naturel", "vivant"], + "na": [] + }, + "GOURDE": { + "is": ["artificiel", "solide"], + "isnot": ["gazeux", "liquide", "naturel"], + "na": [] + }, + "GUÊPE": { + "is": ["naturel", "vivant"], + "isnot": ["artificiel", "inerte"], + "na": ["liquide"] + }, + "HÉLICOPTÈRE": { + "is": ["artificiel", "inerte"], + "isnot": ["gazeux", "naturel", "vivant"], + "na": [] + }, + "KANGOUROU": { + "is": ["naturel", "solide", "vivant"], + "isnot": ["artificiel", "gazeux", "inerte", "liquide"], + "na": [] + }, + "LAMPE": { + "is": ["artificiel", "inerte", "solide"], + "isnot": ["gazeux", "liquide", "naturel", "vivant"], + "na": [] + }, + "LICORNE": { + "is": ["naturel", "solide", "vivant"], + "isnot": ["artificiel", "gazeux", "inerte", "liquide"], + "na": ["naturel"] + }, + "LIVRE": { + "is": ["artificiel"], + "isnot": ["liquide", "naturel"], + "na": [] + }, + "MAISON": { + "is": ["artificiel", "inerte", "solide"], + "isnot": ["gazeux", "liquide", "naturel", "vivant"], + "na": [] + }, + "MOUCHE": { + "is": ["naturel"], + "isnot": ["artificiel"], + "na": ["liquide"] + }, + "NOEUD": { + "is": ["inerte"], + "isnot": ["liquide", "vivant"], + "na": ["artificiel", "solide"] + }, + "ORAGE": { + "is": ["inerte", "naturel"], + "isnot": ["artificiel", "vivant"], + "na": ["solide"] + }, + "OURS": { + "is": ["naturel", "vivant"], + "isnot": ["artificiel", "gazeux", "inerte"], + "na": ["liquide"] + }, + "PAPILLON": { + "is": ["naturel", "vivant"], + "isnot": ["artificiel", "gazeux", "inerte"], + "na": ["solide"] + }, + "PEIGNE": { + "is": ["artificiel", "inerte", "solide"], + "isnot": ["gazeux", "liquide", "naturel", "vivant"], + "na": [] + }, + "PENDULE": { + "is": ["artificiel", "inerte", "solide"], + "isnot": ["gazeux", "liquide", "naturel", "vivant"], + "na": [] + }, + "PIANO": { + "is": ["artificiel", "inerte", "solide"], + "isnot": ["gazeux", "liquide", "naturel", "vivant"], + "na": [] + }, + "PIZZA": { + "is": ["artificiel", "inerte"], + "isnot": ["gazeux", "liquide", "naturel", "vivant"], + "na": [] + }, + "POMME": { + "is": ["naturel", "solide", "vivant"], + "isnot": ["artificiel", "gazeux", "inerte", "liquide"], + "na": [] + }, + "POULET": { + "is": ["naturel", "vivant"], + "isnot": ["artificiel", "gazeux", "inerte"], + "na": ["liquide", "solide"] + }, + "PUZZLE": { + "is": ["artificiel", "inerte"], + "isnot": ["gazeux", "liquide", "naturel", "vivant"], + "na": [] + }, + "PÊCHE": { + "is": ["naturel", "vivant"], + "isnot": ["artificiel", "gazeux", "inerte", "liquide"], + "na": [] + }, + "REQUIN": { + "is": ["naturel", "vivant"], + "isnot": ["artificiel", "inerte"], + "na": [] + }, + "ROSE": { + "is": ["naturel", "solide", "vivant"], + "isnot": ["artificiel", "gazeux", "inerte", "liquide"], + "na": [] + }, + "SAC À DOS": { + "is": ["artificiel", "inerte", "solide"], + "isnot": ["gazeux", "liquide", "naturel", "vivant"], + "na": [] + }, + "SANGLIER": { + "is": ["naturel"], + "isnot": ["artificiel", "gazeux"], + "na": ["solide"] + }, + "SAVON": { + "is": ["artificiel", "inerte"], + "isnot": ["naturel", "vivant"], + "na": ["liquide", "solide"] + }, + "SINGE": { + "is": ["naturel", "vivant"], + "isnot": ["artificiel", "inerte"], + "na": [] + }, + "STADE": { + "is": ["artificiel", "inerte", "solide"], + "isnot": ["gazeux", "liquide", "naturel", "vivant"], + "na": [] + }, + "TABOURET": { + "is": ["artificiel", "inerte", "solide"], + "isnot": ["gazeux", "liquide", "naturel", "vivant"], + "na": [] + }, + "TARTINE": { + "is": ["artificiel", "inerte", "solide"], + "isnot": ["gazeux", "liquide", "naturel", "vivant"], + "na": [] + }, + "TIROIR": { + "is": ["artificiel", "inerte", "solide"], + "isnot": ["gazeux", "liquide", "naturel", "vivant"], + "na": [] + }, + "TORTUE": { + "is": ["naturel", "solide", "vivant"], + "isnot": ["artificiel", "gazeux", "inerte", "liquide"], + "na": ["liquide"] + }, + "TROMPETTE": { + "is": ["inerte", "solide"], + "isnot": ["gazeux", "liquide", "vivant"], + "na": [] + }, + "VACHE": { + "is": ["naturel", "solide", "vivant"], + "isnot": ["artificiel", "gazeux", "inerte", "liquide"], + "na": ["liquide"] + }, + "VOILIER": { + "is": ["artificiel", "inerte", "solide"], + "isnot": ["gazeux", "liquide", "naturel", "vivant"], + "na": [] + }, + "ZÈBRE": { + "is": ["naturel", "vivant"], + "isnot": ["artificiel", "gazeux", "inerte"], + "na": ["solide"] + } + } + } + }; +} diff --git a/lib/main.dart b/lib/main.dart index 19b7ded76a61c82c9de189319de1bf5df982151b..3d37ac60e06f202a1ec314397fd043bc84b6446d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,31 +1,64 @@ +import 'dart:io'; + +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hive/hive.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; +import 'package:path_provider/path_provider.dart'; + +import 'package:sortgame/config/theme.dart'; +import 'package:sortgame/cubit/game_cubit.dart'; +import 'package:sortgame/cubit/settings_game_cubit.dart'; +import 'package:sortgame/cubit/settings_global_cubit.dart'; +import 'package:sortgame/ui/skeleton.dart'; -import 'provider/data.dart'; -import 'screens/home.dart'; +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, + ); -void main() => runApp(const MyApp()); + 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 { const MyApp({super.key}); @override Widget build(BuildContext context) { - return ChangeNotifierProvider( - create: (BuildContext context) => Data(), - child: Consumer<Data>(builder: (context, data, child) { - return MaterialApp( - debugShowCheckedModeBanner: false, - theme: ThemeData( - primaryColor: Colors.blue, - visualDensity: VisualDensity.adaptivePlatformDensity, - ), - home: const Home(), - routes: { - Home.id: (context) => const Home(), - }, - ); - }), + return MultiBlocProvider( + providers: [ + BlocProvider<GameCubit>(create: (context) => GameCubit()), + BlocProvider<GlobalSettingsCubit>(create: (context) => GlobalSettingsCubit()), + BlocProvider<GameSettingsCubit>(create: (context) => GameSettingsCubit()), + ], + child: MaterialApp( + title: 'SortGame', + theme: appTheme, + home: const SkeletonScreen(), + + // Localization stuff + localizationsDelegates: context.localizationDelegates, + supportedLocales: context.supportedLocales, + locale: context.locale, + debugShowCheckedModeBanner: false, + ), ); } } diff --git a/lib/models/data/category.dart b/lib/models/data/category.dart new file mode 100644 index 0000000000000000000000000000000000000000..9e561ecf1da7c9b7a417c58027e316f69b3ba25f --- /dev/null +++ b/lib/models/data/category.dart @@ -0,0 +1,21 @@ +class Category { + final String key; + final String text; + + const Category({ + required this.key, + required this.text, + }); + + Map<String, dynamic> toJson() { + return { + 'key': key, + 'text': text, + }; + } + + @override + String toString() { + return toJson().toString(); + } +} diff --git a/lib/models/data/game_item.dart b/lib/models/data/game_item.dart new file mode 100644 index 0000000000000000000000000000000000000000..f1c8cc3075e4363831920fa2842750a4772a1819 --- /dev/null +++ b/lib/models/data/game_item.dart @@ -0,0 +1,27 @@ +import 'package:sortgame/models/data/category.dart'; +import 'package:sortgame/models/data/item.dart'; + +class GameItem { + final Item item; + final List<Category> isCategory; + final List<Category> isNotCategory; + + GameItem({ + required this.item, + required this.isCategory, + required this.isNotCategory, + }); + + @override + String toString() { + return '$GameItem(${toJson()})'; + } + + Map<String, dynamic>? toJson() { + return <String, dynamic>{ + 'item': item.toJson(), + 'isCategory': isCategory.toString(), + 'isNotCategory': isNotCategory.toString(), + }; + } +} diff --git a/lib/models/data/item.dart b/lib/models/data/item.dart new file mode 100644 index 0000000000000000000000000000000000000000..f22ca4aaf55727f84c08b9a16a7ab1cca5557821 --- /dev/null +++ b/lib/models/data/item.dart @@ -0,0 +1,21 @@ +class Item { + final String key; + final String text; + + const Item({ + required this.key, + required this.text, + }); + + Map<String, dynamic> toJson() { + return { + 'key': key, + 'text': text, + }; + } + + @override + String toString() { + return toJson().toString(); + } +} diff --git a/lib/models/game.dart b/lib/models/game.dart new file mode 100644 index 0000000000000000000000000000000000000000..797ac0b2bac87b3e691bc1a4ad364a0e9e5ba38f --- /dev/null +++ b/lib/models/game.dart @@ -0,0 +1,104 @@ +import 'package:sortgame/data/fetch_data_helper.dart'; +import 'package:sortgame/models/data/game_item.dart'; +import 'package:sortgame/models/settings_game.dart'; +import 'package:sortgame/models/settings_global.dart'; +import 'package:sortgame/utils/tools.dart'; + +class Game { + final List<GameItem> items; + final GameSettings gameSettings; + final GlobalSettings globalSettings; + bool isRunning = false; + bool isFinished = false; + int position = 1; + int score = 0; + + Game({ + required this.items, + required this.gameSettings, + required this.globalSettings, + this.isRunning = false, + this.isFinished = false, + this.position = 1, + this.score = 0, + }); + + factory Game.createNull() { + return Game( + items: [], + gameSettings: GameSettings.createDefault(), + globalSettings: GlobalSettings.createDefault(), + ); + } + + factory Game.createNew({ + GameSettings? gameSettings, + GlobalSettings? globalSettings, + }) { + GameSettings newGameSettings = gameSettings ?? GameSettings.createDefault(); + GlobalSettings newGlobalSettings = globalSettings ?? GlobalSettings.createDefault(); + + List<GameItem> items = FetchDataHelper().getItems(newGameSettings.itemsCount); + + return Game( + items: items, + gameSettings: newGameSettings, + globalSettings: newGlobalSettings, + isRunning: true, + ); + } + + void increaseScore(int? count) { + score += (count ?? 0); + } + + void increasePosition() { + position += 1; + } + + void updateGameIsRunning(bool gameIsRunning) { + isRunning = gameIsRunning; + } + + void updateGameIsFinished(bool gameIsFinished) { + isFinished = gameIsFinished; + } + + GameItem getCurrentQuestion() { + return items[position - 1]; + } + + void dump() { + printlog(''); + printlog('## Current game dump:'); + printlog(''); + gameSettings.dump(); + globalSettings.dump(); + printlog(''); + items.toString(); + printlog(''); + printlog('Game: '); + printlog(' isRunning: $isRunning'); + printlog(' isFinished: $isFinished'); + printlog(' position: $position'); + printlog(' score: $score'); + printlog(''); + } + + @override + String toString() { + return '$Game(${toJson()})'; + } + + Map<String, dynamic>? toJson() { + return <String, dynamic>{ + 'items': items.toString(), + 'gameSettings': gameSettings.toJson(), + 'globalSettings': globalSettings.toJson(), + 'isRunning': isRunning, + 'isFinished': isFinished, + 'position': position, + 'score': score, + }; + } +} diff --git a/lib/models/settings_game.dart b/lib/models/settings_game.dart new file mode 100644 index 0000000000000000000000000000000000000000..67681ec8b9a761986cf41cc0db681fb39f82b8e0 --- /dev/null +++ b/lib/models/settings_game.dart @@ -0,0 +1,40 @@ +import 'package:sortgame/config/default_game_settings.dart'; +import 'package:sortgame/utils/tools.dart'; + +class GameSettings { + final int itemsCount; + + GameSettings({ + required this.itemsCount, + }); + + static int getItemsCountValueFromUnsafe(int itemsCount) { + if (DefaultGameSettings.allowedItemsCountValues.contains(itemsCount)) { + return itemsCount; + } + + return DefaultGameSettings.defaultItemsCountValue; + } + + factory GameSettings.createDefault() { + return GameSettings( + itemsCount: DefaultGameSettings.defaultItemsCountValue, + ); + } + + void dump() { + printlog('Settings: '); + printlog(' itemsCount: $itemsCount'); + } + + @override + String toString() { + return '$GameSettings(${toJson()})'; + } + + Map<String, dynamic>? toJson() { + return <String, dynamic>{ + 'itemsCount': itemsCount, + }; + } +} diff --git a/lib/models/settings_global.dart b/lib/models/settings_global.dart new file mode 100644 index 0000000000000000000000000000000000000000..1da37395f09b1c71c656274c5248a2f6d0edae95 --- /dev/null +++ b/lib/models/settings_global.dart @@ -0,0 +1,22 @@ +import 'package:sortgame/utils/tools.dart'; + +class GlobalSettings { + GlobalSettings(); + + factory GlobalSettings.createDefault() { + return GlobalSettings(); + } + + void dump() { + printlog('Settings: '); + } + + @override + String toString() { + return '$GlobalSettings(${toJson()})'; + } + + Map<String, dynamic>? toJson() { + return <String, dynamic>{}; + } +} diff --git a/lib/provider/data.dart b/lib/provider/data.dart deleted file mode 100644 index 9e9148993cc5dfe304c231ae7165428d8f93c3af..0000000000000000000000000000000000000000 --- a/lib/provider/data.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:flutter/foundation.dart'; - -class Data extends ChangeNotifier { - bool _searchingImage = false; - String _image = ''; - - bool get searchingImage => _searchingImage; - - set searchingImage(bool value) { - _searchingImage = value; - notifyListeners(); - } - - String get image => _image; - - set updateImage(String value) { - _image = value; - notifyListeners(); - } - - void resetGame() { - _image = ''; - notifyListeners(); - } -} diff --git a/lib/screens/home.dart b/lib/screens/home.dart deleted file mode 100644 index 7ba9dea966562c242250a4e19f0d8e91db3508b8..0000000000000000000000000000000000000000 --- a/lib/screens/home.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:sortgame/utils/tools.dart'; - -class Home extends StatelessWidget { - const Home({super.key}); - - static const String id = 'home'; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Sorting game'), - ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: <Widget>[ - Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - margin: const EdgeInsets.all(4), - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: Colors.blue, - borderRadius: BorderRadius.circular(4), - border: Border.all( - color: Colors.green, - width: 4, - ), - ), - child: TextButton( - child: const Text( - '🎲', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 50, - fontWeight: FontWeight.w600, - color: Colors.black, - ), - ), - onPressed: () => printlog('X'), - ), - ), - ], - ), - ], - ), - ), - ); - } -} diff --git a/lib/ui/painters/parameter_painter.dart b/lib/ui/painters/parameter_painter.dart new file mode 100644 index 0000000000000000000000000000000000000000..fd4e3921d5fdecf4812a263148c4f1a3641b2c4e --- /dev/null +++ b/lib/ui/painters/parameter_painter.dart @@ -0,0 +1,149 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +import 'package:sortgame/config/default_game_settings.dart'; +import 'package:sortgame/models/settings_game.dart'; +import 'package:sortgame/models/settings_global.dart'; +import 'package:sortgame/utils/tools.dart'; + +class ParameterPainter extends CustomPainter { + const ParameterPainter({ + required this.code, + required this.value, + required this.isSelected, + required this.gameSettings, + required this.globalSettings, + }); + + final String code; + final int value; + final bool isSelected; + final GameSettings gameSettings; + final GlobalSettings globalSettings; + + @override + void paint(Canvas canvas, Size size) { + // force square + final double canvasSize = min(size.width, size.height); + + const Color borderColorEnabled = Colors.blue; + const Color borderColorDisabled = Colors.white; + + // "enabled/disabled" border + final paint = Paint(); + paint.style = PaintingStyle.stroke; + paint.color = isSelected ? borderColorEnabled : borderColorDisabled; + paint.strokeJoin = StrokeJoin.round; + paint.strokeWidth = 20 / 100 * canvasSize; + canvas.drawRect( + Rect.fromPoints(const Offset(0, 0), Offset(canvasSize, canvasSize)), paint); + + // content + switch (code) { + case 'itemsCount': + paintItemsCountParameterItem(value, canvas, canvasSize); + break; + default: + printlog('Unknown parameter: $code/$value'); + paintUnknownParameterItem(value, canvas, canvasSize); + } + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return false; + } + + // "unknown" parameter -> simple bock with text + void paintUnknownParameterItem( + final int value, + final Canvas canvas, + final double size, + ) { + final paint = Paint(); + paint.strokeJoin = StrokeJoin.round; + paint.strokeWidth = 3 / 100 * size; + + paint.color = Colors.grey; + paint.style = PaintingStyle.fill; + canvas.drawRect(Rect.fromPoints(const Offset(0, 0), Offset(size, size)), paint); + + final textSpan = TextSpan( + text: '?\n$value', + style: const TextStyle( + color: Colors.black, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ); + final textPainter = TextPainter( + text: textSpan, + textDirection: TextDirection.ltr, + ); + textPainter.layout(); + textPainter.paint( + canvas, + Offset( + (size - textPainter.width) * 0.5, + (size - textPainter.height) * 0.5, + ), + ); + } + + void paintItemsCountParameterItem( + final int value, + final Canvas canvas, + final double size, + ) { + Color backgroundColor = Colors.grey; + + switch (value) { + case DefaultGameSettings.itemsCountValueLow: + backgroundColor = Colors.green; + break; + case DefaultGameSettings.itemsCountValueMedium: + backgroundColor = Colors.orange; + break; + case DefaultGameSettings.itemsCountValueHigh: + backgroundColor = Colors.red; + break; + case DefaultGameSettings.itemsCountValueVeryHigh: + backgroundColor = Colors.purple; + break; + default: + printlog('Wrong value for itemsCount parameter value: $value'); + } + + final paint = Paint(); + paint.strokeJoin = StrokeJoin.round; + paint.strokeWidth = 3 / 100 * size; + + // Colored background + paint.color = backgroundColor; + paint.style = PaintingStyle.fill; + canvas.drawRect(Rect.fromPoints(const Offset(0, 0), Offset(size, size)), paint); + + // centered text value + final textSpan = TextSpan( + text: value.toString(), + style: TextStyle( + color: Colors.black, + fontSize: size / 4, + fontWeight: FontWeight.bold, + ), + ); + final textPainter = TextPainter( + text: textSpan, + textDirection: TextDirection.ltr, + ); + textPainter.layout(); + textPainter.paint( + canvas, + Offset( + (size - textPainter.width) * 0.5, + (size - textPainter.height) * 0.5, + ), + ); + } +} diff --git a/lib/ui/screens/screen_game.dart b/lib/ui/screens/screen_game.dart new file mode 100644 index 0000000000000000000000000000000000000000..154842b3c627266ad2e6727980a11c8e40480b27 --- /dev/null +++ b/lib/ui/screens/screen_game.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:sortgame/cubit/game_cubit.dart'; +import 'package:sortgame/models/game.dart'; +import 'package:sortgame/ui/widgets/game_bottom_buttons.dart'; +import 'package:sortgame/ui/widgets/game_question.dart'; +import 'package:sortgame/ui/widgets/game_top_indicator.dart'; + +class ScreenGame extends StatelessWidget { + const ScreenGame({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder<GameCubit, GameState>( + builder: (BuildContext context, GameState gameState) { + final Game currentGame = gameState.currentGame; + + return Container( + padding: const EdgeInsets.all(4), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 8), + const GameTopIndicatorWidget(), + const SizedBox(height: 8), + !currentGame.isFinished + ? const GameQuestionWidget() + : const GameBottomButtonsWidget(), + ], + ), + ); + }, + ); + } +} diff --git a/lib/ui/screens/screen_parameters.dart b/lib/ui/screens/screen_parameters.dart new file mode 100644 index 0000000000000000000000000000000000000000..3c43c7b1d20b335f0b5b33d534d67c03bab958a9 --- /dev/null +++ b/lib/ui/screens/screen_parameters.dart @@ -0,0 +1,159 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:sortgame/config/default_game_settings.dart'; +import 'package:sortgame/config/default_global_settings.dart'; +import 'package:sortgame/cubit/game_cubit.dart'; +import 'package:sortgame/cubit/settings_game_cubit.dart'; +import 'package:sortgame/cubit/settings_global_cubit.dart'; +import 'package:sortgame/ui/painters/parameter_painter.dart'; + +class ScreenParameters extends StatelessWidget { + const ScreenParameters({super.key}); + + final double separatorHeight = 8.0; + + @override + Widget build(BuildContext context) { + final List<Widget> lines = []; + + // Game settings + for (String code in DefaultGameSettings.availableParameters) { + lines.add(Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: buildParametersLine( + code: code, + isGlobal: false, + ), + )); + + lines.add(SizedBox(height: separatorHeight)); + } + + lines.add(SizedBox(height: separatorHeight)); + lines.add(Expanded(child: buildStartNewGameButton())); + 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<int> availableValues = isGlobal + ? DefaultGlobalSettings.getAvailableValues(code) + : DefaultGameSettings.getAvailableValues(code); + + for (int 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 int currentValue = isGlobal + ? globalSettingsCubit.getParameterValue(code) + : gameSettingsCubit.getParameterValue(code); + + final bool isActive = (value == currentValue); + + final double displayWidth = MediaQuery.of(context).size.width; + final double itemWidth = displayWidth / availableValues.length - 25; + + return TextButton( + child: Container( + margin: const EdgeInsets.all(0), + padding: const EdgeInsets.all(0), + child: CustomPaint( + size: Size(itemWidth, itemWidth), + willChange: false, + painter: ParameterPainter( + code: code, + value: value, + isSelected: isActive, + gameSettings: gameSettingsState.settings, + globalSettings: globalSettingsState.settings, + ), + isComplex: true, + ), + ), + onPressed: () => isGlobal + ? globalSettingsCubit.setParameterValue(code, value) + : gameSettingsCubit.setParameterValue(code, value), + ); + }, + ); + }, + ); + + parameterButtons.add(parameterButton); + } + + return parameterButtons; + } + + Image buildImageWidget(String imageAssetCode) { + return Image( + image: AssetImage('assets/icons/$imageAssetCode.png'), + fit: BoxFit.fill, + ); + } + + Container buildImageContainerWidget(String imageAssetCode) { + return Container( + child: buildImageWidget(imageAssetCode), + ); + } + + Column buildDecorationImageWidget() { + return Column( + children: [ + TextButton( + child: buildImageContainerWidget('placeholder'), + onPressed: () {}, + ), + ], + ); + } + + Widget buildStartNewGameButton() { + return BlocBuilder<GameSettingsCubit, GameSettingsState>( + builder: (BuildContext context, GameSettingsState gameSettingsState) { + return BlocBuilder<GlobalSettingsCubit, GlobalSettingsState>( + builder: (BuildContext context, GlobalSettingsState globalSettingsState) { + final GameCubit gameCubit = BlocProvider.of<GameCubit>(context); + + return TextButton( + child: buildImageContainerWidget('button_start'), + onPressed: () => gameCubit.startNewGame( + gameSettings: gameSettingsState.settings, + globalSettings: globalSettingsState.settings, + ), + ); + }, + ); + }, + ); + } +} diff --git a/lib/ui/skeleton.dart b/lib/ui/skeleton.dart new file mode 100644 index 0000000000000000000000000000000000000000..ecf652e45d5563f8e7f6d64c88723594f2784e86 --- /dev/null +++ b/lib/ui/skeleton.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:sortgame/cubit/game_cubit.dart'; +import 'package:sortgame/ui/screens/screen_game.dart'; +import 'package:sortgame/ui/screens/screen_parameters.dart'; +import 'package:sortgame/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.background, + child: BlocBuilder<GameCubit, GameState>( + builder: (BuildContext context, GameState gameState) { + return gameState.currentGame.isRunning + ? const ScreenGame() + : const ScreenParameters(); + }, + ), + ), + backgroundColor: Theme.of(context).colorScheme.background, + ); + } +} diff --git a/lib/ui/widgets/game_bottom_buttons.dart b/lib/ui/widgets/game_bottom_buttons.dart new file mode 100644 index 0000000000000000000000000000000000000000..2af6424a0a7633602a6ba6b3ba53db9877ff4017 --- /dev/null +++ b/lib/ui/widgets/game_bottom_buttons.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:sortgame/cubit/game_cubit.dart'; + +class GameBottomButtonsWidget extends StatelessWidget { + const GameBottomButtonsWidget({super.key}); + + @override + Widget build(BuildContext context) { + const String decorationImageAssetName = 'assets/icons/placeholder.png'; + + const Widget decorationWidget = TextButton( + onPressed: null, + child: Image( + image: AssetImage(decorationImageAssetName), + fit: BoxFit.fill, + ), + ); + + return Table( + defaultColumnWidth: const IntrinsicColumnWidth(), + children: [ + TableRow( + children: [ + const Column( + children: [decorationWidget], + ), + Column( + children: [ + TextButton( + child: const Image( + image: AssetImage('assets/icons/button_back.png'), + fit: BoxFit.fill, + ), + onPressed: () { + final GameCubit gameCubit = BlocProvider.of<GameCubit>(context); + gameCubit.quitGame(); + }, + ) + ], + ), + const Column( + children: [decorationWidget], + ), + ], + ), + ], + ); + } +} diff --git a/lib/ui/widgets/game_question.dart b/lib/ui/widgets/game_question.dart new file mode 100644 index 0000000000000000000000000000000000000000..83153dc46dc7111b58321611d52d73ff7cb1d6fa --- /dev/null +++ b/lib/ui/widgets/game_question.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:sortgame/cubit/game_cubit.dart'; +import 'package:sortgame/models/data/game_item.dart'; +import 'package:sortgame/models/game.dart'; +import 'package:sortgame/ui/widgets/games/buttons_yes_no.dart'; +import 'package:sortgame/ui/widgets/helpers/outlined_text_widget.dart'; + +class GameQuestionWidget extends StatelessWidget { + const GameQuestionWidget({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder<GameCubit, GameState>( + builder: (BuildContext context, GameState gameState) { + final Game currentGame = gameState.currentGame; + + final GameItem currentQuestion = currentGame.getCurrentQuestion(); + + return Column( + children: [ + OutlinedText( + text: currentQuestion.item.text, + fontSize: 50, + textColor: Theme.of(context).colorScheme.onSurface, + ), + Container( + padding: const EdgeInsets.all(10), + margin: const EdgeInsets.all(20), + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.surface, + width: 8, + ), + borderRadius: const BorderRadius.all(Radius.circular(20)), + color: Theme.of(context).colorScheme.inversePrimary, + ), + child: GameButtonsYesNo(question: currentQuestion), + ), + ], + ); + }, + ); + } +} diff --git a/lib/ui/widgets/game_top_indicator.dart b/lib/ui/widgets/game_top_indicator.dart new file mode 100644 index 0000000000000000000000000000000000000000..6ff87554a7395e946d12278cda6a6160c74e080e --- /dev/null +++ b/lib/ui/widgets/game_top_indicator.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:sortgame/cubit/game_cubit.dart'; +import 'package:sortgame/models/game.dart'; +import 'package:sortgame/ui/widgets/indicators/indicator_position.dart'; +import 'package:sortgame/ui/widgets/indicators/indicator_score.dart'; + +class GameTopIndicatorWidget extends StatelessWidget { + const GameTopIndicatorWidget({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder<GameCubit, GameState>( + builder: (BuildContext context, GameState gameState) { + final Game currentGame = gameState.currentGame; + + return Column( + children: [ + PositionIndicator(game: currentGame), + ScoreIndicator(game: currentGame), + ], + ); + }, + ); + } +} diff --git a/lib/ui/widgets/games/buttons_yes_no.dart b/lib/ui/widgets/games/buttons_yes_no.dart new file mode 100644 index 0000000000000000000000000000000000000000..44f92806d1f0990a2a21e9c89cff22182fa4ed5a --- /dev/null +++ b/lib/ui/widgets/games/buttons_yes_no.dart @@ -0,0 +1,68 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:unicons/unicons.dart'; + +import 'package:sortgame/cubit/game_cubit.dart'; +import 'package:sortgame/models/data/category.dart'; +import 'package:sortgame/models/data/game_item.dart'; +import 'package:sortgame/ui/widgets/helpers/outlined_text_widget.dart'; + +class GameButtonsYesNo extends StatelessWidget { + const GameButtonsYesNo({super.key, required this.question}); + + final GameItem question; + + @override + Widget build(BuildContext context) { + final GameCubit gameCubit = BlocProvider.of<GameCubit>(context); + + final bool pickInIsCategory = Random().nextBool(); + + final List<Category> categories = + pickInIsCategory ? question.isCategory : question.isNotCategory; + + categories.shuffle(); + final Category category = categories.first; + + final List<Widget> buttons = [ + IconButton( + color: Theme.of(context).colorScheme.onSurface, + iconSize: 80, + onPressed: () { + if (pickInIsCategory) { + gameCubit.increaseScore(1); + } + gameCubit.increasePosition(); + }, + icon: const Icon(UniconsLine.thumbs_up), + ), + IconButton( + color: Theme.of(context).colorScheme.onSurface, + iconSize: 80, + onPressed: () { + if (!pickInIsCategory) { + gameCubit.increaseScore(1); + } + gameCubit.increasePosition(); + }, + icon: const Icon(UniconsLine.thumbs_down), + ), + ]; + + return Column( + children: [ + OutlinedText( + text: category.text, + fontSize: 40, + textColor: Theme.of(context).colorScheme.onSurface, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: buttons, + ) + ], + ); + } +} diff --git a/lib/ui/widgets/global_app_bar.dart b/lib/ui/widgets/global_app_bar.dart new file mode 100644 index 0000000000000000000000000000000000000000..02ff38f5b214d1239b144fedfe916e534fbb48d0 --- /dev/null +++ b/lib/ui/widgets/global_app_bar.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:sortgame/cubit/game_cubit.dart'; +import 'package:sortgame/models/game.dart'; +import 'package:sortgame/ui/widgets/helpers/app_titles.dart'; + +class GlobalAppBar extends StatelessWidget implements PreferredSizeWidget { + const GlobalAppBar({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder<GameCubit, GameState>( + builder: (BuildContext context, GameState gameState) { + final Game currentGame = gameState.currentGame; + + final List<Widget> menuActions = []; + + if (currentGame.isRunning) { + menuActions.add(TextButton( + child: const Image( + image: AssetImage('assets/icons/button_back.png'), + fit: BoxFit.fill, + ), + onPressed: () {}, + onLongPress: () { + final GameCubit gameCubit = BlocProvider.of<GameCubit>(context); + gameCubit.quitGame(); + }, + )); + } + + return AppBar( + title: const AppTitle(text: 'app_name'), + actions: menuActions, + ); + }, + ); + } + + @override + Size get preferredSize => const Size.fromHeight(50); +} diff --git a/lib/ui/widgets/helpers/app_titles.dart b/lib/ui/widgets/helpers/app_titles.dart new file mode 100644 index 0000000000000000000000000000000000000000..7cbbb2030419047b3dcf093a2195a498bd8e8ce9 --- /dev/null +++ b/lib/ui/widgets/helpers/app_titles.dart @@ -0,0 +1,17 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; + +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.headlineLarge!.apply(fontWeightDelta: 2), + ); + } +} diff --git a/lib/ui/widgets/helpers/outlined_text_widget.dart b/lib/ui/widgets/helpers/outlined_text_widget.dart new file mode 100644 index 0000000000000000000000000000000000000000..bdf29f25e24105c2d7137b8d9cffd421ad9c0f46 --- /dev/null +++ b/lib/ui/widgets/helpers/outlined_text_widget.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; + +import 'package:sortgame/utils/color_extensions.dart'; + +class OutlinedText extends StatelessWidget { + const OutlinedText({ + super.key, + required this.text, + required this.fontSize, + required this.textColor, + this.outlineColor, + }); + + final String text; + final double fontSize; + final Color textColor; + final Color? outlineColor; + + @override + Widget build(BuildContext context) { + final double delta = fontSize / 35; + + return Text( + text, + style: TextStyle( + inherit: true, + fontSize: fontSize, + fontWeight: FontWeight.w600, + color: textColor, + shadows: [ + Shadow( + offset: Offset(-delta, -delta), + color: outlineColor ?? textColor.darken(), + ), + Shadow( + offset: Offset(delta, -delta), + color: outlineColor ?? textColor.darken(), + ), + Shadow( + offset: Offset(delta, delta), + color: outlineColor ?? textColor.darken(), + ), + Shadow( + offset: Offset(-delta, delta), + color: outlineColor ?? textColor.darken(), + ), + ], + ), + ); + } +} diff --git a/lib/ui/widgets/indicators/indicator_position.dart b/lib/ui/widgets/indicators/indicator_position.dart new file mode 100644 index 0000000000000000000000000000000000000000..335cad77e235f59b0363cf2fe3f28dc016fdfba5 --- /dev/null +++ b/lib/ui/widgets/indicators/indicator_position.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; + +import 'package:sortgame/models/game.dart'; +import 'package:sortgame/ui/widgets/helpers/outlined_text_widget.dart'; +import 'package:sortgame/utils/color_extensions.dart'; + +class PositionIndicator extends StatelessWidget { + const PositionIndicator({super.key, required this.game}); + + final Game game; + + @override + Widget build(BuildContext context) { + // Normalized [0..1] value + final double barValue = game.position / game.gameSettings.itemsCount; + + const Color baseColor = Color.fromARGB(255, 215, 1, 133); + + const barHeight = 40.0; + const Color textColor = Color.fromARGB(255, 238, 238, 238); + const Color outlineColor = Color.fromARGB(255, 200, 200, 200); + + return Stack( + alignment: Alignment.center, + children: [ + LinearProgressIndicator( + value: barValue, + color: baseColor, + backgroundColor: baseColor.darken(), + minHeight: barHeight, + borderRadius: const BorderRadius.all(Radius.circular(barHeight / 4)), + ), + OutlinedText( + text: '${game.position}/${game.gameSettings.itemsCount}', + fontSize: 0.9 * barHeight, + textColor: textColor, + outlineColor: outlineColor, + ), + ], + ); + } +} diff --git a/lib/ui/widgets/indicators/indicator_score.dart b/lib/ui/widgets/indicators/indicator_score.dart new file mode 100644 index 0000000000000000000000000000000000000000..2ab1774f25e476312a5b6b9683ebd531299e6273 --- /dev/null +++ b/lib/ui/widgets/indicators/indicator_score.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; + +import 'package:sortgame/models/game.dart'; +import 'package:sortgame/ui/widgets/helpers/outlined_text_widget.dart'; + +class ScoreIndicator extends StatelessWidget { + const ScoreIndicator({super.key, required this.game}); + + final Game game; + + @override + Widget build(BuildContext context) { + const Color baseColor = Color.fromARGB(255, 121, 93, 246); + + return OutlinedText( + text: game.score.toString(), + fontSize: 70, + textColor: baseColor, + ); + } +} 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 b15ebfbb8fc4b11d61a5733db2b95e116d18c681..e2bc2cc1cc9e169f5a1b8dde16dff652ac4d8d4c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,22 @@ # 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" + bloc: + dependency: transitive + description: + name: bloc + sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" + url: "https://pub.dev" + source: hosted + version: "8.1.4" characters: dependency: transitive description: @@ -9,6 +25,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,19 +41,109 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + easy_localization: + dependency: "direct main" + description: + name: easy_localization + sha256: c145aeb6584aedc7c862ab8c737c3277788f47488bfdf9bae0fe112bd0a4789c + url: "https://pub.dev" + source: hosted + version: "3.0.5" + easy_logger: + dependency: transitive + description: + name: easy_logger + sha256: c764a6e024846f33405a2342caf91c62e357c24b02c04dbc712ef232bf30ffb7 + url: "https://pub.dev" + source: hosted + version: "0.0.2" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" + ffi: + dependency: transitive + description: + name: ffi + sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_bloc: + dependency: "direct main" + description: + name: flutter_bloc + sha256: f0ecf6e6eb955193ca60af2d5ca39565a86b8a142452c5b24d96fb477428f4d2 + url: "https://pub.dev" + source: hosted + version: "8.1.5" flutter_lints: dependency: "direct dev" description: name: flutter_lints - sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7 + sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" + 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" + 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: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + url: "https://pub.dev" + source: hosted + version: "0.18.1" lints: dependency: transitive description: @@ -62,19 +176,171 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" - provider: + 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: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + url: "https://pub.dev" + source: hosted + version: "2.1.3" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d + url: "https://pub.dev" + source: hosted + version: "2.2.4" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + path_provider_linux: + dependency: transitive + description: + 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: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + platform: + dependency: transitive + description: + name: platform + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + 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: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 + url: "https://pub.dev" + source: hosted + version: "2.2.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" + url: "https://pub.dev" + source: hosted + version: "2.3.5" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + url: "https://pub.dev" + source: hosted + version: "2.3.2" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.99" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" + url: "https://pub.dev" + source: hosted + version: "3.1.0+1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + unicons: + dependency: "direct main" + description: + name: unicons + sha256: dbfcf93ff4d4ea19b324113857e358e4882115ab85db04417a4ba1c72b17a670 + url: "https://pub.dev" + source: hosted + version: "2.1.1" vector_math: dependency: transitive description: @@ -83,6 +349,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + web: + dependency: transitive + description: + name: web + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + url: "https://pub.dev" + source: hosted + version: "0.5.1" + win32: + dependency: transitive + description: + name: win32 + sha256: "0a989dc7ca2bb51eac91e8fd00851297cfffd641aa7538b165c62637ca0eaa4a" + url: "https://pub.dev" + source: hosted + version: "5.4.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + url: "https://pub.dev" + source: hosted + version: "1.0.4" sdks: - dart: ">=3.2.0-0 <4.0.0" - flutter: ">=1.16.0" + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.19.0" diff --git a/pubspec.yaml b/pubspec.yaml index 9de92ceae6e2132cf07b41ead441a8bfcde88910..bc808043a9ba69f21af5f7e701e5157e2d434bd6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,9 @@ name: sortgame description: A sorting game application. + publish_to: 'none' -version: 0.0.17+17 + +version: 0.0.18+18 environment: sdk: '^3.0.0' @@ -9,12 +11,31 @@ environment: dependencies: flutter: sdk: flutter - provider: ^6.0.5 + easy_localization: ^3.0.1 + equatable: ^2.0.5 + flutter_bloc: ^8.1.1 + hive: ^2.2.3 + hydrated_bloc: ^9.0.0 + path_provider: ^2.0.11 + unicons: ^2.1.1 dev_dependencies: flutter_lints: ^3.0.1 flutter: - uses-material-design: true + uses-material-design: false assets: - - assets/files/ + - assets/icons/ + - assets/translations/ + + fonts: + - family: Nunito + fonts: + - asset: assets/fonts/Nunito-Bold.ttf + weight: 700 + - asset: assets/fonts/Nunito-Medium.ttf + weight: 500 + - asset: assets/fonts/Nunito-Regular.ttf + weight: 400 + - asset: assets/fonts/Nunito-Light.ttf + weight: 300