From f7fe85a2eb8aa4685ab50a73121dfac9a6b3fdf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Harrault?= <benoit@harrault.fr> Date: Wed, 15 May 2024 15:45:32 +0200 Subject: [PATCH] Normalize game architecture --- android/gradle.properties | 4 +- assets/translations/en.json | 10 +- assets/translations/fr.json | 10 +- assets/{icons => ui}/button_back.png | Bin assets/ui/button_delete_saved_game.png | Bin 0 -> 5813 bytes assets/ui/button_resume_game.png | Bin 0 -> 3659 bytes assets/{icons => ui}/button_start.png | Bin assets/ui/game_fail.png | Bin 0 -> 3647 bytes assets/ui/game_win.png | Bin 0 -> 7937 bytes assets/{icons => ui}/placeholder.png | Bin .../metadata/android/en-US/changelogs/27.txt | 1 + .../metadata/android/fr-FR/changelogs/27.txt | 1 + icons/build_game_icons.sh | 78 ------------- lib/config/default_game_settings.dart | 36 +++--- lib/config/default_global_settings.dart | 27 ++++- lib/config/menu.dart | 9 +- lib/config/theme.dart | 10 +- lib/cubit/game_cubit.dart | 36 ++++-- lib/cubit/game_state.dart | 4 - lib/cubit/settings_game_cubit.dart | 46 ++++---- lib/cubit/settings_game_state.dart | 4 - lib/cubit/settings_global_cubit.dart | 42 ++++--- lib/cubit/settings_global_state.dart | 4 - lib/data/fetch_data_helper.dart | 28 +++-- lib/main.dart | 49 ++++++-- lib/models/{ => game}/game.dart | 89 +++++++++----- lib/models/{ => settings}/settings_game.dart | 28 ++--- lib/models/settings/settings_global.dart | 41 +++++++ lib/models/settings_global.dart | 22 ---- lib/ui/game/game_end.dart | 52 +++++++++ lib/ui/game/game_top.dart | 18 +++ lib/ui/helpers/app_titles.dart | 32 +++++ .../helpers/outlined_text_widget.dart | 0 lib/ui/layouts/game_layout.dart | 38 ++++++ .../parameters_layout.dart} | 79 +++++++++---- lib/ui/parameters/parameter_image.dart | 38 ++++++ .../parameter_painter.dart | 27 +++-- lib/ui/screens/page_about.dart | 52 +++++---- lib/ui/screens/page_game.dart | 16 ++- lib/ui/screens/page_settings.dart | 26 +++-- .../{widgets => }/settings/settings_form.dart | 2 +- lib/ui/{widgets => }/settings/theme_card.dart | 0 lib/ui/skeleton.dart | 4 +- .../actions/button_delete_saved_game.dart | 21 ++++ lib/ui/widgets/actions/button_game_quit.dart | 21 ++++ .../{ => actions}/button_game_start_new.dart | 14 +-- .../actions/button_resume_saved_game.dart | 21 ++++ lib/ui/widgets/game/game.dart | 38 ------ lib/ui/widgets/game/game_board.dart | 25 ++++ lib/ui/widgets/game/game_bottom_buttons.dart | 51 -------- lib/ui/widgets/game/game_question.dart | 4 +- lib/ui/widgets/game/game_top_indicator.dart | 27 ----- lib/ui/widgets/game/indicator_position.dart | 42 ------- lib/ui/widgets/game/indicator_score.dart | 21 ---- lib/ui/widgets/global_app_bar.dart | 13 +-- lib/ui/widgets/helpers/app_header.dart | 24 ---- lib/ui/widgets/helpers/app_title.dart | 17 --- .../indicators/indicator_position.dart | 49 ++++++++ .../widgets/indicators/indicator_score.dart | 27 +++++ pubspec.lock | 44 +++---- pubspec.yaml | 11 +- .../app/build_application_resources.sh | 2 +- {icons => resources/app}/featureGraphic.svg | 0 {icons => resources/app}/icon.svg | 0 resources/build_resources.sh | 7 ++ {scripts => resources/data}/data.json | 0 {scripts => resources/data}/manage_data.php | 0 {scripts => resources/data}/manage_data.sh | 0 resources/ui/build_ui_resources.sh | 110 ++++++++++++++++++ .../ui/images}/button_back.svg | 0 .../ui/images/button_delete_saved_game.svg | 2 + resources/ui/images/button_resume_game.svg | 2 + .../ui/images}/button_start.svg | 0 resources/ui/images/game_fail.svg | 2 + resources/ui/images/game_win.svg | 2 + .../ui/images}/placeholder.svg | 0 76 files changed, 957 insertions(+), 603 deletions(-) rename assets/{icons => ui}/button_back.png (100%) create mode 100644 assets/ui/button_delete_saved_game.png create mode 100644 assets/ui/button_resume_game.png rename assets/{icons => ui}/button_start.png (100%) create mode 100644 assets/ui/game_fail.png create mode 100644 assets/ui/game_win.png rename assets/{icons => ui}/placeholder.png (100%) create mode 100644 fastlane/metadata/android/en-US/changelogs/27.txt create mode 100644 fastlane/metadata/android/fr-FR/changelogs/27.txt delete mode 100755 icons/build_game_icons.sh rename lib/models/{ => game}/game.dart (53%) rename lib/models/{ => settings}/settings_game.dart (51%) create mode 100644 lib/models/settings/settings_global.dart delete mode 100644 lib/models/settings_global.dart create mode 100644 lib/ui/game/game_end.dart create mode 100644 lib/ui/game/game_top.dart create mode 100644 lib/ui/helpers/app_titles.dart rename lib/ui/{widgets => }/helpers/outlined_text_widget.dart (100%) create mode 100644 lib/ui/layouts/game_layout.dart rename lib/ui/{widgets/parameters.dart => layouts/parameters_layout.dart} (56%) create mode 100644 lib/ui/parameters/parameter_image.dart rename lib/ui/{painters => parameters}/parameter_painter.dart (89%) rename lib/ui/{widgets => }/settings/settings_form.dart (96%) rename lib/ui/{widgets => }/settings/theme_card.dart (100%) create mode 100644 lib/ui/widgets/actions/button_delete_saved_game.dart create mode 100644 lib/ui/widgets/actions/button_game_quit.dart rename lib/ui/widgets/{ => actions}/button_game_start_new.dart (72%) create mode 100644 lib/ui/widgets/actions/button_resume_saved_game.dart delete mode 100644 lib/ui/widgets/game/game.dart create mode 100644 lib/ui/widgets/game/game_board.dart delete mode 100644 lib/ui/widgets/game/game_bottom_buttons.dart delete mode 100644 lib/ui/widgets/game/game_top_indicator.dart delete mode 100644 lib/ui/widgets/game/indicator_position.dart delete mode 100644 lib/ui/widgets/game/indicator_score.dart delete mode 100644 lib/ui/widgets/helpers/app_header.dart delete mode 100644 lib/ui/widgets/helpers/app_title.dart create mode 100644 lib/ui/widgets/indicators/indicator_position.dart create mode 100644 lib/ui/widgets/indicators/indicator_score.dart rename icons/build_application_icons.sh => resources/app/build_application_resources.sh (98%) rename {icons => resources/app}/featureGraphic.svg (100%) rename {icons => resources/app}/icon.svg (100%) create mode 100755 resources/build_resources.sh rename {scripts => resources/data}/data.json (100%) rename {scripts => resources/data}/manage_data.php (100%) rename {scripts => resources/data}/manage_data.sh (100%) create mode 100755 resources/ui/build_ui_resources.sh rename {icons => resources/ui/images}/button_back.svg (100%) create mode 100644 resources/ui/images/button_delete_saved_game.svg create mode 100644 resources/ui/images/button_resume_game.svg rename {icons => resources/ui/images}/button_start.svg (100%) create mode 100644 resources/ui/images/game_fail.svg create mode 100644 resources/ui/images/game_win.svg rename {icons => resources/ui/images}/placeholder.svg (100%) diff --git a/android/gradle.properties b/android/gradle.properties index f0fcf6c..c7c59fb 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.26 -app.versionCode=26 +app.versionName=0.1.0 +app.versionCode=27 diff --git a/assets/translations/en.json b/assets/translations/en.json index 5b07e64..ea0863c 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -1,14 +1,12 @@ { - "app_name": "SortGame", - - "bottom_nav_home": "Game", - "bottom_nav_settings": "Settings", - "bottom_nav_about": "About", + "app_name": "Sort Game", "settings_title": "Settings", "settings_label_theme": "Theme mode", "about_title": "About", "about_content": "Sort game, simple and classic.", - "about_version": "Version: {version}" + "about_version": "Version: {version}", + + "": "" } diff --git a/assets/translations/fr.json b/assets/translations/fr.json index 0cd4ca7..69c7164 100644 --- a/assets/translations/fr.json +++ b/assets/translations/fr.json @@ -1,14 +1,12 @@ { - "app_name": "SortGame", - - "bottom_nav_home": "Jeu", - "bottom_nav_settings": "Réglages", - "bottom_nav_about": "Infos", + "app_name": "Jeu de tri", "settings_title": "Réglages", "settings_label_theme": "Thème de couleurs", "about_title": "Informations", "about_content": "Jeu de tri.", - "about_version": "Version : {version}" + "about_version": "Version : {version}", + + "": "" } diff --git a/assets/icons/button_back.png b/assets/ui/button_back.png similarity index 100% rename from assets/icons/button_back.png rename to assets/ui/button_back.png diff --git a/assets/ui/button_delete_saved_game.png b/assets/ui/button_delete_saved_game.png new file mode 100644 index 0000000000000000000000000000000000000000..5e4f217689b11e444b7163557d7e5d68f3bbfe7d GIT binary patch literal 5813 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 literal 0 HcmV?d00001 diff --git a/assets/ui/button_resume_game.png b/assets/ui/button_resume_game.png new file mode 100644 index 0000000000000000000000000000000000000000..b2ea0a02d05e42377eb551a4b51428b511a32f5d GIT binary patch literal 3659 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 literal 0 HcmV?d00001 diff --git a/assets/icons/button_start.png b/assets/ui/button_start.png similarity index 100% rename from assets/icons/button_start.png rename to assets/ui/button_start.png diff --git a/assets/ui/game_fail.png b/assets/ui/game_fail.png new file mode 100644 index 0000000000000000000000000000000000000000..93f2801f9d6bb2ce508e1293cd64d6ff2e9970ec GIT binary patch literal 3647 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 a*}uy$dF>W!zRtkFz~JfX=d#Wzp$P!#%#zLk literal 0 HcmV?d00001 diff --git a/assets/ui/game_win.png b/assets/ui/game_win.png new file mode 100644 index 0000000000000000000000000000000000000000..876334279c1711b349a62131a33607eecf924eb6 GIT binary patch literal 7937 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 P3=9mOu6{1-oD!M<Ur^e& literal 0 HcmV?d00001 diff --git a/assets/icons/placeholder.png b/assets/ui/placeholder.png similarity index 100% rename from assets/icons/placeholder.png rename to assets/ui/placeholder.png diff --git a/fastlane/metadata/android/en-US/changelogs/27.txt b/fastlane/metadata/android/en-US/changelogs/27.txt new file mode 100644 index 0000000..d4afd51 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/27.txt @@ -0,0 +1 @@ +Improve/normalize game architecture. diff --git a/fastlane/metadata/android/fr-FR/changelogs/27.txt b/fastlane/metadata/android/fr-FR/changelogs/27.txt new file mode 100644 index 0000000..6a9871a --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/27.txt @@ -0,0 +1 @@ +Amélioration/normalisation de l'architecture du jeu. diff --git a/icons/build_game_icons.sh b/icons/build_game_icons.sh deleted file mode 100755 index 7368dc4..0000000 --- a/icons/build_game_icons.sh +++ /dev/null @@ -1,78 +0,0 @@ -#! /bin/bash - -# Check dependencies -command -v inkscape >/dev/null 2>&1 || { echo >&2 "I require inkscape but it's not installed. Aborting."; exit 1; } -command -v scour >/dev/null 2>&1 || { echo >&2 "I require scour but it's not installed. Aborting."; exit 1; } -command -v optipng >/dev/null 2>&1 || { echo >&2 "I require optipng but it's not installed. Aborting."; exit 1; } - -CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" -BASE_DIR="$(dirname "${CURRENT_DIR}")" -ASSETS_DIR="${BASE_DIR}/assets" - -OPTIPNG_OPTIONS="-preserve -quiet -o7" -ICON_SIZE=192 - -####################################################### - -# Game images -AVAILABLE_GAME_IMAGES=" - button_back - button_start - placeholder -" - -####################################################### - -# optimize svg -function optimize_svg() { - SOURCE="$1" - - cp ${SOURCE} ${SOURCE}.tmp - scour \ - --remove-descriptive-elements \ - --enable-id-stripping \ - --enable-viewboxing \ - --enable-comment-stripping \ - --nindent=4 \ - --quiet \ - -i ${SOURCE}.tmp \ - -o ${SOURCE} - rm ${SOURCE}.tmp -} - -# build icons -function build_icon() { - SOURCE="$1" - TARGET="$2" - - echo "Building ${TARGET}" - - if [ ! -f "${SOURCE}" ]; then - echo "Missing file: ${SOURCE}" - exit 1 - fi - - optimize_svg "${SOURCE}" - - inkscape \ - --export-width=${ICON_SIZE} \ - --export-height=${ICON_SIZE} \ - --export-filename=${TARGET} \ - ${SOURCE} - - optipng ${OPTIPNG_OPTIONS} ${TARGET} -} - -####################################################### - -# Create output folders -mkdir -p ${ASSETS_DIR}/icons - -# Delete existing generated images -find ${ASSETS_DIR}/icons -type f -name "*.png" -delete - -# build game images -for GAME_IMAGE in ${AVAILABLE_GAME_IMAGES} -do - build_icon ${CURRENT_DIR}/${GAME_IMAGE}.svg ${ASSETS_DIR}/icons/${GAME_IMAGE}.png -done diff --git a/lib/config/default_game_settings.dart b/lib/config/default_game_settings.dart index dd16dbf..e43bea3 100644 --- a/lib/config/default_game_settings.dart +++ b/lib/config/default_game_settings.dart @@ -4,38 +4,46 @@ import 'package:sortgame/utils/tools.dart'; class DefaultGameSettings { // available game parameters codes static const String parameterCodeItemsCount = 'itemsCount'; - static const String parameterCodeThemeIndex = 'theme'; + static const String parameterCodeThemeCode = 'themeCode'; static const List<String> availableParameters = [ parameterCodeItemsCount, - parameterCodeThemeIndex, + parameterCodeThemeCode, ]; // items count: available values - static const int itemsCountValueLow = 5; - static const int itemsCountValueMedium = 10; - static const int itemsCountValueHigh = 15; - static const int itemsCountValueVeryHigh = 20; - static const List<int> allowedItemsCountValues = [ + static const String itemsCountValueLow = '5'; + static const String itemsCountValueMedium = '10'; + static const String itemsCountValueHigh = '15'; + static const String itemsCountValueVeryHigh = '20'; + static const List<String> allowedItemsCountValues = [ itemsCountValueLow, itemsCountValueMedium, itemsCountValueHigh, itemsCountValueVeryHigh, ]; // items count: default value - static const int defaultItemsCountValue = itemsCountValueMedium; + static const String defaultItemsCountValue = itemsCountValueMedium; - static const int defaultThemeValue = 0; + // theme code: available values + static List<String> allowedThemeCodeValues = FetchDataHelper().getThemesCodes(); + // theme code: default value + static const String defaultThemeCodeValue = ''; - static List<int> getAvailableValues(String parameterCode) { + // available values from parameter code + static List<String> getAvailableValues(String parameterCode) { switch (parameterCode) { - case 'itemsCount': + case parameterCodeItemsCount: return DefaultGameSettings.allowedItemsCountValues; - case 'theme': - final int count = FetchDataHelper().getThemes().length; - return List<int>.generate(count, (i) => i); + case parameterCodeThemeCode: + return DefaultGameSettings.allowedThemeCodeValues; } printlog('Did not find any available value for game parameter "$parameterCode".'); return []; } + + // parameters displayed with assets (instead of painter) + static List<String> displayedWithAssets = [ + // + ]; } diff --git a/lib/config/default_global_settings.dart b/lib/config/default_global_settings.dart index 8b0d15c..8ec6609 100644 --- a/lib/config/default_global_settings.dart +++ b/lib/config/default_global_settings.dart @@ -1,10 +1,33 @@ import 'package:sortgame/utils/tools.dart'; class DefaultGlobalSettings { - static const List<String> availableParameters = []; + // 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; + } - static List<int> getAvailableValues(String parameterCode) { printlog('Did not find any available value for global parameter "$parameterCode".'); return []; } + + // parameters displayed with assets (instead of painter) + static List<String> displayedWithAssets = [ + // + ]; } diff --git a/lib/config/menu.dart b/lib/config/menu.dart index 674e6fe..f161e45 100644 --- a/lib/config/menu.dart +++ b/lib/config/menu.dart @@ -1,17 +1,15 @@ import 'package:flutter/material.dart'; -import 'package:unicons/unicons.dart'; - import 'package:sortgame/ui/screens/page_about.dart'; import 'package:sortgame/ui/screens/page_game.dart'; import 'package:sortgame/ui/screens/page_settings.dart'; +import 'package:unicons/unicons.dart'; + class MenuItem { - final String code; final Icon icon; final Widget page; const MenuItem({ - required this.code, required this.icon, required this.page, }); @@ -20,21 +18,18 @@ class MenuItem { class Menu { static const indexGame = 0; static const menuItemGame = MenuItem( - code: 'bottom_nav_game', icon: Icon(UniconsLine.home), page: PageGame(), ); static const indexSettings = 1; static const menuItemSettings = MenuItem( - code: 'bottom_nav_settings', icon: Icon(UniconsLine.setting), page: PageSettings(), ); static const indexAbout = 2; static const menuItemAbout = MenuItem( - code: 'bottom_nav_about', icon: Icon(UniconsLine.info_circle), page: PageAbout(), ); diff --git a/lib/config/theme.dart b/lib/config/theme.dart index be39034..74f532f 100644 --- a/lib/config/theme.dart +++ b/lib/config/theme.dart @@ -39,11 +39,9 @@ final ColorScheme lightColorScheme = ColorScheme.light( secondary: primarySwatch.shade500, onSecondary: Colors.white, error: errorColor, - background: textSwatch.shade200, - onBackground: textSwatch.shade500, onSurface: textSwatch.shade500, surface: textSwatch.shade50, - surfaceVariant: Colors.white, + surfaceContainerHighest: Colors.white, shadow: textSwatch.shade900.withOpacity(.1), ); @@ -52,11 +50,9 @@ final ColorScheme darkColorScheme = ColorScheme.dark( 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), + surfaceContainerHighest: const Color(0xFF282832), shadow: textSwatch.shade900.withOpacity(.2), ); @@ -192,5 +188,3 @@ final ThemeData darkTheme = lightTheme.copyWith( ), ), ); - -final ThemeData appTheme = darkTheme; diff --git a/lib/cubit/game_cubit.dart b/lib/cubit/game_cubit.dart index b21f2af..ec76c9a 100644 --- a/lib/cubit/game_cubit.dart +++ b/lib/cubit/game_cubit.dart @@ -2,16 +2,16 @@ 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'; +import 'package:sortgame/models/game/game.dart'; +import 'package:sortgame/models/settings/settings_game.dart'; +import 'package:sortgame/models/settings/settings_global.dart'; part 'game_state.dart'; class GameCubit extends HydratedCubit<GameState> { GameCubit() : super(GameState( - currentGame: Game.createNull(), + currentGame: Game.createEmpty(), )); void updateState(Game game) { @@ -22,14 +22,21 @@ class GameCubit extends HydratedCubit<GameState> { void refresh() { final Game game = Game( - items: state.currentGame.items, + // 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 + items: state.currentGame.items, + // Game data position: state.currentGame.position, score: state.currentGame.score, ); + // game.dump(); updateState(game); } @@ -38,7 +45,8 @@ class GameCubit extends HydratedCubit<GameState> { required GameSettings gameSettings, required GlobalSettings globalSettings, }) { - Game newGame = Game.createNew( + final Game newGame = Game.createNew( + // Settings gameSettings: gameSettings, globalSettings: globalSettings, ); @@ -50,11 +58,23 @@ class GameCubit extends HydratedCubit<GameState> { } void quitGame() { - state.currentGame.updateGameIsRunning(false); + state.currentGame.isRunning = false; + refresh(); + } + + void resumeSavedGame() { + state.currentGame.isRunning = true; + refresh(); + } + + void deleteSavedGame() { + state.currentGame.isRunning = false; + state.currentGame.isFinished = true; refresh(); } void increasePosition() { + state.currentGame.isStarted = true; if (state.currentGame.position < state.currentGame.items.length) { state.currentGame.increasePosition(); } else { @@ -70,7 +90,7 @@ class GameCubit extends HydratedCubit<GameState> { @override GameState? fromJson(Map<String, dynamic> json) { - Game currentGame = json['currentGame'] as Game; + final Game currentGame = json['currentGame'] as Game; return GameState( currentGame: currentGame, diff --git a/lib/cubit/game_state.dart b/lib/cubit/game_state.dart index 3fd161a..00e2116 100644 --- a/lib/cubit/game_state.dart +++ b/lib/cubit/game_state.dart @@ -12,8 +12,4 @@ class GameState extends Equatable { 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 index c411ae1..5278e89 100644 --- a/lib/cubit/settings_game_cubit.dart +++ b/lib/cubit/settings_game_cubit.dart @@ -2,8 +2,8 @@ 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'; +import 'package:sortgame/config/default_game_settings.dart'; +import 'package:sortgame/models/settings/settings_game.dart'; part 'settings_game_state.dart'; @@ -11,51 +11,53 @@ class GameSettingsCubit extends HydratedCubit<GameSettingsState> { GameSettingsCubit() : super(GameSettingsState(settings: GameSettings.createDefault())); void setValues({ - int? itemsCount, - int? theme, + String? itemsCount, + String? themeCode, }) { emit( GameSettingsState( settings: GameSettings( itemsCount: itemsCount ?? state.settings.itemsCount, - theme: theme ?? state.settings.theme, + themeCode: themeCode ?? state.settings.themeCode, ), ), ); } - int getParameterValue(String code) { + String getParameterValue(String code) { switch (code) { - case 'itemsCount': + case DefaultGameSettings.parameterCodeItemsCount: return GameSettings.getItemsCountValueFromUnsafe(state.settings.itemsCount); - case 'theme': - return GameSettings.getThemeValueFromUnsafe(state.settings.theme); + case DefaultGameSettings.parameterCodeThemeCode: + return GameSettings.getThemeValueFromUnsafe(state.settings.themeCode); } - return 0; - } - void setParameterValue(String code, int value) { - printlog('GameSettingsCubit.setParameterValue'); - printlog('code: $code / value: $value'); + return ''; + } - int itemsCount = code == 'itemsCount' ? value : getParameterValue('itemsCount'); - int theme = code == 'theme' ? value : getParameterValue('theme'); + void setParameterValue(String code, String value) { + final String itemsCount = code == DefaultGameSettings.parameterCodeItemsCount + ? value + : getParameterValue(DefaultGameSettings.parameterCodeItemsCount); + final String themeCode = code == DefaultGameSettings.parameterCodeThemeCode + ? value + : getParameterValue(DefaultGameSettings.parameterCodeThemeCode); setValues( itemsCount: itemsCount, - theme: theme, + themeCode: themeCode, ); } @override GameSettingsState? fromJson(Map<String, dynamic> json) { - int itemsCount = json['itemsCount'] as int; - int theme = json['theme'] as int; + final String itemsCount = json[DefaultGameSettings.parameterCodeItemsCount] as String; + final String themeCode = json[DefaultGameSettings.parameterCodeThemeCode] as String; return GameSettingsState( settings: GameSettings( itemsCount: itemsCount, - theme: theme, + themeCode: themeCode, ), ); } @@ -63,8 +65,8 @@ class GameSettingsCubit extends HydratedCubit<GameSettingsState> { @override Map<String, dynamic>? toJson(GameSettingsState state) { return <String, dynamic>{ - 'itemsCount': state.settings.itemsCount, - 'theme': state.settings.theme, + DefaultGameSettings.parameterCodeItemsCount: state.settings.itemsCount, + DefaultGameSettings.parameterCodeThemeCode: state.settings.themeCode, }; } } diff --git a/lib/cubit/settings_game_state.dart b/lib/cubit/settings_game_state.dart index b773dc6..5acd85b 100644 --- a/lib/cubit/settings_game_state.dart +++ b/lib/cubit/settings_game_state.dart @@ -12,8 +12,4 @@ class GameSettingsState extends Equatable { 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 index 36b4b20..9a404fe 100644 --- a/lib/cubit/settings_global_cubit.dart +++ b/lib/cubit/settings_global_cubit.dart @@ -2,43 +2,59 @@ 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'; +import 'package:sortgame/config/default_global_settings.dart'; +import 'package:sortgame/models/settings/settings_global.dart'; part 'settings_global_state.dart'; class GlobalSettingsCubit extends HydratedCubit<GlobalSettingsState> { GlobalSettingsCubit() : super(GlobalSettingsState(settings: GlobalSettings.createDefault())); - void setValues() { + void setValues({ + String? skin, + }) { emit( GlobalSettingsState( - settings: GlobalSettings(), + settings: GlobalSettings( + skin: skin ?? state.settings.skin, + ), ), ); } - int getParameterValue(String code) { - switch (code) {} - return 0; + String getParameterValue(String code) { + switch (code) { + case DefaultGlobalSettings.parameterCodeSkin: + return GlobalSettings.getSkinValueFromUnsafe(state.settings.skin); + } + return ''; } - void setParameterValue(String code, int value) { - printlog('GlobalSettingsCubit.setParameterValue'); - printlog('code: $code / value: $value'); + void setParameterValue(String code, String value) { + final String skin = (code == DefaultGlobalSettings.parameterCodeSkin) + ? value + : getParameterValue(DefaultGlobalSettings.parameterCodeSkin); - setValues(); + setValues( + skin: skin, + ); } @override GlobalSettingsState? fromJson(Map<String, dynamic> json) { + final String skin = json[DefaultGlobalSettings.parameterCodeSkin] as String; + return GlobalSettingsState( - settings: GlobalSettings(), + settings: GlobalSettings( + skin: skin, + ), ); } @override Map<String, dynamic>? toJson(GlobalSettingsState state) { - return <String, dynamic>{}; + return <String, dynamic>{ + DefaultGlobalSettings.parameterCodeSkin: state.settings.skin, + }; } } diff --git a/lib/cubit/settings_global_state.dart b/lib/cubit/settings_global_state.dart index 4e4fbdf..ebcddd7 100644 --- a/lib/cubit/settings_global_state.dart +++ b/lib/cubit/settings_global_state.dart @@ -12,8 +12,4 @@ class GlobalSettingsState extends Equatable { 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 index 7585817..405199e 100644 --- a/lib/data/fetch_data_helper.dart +++ b/lib/data/fetch_data_helper.dart @@ -3,7 +3,7 @@ import 'package:sortgame/models/data/category.dart'; import 'package:sortgame/models/data/game_item.dart'; import 'package:sortgame/models/data/game_theme.dart'; import 'package:sortgame/models/data/item.dart'; -import 'package:sortgame/models/settings_game.dart'; +import 'package:sortgame/models/settings/settings_game.dart'; import 'package:sortgame/utils/tools.dart'; class FetchDataHelper { @@ -15,8 +15,8 @@ class FetchDataHelper { final List<Item> _items = []; List<Item> get items => _items; - final List<GameTheme> _themes = []; - List<GameTheme> get themes => _themes; + final Map<String, GameTheme> _themes = {}; + Map<String, GameTheme> get themes => _themes; final List<GameItem> _mapping = []; @@ -52,7 +52,7 @@ class FetchDataHelper { final category = getCategory(rawElement.toString()); categories.add(category); } - _themes.add(GameTheme(code: code, categories: categories)); + _themes[code] = GameTheme(code: code, categories: categories); }); final List<dynamic> rawItems = gameData['items'] as List<dynamic>; @@ -95,14 +95,14 @@ class FetchDataHelper { init(); } - final int count = gameSettings.itemsCount; - final int theme = gameSettings.theme; + final int count = int.parse(gameSettings.itemsCount); + final String themeCode = gameSettings.themeCode; List<GameItem> items = _mapping; // Remove unwanted categories if theme is selected - if (theme != 0) { - final GameTheme gameTheme = _themes[theme]; + if (themeCode != '') { + final GameTheme gameTheme = getTheme(code: themeCode); for (GameItem item in items) { item.isCategory.removeWhere((Category category) => (!gameTheme.categories.map((Category c) => c.key).contains(category.key))); @@ -120,19 +120,23 @@ class FetchDataHelper { return items.take(count).toList(); } - List<GameTheme> getThemes() { + List<String> getThemesCodes() { if (_themes.isEmpty) { init(); } - return _themes.toList(); + return _themes.keys.toList(); } - GameTheme getTheme(int themeIndex) { + GameTheme getTheme({required String code}) { if (_themes.isEmpty) { init(); } - return _themes[themeIndex]; + return _themes[code] ?? + GameTheme( + code: '', + categories: [], + ); } } diff --git a/lib/main.dart b/lib/main.dart index 667de35..9f5c582 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,6 +2,7 @@ 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'; @@ -25,18 +26,17 @@ void main() async { storageDirectory: tmpDir, ); - runApp( - EasyLocalization( - path: 'assets/translations', - supportedLocales: const <Locale>[ - Locale('en'), - Locale('fr'), - ], - fallbackLocale: const Locale('en'), - useFallbackTranslations: true, - child: const MyApp(), - ), - ); + SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]) + .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 { @@ -44,6 +44,11 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { + final List<String> assets = getImagesAssets(); + for (String asset in assets) { + precacheImage(AssetImage(asset), context); + } + return MultiBlocProvider( providers: [ BlocProvider<NavCubit>(create: (context) => NavCubit()), @@ -73,4 +78,24 @@ class MyApp extends StatelessWidget { ), ); } + + List<String> getImagesAssets() { + final List<String> assets = []; + + final 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/models/game.dart b/lib/models/game/game.dart similarity index 53% rename from lib/models/game.dart rename to lib/models/game/game.dart index 74e9d02..bada93d 100644 --- a/lib/models/game.dart +++ b/lib/models/game/game.dart @@ -1,33 +1,53 @@ 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/models/settings/settings_game.dart'; +import 'package:sortgame/models/settings/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, + // Settings required this.gameSettings, required this.globalSettings, + + // State this.isRunning = false, + this.isStarted = false, this.isFinished = false, + this.animationInProgress = false, + + // Base data + required this.items, + + // Game data this.position = 1, this.score = 0, }); - factory Game.createNull() { + // Settings + final GameSettings gameSettings; + final GlobalSettings globalSettings; + + // State + bool isRunning; + bool isStarted; + bool isFinished; + bool animationInProgress; + + // Base data + final List<GameItem> items; + + // Game data + int position; + int score; + + factory Game.createEmpty() { return Game( - items: [], + // Settings gameSettings: GameSettings.createDefault(), globalSettings: GlobalSettings.createDefault(), + // Base data + items: [], ); } @@ -35,19 +55,26 @@ class Game { GameSettings? gameSettings, GlobalSettings? globalSettings, }) { - GameSettings newGameSettings = gameSettings ?? GameSettings.createDefault(); - GlobalSettings newGlobalSettings = globalSettings ?? GlobalSettings.createDefault(); + final GameSettings newGameSettings = gameSettings ?? GameSettings.createDefault(); + final GlobalSettings newGlobalSettings = globalSettings ?? GlobalSettings.createDefault(); - List<GameItem> items = FetchDataHelper().getItems(newGameSettings); + final List<GameItem> items = FetchDataHelper().getItems(newGameSettings); return Game( - items: items, + // Settings gameSettings: newGameSettings, globalSettings: newGlobalSettings, + // State isRunning: true, + // Base data + items: items, ); } + bool get canBeResumed => isStarted && !isFinished; + + bool get gameWon => isRunning && isStarted && isFinished; + void increaseScore(int? count) { score += (count ?? 0); } @@ -72,16 +99,20 @@ class Game { printlog(''); printlog('## Current game dump:'); printlog(''); + printlog('$Game:'); + printlog(' Settings'); gameSettings.dump(); globalSettings.dump(); - printlog(''); - items.toString(); - printlog(''); - printlog('Game: '); - printlog(' isRunning: $isRunning'); - printlog(' isFinished: $isFinished'); - printlog(' position: $position'); - printlog(' score: $score'); + printlog(' State'); + printlog(' isRunning: $isRunning'); + printlog(' isStarted: $isStarted'); + printlog(' isFinished: $isFinished'); + printlog(' animationInProgress: $animationInProgress'); + printlog(' Base data'); + printlog(' items: $items'); + printlog(' Game data'); + printlog(' position: $position'); + printlog(' score: $score'); printlog(''); } @@ -92,11 +123,17 @@ class Game { Map<String, dynamic>? toJson() { return <String, dynamic>{ - 'items': items.toString(), + // Settings 'gameSettings': gameSettings.toJson(), 'globalSettings': globalSettings.toJson(), + // State 'isRunning': isRunning, + 'isStarted': isStarted, 'isFinished': isFinished, + 'animationInProgress': animationInProgress, + // Base data + 'items': items, + // Game data 'position': position, 'score': score, }; diff --git a/lib/models/settings_game.dart b/lib/models/settings/settings_game.dart similarity index 51% rename from lib/models/settings_game.dart rename to lib/models/settings/settings_game.dart index 3528fa4..c8eac0d 100644 --- a/lib/models/settings_game.dart +++ b/lib/models/settings/settings_game.dart @@ -2,15 +2,15 @@ import 'package:sortgame/config/default_game_settings.dart'; import 'package:sortgame/utils/tools.dart'; class GameSettings { - final int itemsCount; - final int theme; + final String itemsCount; + final String themeCode; GameSettings({ required this.itemsCount, - required this.theme, + required this.themeCode, }); - static int getItemsCountValueFromUnsafe(int itemsCount) { + static String getItemsCountValueFromUnsafe(String itemsCount) { if (DefaultGameSettings.allowedItemsCountValues.contains(itemsCount)) { return itemsCount; } @@ -18,25 +18,27 @@ class GameSettings { return DefaultGameSettings.defaultItemsCountValue; } - static int getThemeValueFromUnsafe(int theme) { - if (DefaultGameSettings.getAvailableValues('theme').contains(theme)) { - return theme; + static String getThemeValueFromUnsafe(String themeCode) { + if (DefaultGameSettings.getAvailableValues(DefaultGameSettings.parameterCodeThemeCode) + .contains(themeCode)) { + return themeCode; } - return DefaultGameSettings.defaultThemeValue; + return DefaultGameSettings.defaultThemeCodeValue; } factory GameSettings.createDefault() { return GameSettings( itemsCount: DefaultGameSettings.defaultItemsCountValue, - theme: DefaultGameSettings.defaultThemeValue, + themeCode: DefaultGameSettings.defaultThemeCodeValue, ); } void dump() { - printlog('Settings: '); + printlog('$GameSettings:'); printlog(' itemsCount: $itemsCount'); - printlog(' theme: $theme'); + printlog(' themeCode: $themeCode'); + printlog(''); } @override @@ -46,8 +48,8 @@ class GameSettings { Map<String, dynamic>? toJson() { return <String, dynamic>{ - 'itemsCount': itemsCount, - 'theme': theme, + DefaultGameSettings.parameterCodeItemsCount: itemsCount, + DefaultGameSettings.parameterCodeThemeCode: themeCode, }; } } diff --git a/lib/models/settings/settings_global.dart b/lib/models/settings/settings_global.dart new file mode 100644 index 0000000..5442540 --- /dev/null +++ b/lib/models/settings/settings_global.dart @@ -0,0 +1,41 @@ +import 'package:sortgame/config/default_global_settings.dart'; +import 'package:sortgame/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/models/settings_global.dart b/lib/models/settings_global.dart deleted file mode 100644 index 1da3739..0000000 --- a/lib/models/settings_global.dart +++ /dev/null @@ -1,22 +0,0 @@ -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/ui/game/game_end.dart b/lib/ui/game/game_end.dart new file mode 100644 index 0000000..468138c --- /dev/null +++ b/lib/ui/game/game_end.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:sortgame/cubit/game_cubit.dart'; +import 'package:sortgame/models/game/game.dart'; +import 'package:sortgame/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 Image decorationImage = Image( + image: AssetImage( + currentGame.gameWon ? 'assets/ui/game_win.png' : 'assets/ui/game_fail.png'), + fit: BoxFit.fill, + ); + + return Container( + margin: const EdgeInsets.all(2), + padding: const EdgeInsets.all(2), + child: Table( + defaultColumnWidth: const IntrinsicColumnWidth(), + children: [ + TableRow( + children: [ + Column( + children: [decorationImage], + ), + Column( + children: [ + currentGame.animationInProgress == true + ? decorationImage + : const QuitGameButton() + ], + ), + Column( + children: [decorationImage], + ), + ], + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/ui/game/game_top.dart b/lib/ui/game/game_top.dart new file mode 100644 index 0000000..acaa737 --- /dev/null +++ b/lib/ui/game/game_top.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +import 'package:sortgame/ui/widgets/indicators/indicator_position.dart'; +import 'package:sortgame/ui/widgets/indicators/indicator_score.dart'; + +class GameTopWidget extends StatelessWidget { + const GameTopWidget({super.key}); + + @override + Widget build(BuildContext context) { + return const Column( + children: [ + PositionIndicator(), + ScoreIndicator(), + ], + ); + } +} diff --git a/lib/ui/helpers/app_titles.dart b/lib/ui/helpers/app_titles.dart new file mode 100644 index 0000000..b98107b --- /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/widgets/helpers/outlined_text_widget.dart b/lib/ui/helpers/outlined_text_widget.dart similarity index 100% rename from lib/ui/widgets/helpers/outlined_text_widget.dart rename to lib/ui/helpers/outlined_text_widget.dart diff --git a/lib/ui/layouts/game_layout.dart b/lib/ui/layouts/game_layout.dart new file mode 100644 index 0000000..0f3d33b --- /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:sortgame/cubit/game_cubit.dart'; +import 'package:sortgame/models/game/game.dart'; +import 'package:sortgame/ui/game/game_end.dart'; +import 'package:sortgame/ui/game/game_top.dart'; +import 'package:sortgame/ui/widgets/game/game_board.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 GameTopWidget(), + const SizedBox(height: 8), + const GameBoardWidget(), + const SizedBox(height: 8), + const Expanded(child: SizedBox.shrink()), + currentGame.isFinished ? const GameEndWidget() : const SizedBox.shrink(), + ], + ), + ); + }, + ); + } +} diff --git a/lib/ui/widgets/parameters.dart b/lib/ui/layouts/parameters_layout.dart similarity index 56% rename from lib/ui/widgets/parameters.dart rename to lib/ui/layouts/parameters_layout.dart index 800dce1..52a22be 100644 --- a/lib/ui/widgets/parameters.dart +++ b/lib/ui/layouts/parameters_layout.dart @@ -5,11 +5,16 @@ import 'package:sortgame/config/default_game_settings.dart'; import 'package:sortgame/config/default_global_settings.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'; -import 'package:sortgame/ui/widgets/button_game_start_new.dart'; +import 'package:sortgame/ui/parameters/parameter_image.dart'; +import 'package:sortgame/ui/parameters/parameter_painter.dart'; +import 'package:sortgame/ui/widgets/actions/button_delete_saved_game.dart'; +import 'package:sortgame/ui/widgets/actions/button_game_start_new.dart'; +import 'package:sortgame/ui/widgets/actions/button_resume_saved_game.dart'; -class Parameters extends StatelessWidget { - const Parameters({super.key}); +class ParametersLayout extends StatelessWidget { + const ParametersLayout({super.key, required this.canResume}); + + final bool canResume; final double separatorHeight = 8.0; @@ -31,7 +36,24 @@ class Parameters extends StatelessWidget { } lines.add(SizedBox(height: separatorHeight)); - lines.add(const Expanded(child: StartNewGameButton())); + + if (canResume == false) { + // Start new game + lines.add(const Expanded( + child: StartNewGameButton(), + )); + } else { + // Resume game + lines.add(const Expanded( + child: ResumeSavedGameButton(), + )); + // Delete saved game + lines.add(SizedBox.square( + dimension: MediaQuery.of(context).size.width / 4, + child: const DeleteSavedGameButton(), + )); + } + lines.add(SizedBox(height: separatorHeight)); // Global settings @@ -58,7 +80,7 @@ class Parameters extends StatelessWidget { }) { final List<Widget> parameterButtons = []; - final List<int> availableValues = isGlobal + final List<String> availableValues = isGlobal ? DefaultGlobalSettings.getAvailableValues(code) : DefaultGameSettings.getAvailableValues(code); @@ -66,7 +88,7 @@ class Parameters extends StatelessWidget { return []; } - for (int value in availableValues) { + for (String value in availableValues) { final Widget parameterButton = BlocBuilder<GameSettingsCubit, GameSettingsState>( builder: (BuildContext context, GameSettingsState gameSettingsState) { return BlocBuilder<GlobalSettingsCubit, GlobalSettingsState>( @@ -76,31 +98,42 @@ class Parameters extends StatelessWidget { final GlobalSettingsCubit globalSettingsCubit = BlocProvider.of<GlobalSettingsCubit>(context); - final int currentValue = isGlobal + final String currentValue = isGlobal ? globalSettingsCubit.getParameterValue(code) : gameSettingsCubit.getParameterValue(code); final bool isActive = (value == currentValue); final double displayWidth = MediaQuery.of(context).size.width; - final double itemWidth = displayWidth / availableValues.length - 30; + final double itemWidth = displayWidth / availableValues.length - 26; + + final bool displayedWithAssets = + DefaultGlobalSettings.displayedWithAssets.contains(code) || + DefaultGameSettings.displayedWithAssets.contains(code); 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, - ), + child: displayedWithAssets + ? SizedBox.square( + dimension: itemWidth, + child: ParameterImage( + code: code, + value: value, + isSelected: isActive, + ), + ) + : CustomPaint( + size: Size(itemWidth, itemWidth), + willChange: false, + painter: ParameterPainter( + code: code, + value: value, + isSelected: isActive, + gameSettings: gameSettingsState.settings, + globalSettings: globalSettingsState.settings, + ), + isComplex: true, + ), ), onPressed: () => isGlobal ? globalSettingsCubit.setParameterValue(code, value) diff --git a/lib/ui/parameters/parameter_image.dart b/lib/ui/parameters/parameter_image.dart new file mode 100644 index 0000000..fc4b576 --- /dev/null +++ b/lib/ui/parameters/parameter_image.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +class ParameterImage extends StatelessWidget { + const ParameterImage({ + super.key, + required this.code, + required this.value, + required this.isSelected, + }); + + final String code; + final String value; + final bool isSelected; + + static const Color buttonBackgroundColor = Colors.white; + static const Color buttonBorderColorActive = Colors.blue; + static const Color buttonBorderColorInactive = Colors.white; + static const double buttonBorderWidth = 8.0; + static const double buttonBorderRadius = 8.0; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: buttonBackgroundColor, + borderRadius: BorderRadius.circular(buttonBorderRadius), + border: Border.all( + color: isSelected ? buttonBorderColorActive : buttonBorderColorInactive, + width: buttonBorderWidth, + ), + ), + child: Image( + image: AssetImage('assets/ui/${code}_$value.png'), + fit: BoxFit.fill, + ), + ); + } +} diff --git a/lib/ui/painters/parameter_painter.dart b/lib/ui/parameters/parameter_painter.dart similarity index 89% rename from lib/ui/painters/parameter_painter.dart rename to lib/ui/parameters/parameter_painter.dart index 2f14e73..8aa964d 100644 --- a/lib/ui/painters/parameter_painter.dart +++ b/lib/ui/parameters/parameter_painter.dart @@ -5,8 +5,8 @@ import 'package:flutter/material.dart'; import 'package:sortgame/config/default_game_settings.dart'; import 'package:sortgame/data/fetch_data_helper.dart'; import 'package:sortgame/models/data/game_theme.dart'; -import 'package:sortgame/models/settings_game.dart'; -import 'package:sortgame/models/settings_global.dart'; +import 'package:sortgame/models/settings/settings_game.dart'; +import 'package:sortgame/models/settings/settings_global.dart'; import 'package:sortgame/utils/tools.dart'; class ParameterPainter extends CustomPainter { @@ -19,7 +19,7 @@ class ParameterPainter extends CustomPainter { }); final String code; - final int value; + final String value; final bool isSelected; final GameSettings gameSettings; final GlobalSettings globalSettings; @@ -37,16 +37,16 @@ class ParameterPainter extends CustomPainter { paint.style = PaintingStyle.stroke; paint.color = isSelected ? borderColorEnabled : borderColorDisabled; paint.strokeJoin = StrokeJoin.round; - paint.strokeWidth = 20 / 100 * canvasSize; + paint.strokeWidth = 10; canvas.drawRect( Rect.fromPoints(const Offset(0, 0), Offset(canvasSize, canvasSize)), paint); // content switch (code) { - case 'itemsCount': + case DefaultGameSettings.parameterCodeItemsCount: paintItemsCountParameterItem(value, canvas, canvasSize); break; - case 'theme': + case DefaultGameSettings.parameterCodeThemeCode: paintThemeParameterItem(value, canvas, canvasSize); break; default: @@ -62,13 +62,13 @@ class ParameterPainter extends CustomPainter { // "unknown" parameter -> simple block with text void paintUnknownParameterItem( - final int value, + final String value, final Canvas canvas, final double size, ) { final paint = Paint(); paint.strokeJoin = StrokeJoin.round; - paint.strokeWidth = 3 / 100 * size; + paint.strokeWidth = 3; paint.color = Colors.grey; paint.style = PaintingStyle.fill; @@ -85,6 +85,7 @@ class ParameterPainter extends CustomPainter { final textPainter = TextPainter( text: textSpan, textDirection: TextDirection.ltr, + textAlign: TextAlign.center, ); textPainter.layout(); textPainter.paint( @@ -97,7 +98,7 @@ class ParameterPainter extends CustomPainter { } void paintItemsCountParameterItem( - final int value, + final String value, final Canvas canvas, final double size, ) { @@ -141,6 +142,7 @@ class ParameterPainter extends CustomPainter { final textPainter = TextPainter( text: textSpan, textDirection: TextDirection.ltr, + textAlign: TextAlign.center, ); textPainter.layout(); textPainter.paint( @@ -153,18 +155,18 @@ class ParameterPainter extends CustomPainter { } void paintThemeParameterItem( - final int value, + final String value, final Canvas canvas, final double size, ) { - final GameTheme theme = FetchDataHelper().getTheme(value); + final GameTheme theme = FetchDataHelper().getTheme(code: value); final Color backgroundColor = Color((theme.code.hashCode * 0xFFFFFF).toInt()).withOpacity(1.0); final paint = Paint(); paint.strokeJoin = StrokeJoin.round; - paint.strokeWidth = 3 / 100 * size; + paint.strokeWidth = 3; // Colored background paint.color = backgroundColor; @@ -183,6 +185,7 @@ class ParameterPainter extends CustomPainter { final textPainter = TextPainter( text: textSpan, textDirection: TextDirection.ltr, + textAlign: TextAlign.center, ); textPainter.layout(); textPainter.paint( diff --git a/lib/ui/screens/page_about.dart b/lib/ui/screens/page_about.dart index ff9f710..080fcaf 100644 --- a/lib/ui/screens/page_about.dart +++ b/lib/ui/screens/page_about.dart @@ -2,36 +2,40 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:package_info_plus/package_info_plus.dart'; -import 'package:sortgame/ui/widgets/helpers/app_header.dart'; +import 'package:sortgame/ui/helpers/app_titles.dart'; class PageAbout extends StatelessWidget { const PageAbout({super.key}); @override Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - children: <Widget>[ - const AppHeader(text: 'about_title'), - const Text('about_content').tr(), - FutureBuilder<PackageInfo>( - future: PackageInfo.fromPlatform(), - builder: (context, snapshot) { - switch (snapshot.connectionState) { - case ConnectionState.done: - return const Text('about_version').tr( - namedArgs: { - 'version': snapshot.data!.version, - }, - ); - default: - return const SizedBox(); - } - }, - ), - ], + 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 index 2f459d3..0599770 100644 --- a/lib/ui/screens/page_game.dart +++ b/lib/ui/screens/page_game.dart @@ -2,8 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:sortgame/cubit/game_cubit.dart'; -import 'package:sortgame/ui/widgets/game/game.dart'; -import 'package:sortgame/ui/widgets/parameters.dart'; +import 'package:sortgame/models/game/game.dart'; +import 'package:sortgame/ui/layouts/game_layout.dart'; +import 'package:sortgame/ui/layouts/parameters_layout.dart'; class PageGame extends StatelessWidget { const PageGame({super.key}); @@ -11,8 +12,13 @@ class PageGame extends StatelessWidget { @override Widget build(BuildContext context) { return BlocBuilder<GameCubit, GameState>( - builder: (BuildContext context, GameState gameState) { - return gameState.currentGame.isRunning ? const GameWidget() : const Parameters(); - }); + 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 index 14fbe4d..508116c 100644 --- a/lib/ui/screens/page_settings.dart +++ b/lib/ui/screens/page_settings.dart @@ -1,22 +1,26 @@ import 'package:flutter/material.dart'; -import 'package:sortgame/ui/widgets/helpers/app_header.dart'; -import 'package:sortgame/ui/widgets/settings/settings_form.dart'; +import 'package:sortgame/ui/helpers/app_titles.dart'; +import 'package:sortgame/ui/settings/settings_form.dart'; class PageSettings extends StatelessWidget { const PageSettings({super.key}); @override Widget build(BuildContext context) { - return const Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - children: <Widget>[ - AppHeader(text: 'settings_title'), - SizedBox(height: 8), - SettingsForm(), - ], + 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/widgets/settings/settings_form.dart b/lib/ui/settings/settings_form.dart similarity index 96% rename from lib/ui/widgets/settings/settings_form.dart rename to lib/ui/settings/settings_form.dart index 2e5d8ac..b9871e4 100644 --- a/lib/ui/widgets/settings/settings_form.dart +++ b/lib/ui/settings/settings_form.dart @@ -2,7 +2,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:unicons/unicons.dart'; -import 'package:sortgame/ui/widgets/settings/theme_card.dart'; +import 'package:sortgame/ui/settings/theme_card.dart'; class SettingsForm extends StatefulWidget { const SettingsForm({super.key}); diff --git a/lib/ui/widgets/settings/theme_card.dart b/lib/ui/settings/theme_card.dart similarity index 100% rename from lib/ui/widgets/settings/theme_card.dart rename to lib/ui/settings/theme_card.dart diff --git a/lib/ui/skeleton.dart b/lib/ui/skeleton.dart index 285eeae..5f76b70 100644 --- a/lib/ui/skeleton.dart +++ b/lib/ui/skeleton.dart @@ -14,7 +14,7 @@ class SkeletonScreen extends StatelessWidget { appBar: const GlobalAppBar(), extendBodyBehindAppBar: false, body: Material( - color: Theme.of(context).colorScheme.background, + color: Theme.of(context).colorScheme.surface, child: BlocBuilder<NavCubit, int>( builder: (BuildContext context, int pageIndex) { return Padding( @@ -28,7 +28,7 @@ class SkeletonScreen extends StatelessWidget { }, ), ), - backgroundColor: Theme.of(context).colorScheme.background, + 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 0000000..d09733a --- /dev/null +++ b/lib/ui/widgets/actions/button_delete_saved_game.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:sortgame/cubit/game_cubit.dart'; + +class DeleteSavedGameButton extends StatelessWidget { + const DeleteSavedGameButton({super.key}); + + @override + Widget build(BuildContext context) { + return TextButton( + child: const Image( + image: AssetImage('assets/ui/button_delete_saved_game.png'), + fit: BoxFit.fill, + ), + onPressed: () { + BlocProvider.of<GameCubit>(context).deleteSavedGame(); + }, + ); + } +} diff --git a/lib/ui/widgets/actions/button_game_quit.dart b/lib/ui/widgets/actions/button_game_quit.dart new file mode 100644 index 0000000..7e8d2ec --- /dev/null +++ b/lib/ui/widgets/actions/button_game_quit.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:sortgame/cubit/game_cubit.dart'; + +class QuitGameButton extends StatelessWidget { + const QuitGameButton({super.key}); + + @override + Widget build(BuildContext context) { + return TextButton( + child: const Image( + image: AssetImage('assets/ui/button_back.png'), + fit: BoxFit.fill, + ), + onPressed: () { + BlocProvider.of<GameCubit>(context).quitGame(); + }, + ); + } +} diff --git a/lib/ui/widgets/button_game_start_new.dart b/lib/ui/widgets/actions/button_game_start_new.dart similarity index 72% rename from lib/ui/widgets/button_game_start_new.dart rename to lib/ui/widgets/actions/button_game_start_new.dart index e302ac5..dcefbd7 100644 --- a/lib/ui/widgets/button_game_start_new.dart +++ b/lib/ui/widgets/actions/button_game_start_new.dart @@ -14,17 +14,17 @@ class StartNewGameButton extends StatelessWidget { builder: (BuildContext context, GameSettingsState gameSettingsState) { return BlocBuilder<GlobalSettingsCubit, GlobalSettingsState>( builder: (BuildContext context, GlobalSettingsState globalSettingsState) { - final GameCubit gameCubit = BlocProvider.of<GameCubit>(context); - return TextButton( child: const Image( - image: AssetImage('assets/icons/button_start.png'), + image: AssetImage('assets/ui/button_start.png'), fit: BoxFit.fill, ), - onPressed: () => gameCubit.startNewGame( - gameSettings: gameSettingsState.settings, - globalSettings: globalSettingsState.settings, - ), + onPressed: () { + BlocProvider.of<GameCubit>(context).startNewGame( + gameSettings: gameSettingsState.settings, + globalSettings: globalSettingsState.settings, + ); + }, ); }, ); diff --git a/lib/ui/widgets/actions/button_resume_saved_game.dart b/lib/ui/widgets/actions/button_resume_saved_game.dart new file mode 100644 index 0000000..20dada7 --- /dev/null +++ b/lib/ui/widgets/actions/button_resume_saved_game.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:sortgame/cubit/game_cubit.dart'; + +class ResumeSavedGameButton extends StatelessWidget { + const ResumeSavedGameButton({super.key}); + + @override + Widget build(BuildContext context) { + return TextButton( + child: const Image( + image: AssetImage('assets/ui/button_resume_game.png'), + fit: BoxFit.fill, + ), + onPressed: () { + BlocProvider.of<GameCubit>(context).resumeSavedGame(); + }, + ); + } +} diff --git a/lib/ui/widgets/game/game.dart b/lib/ui/widgets/game/game.dart deleted file mode 100644 index f218354..0000000 --- a/lib/ui/widgets/game/game.dart +++ /dev/null @@ -1,38 +0,0 @@ -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/game_bottom_buttons.dart'; -import 'package:sortgame/ui/widgets/game/game_question.dart'; -import 'package:sortgame/ui/widgets/game/game_top_indicator.dart'; - -class GameWidget extends StatelessWidget { - const GameWidget({super.key}); - - @override - Widget build(BuildContext context) { - return BlocBuilder<GameCubit, GameState>( - builder: (BuildContext context, GameState gameState) { - final Game currentGame = gameState.currentGame; - - return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const GameTopIndicatorWidget(), - const SizedBox(height: 8), - Expanded( - child: !currentGame.isFinished - ? const GameQuestionWidget() - : const SizedBox(height: 8), - ), - !currentGame.isFinished - ? const SizedBox(height: 8) - : const GameBottomButtonsWidget(), - ], - ); - }, - ); - } -} diff --git a/lib/ui/widgets/game/game_board.dart b/lib/ui/widgets/game/game_board.dart new file mode 100644 index 0000000..97b206b --- /dev/null +++ b/lib/ui/widgets/game/game_board.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:sortgame/cubit/game_cubit.dart'; +import 'package:sortgame/models/game/game.dart'; +import 'package:sortgame/ui/widgets/game/game_question.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 Game currentGame = gameState.currentGame; + + return !currentGame.isFinished + ? const GameQuestionWidget() + : const SizedBox.shrink(); + }, + ), + ); + } +} diff --git a/lib/ui/widgets/game/game_bottom_buttons.dart b/lib/ui/widgets/game/game_bottom_buttons.dart deleted file mode 100644 index 2af6424..0000000 --- a/lib/ui/widgets/game/game_bottom_buttons.dart +++ /dev/null @@ -1,51 +0,0 @@ -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/game_question.dart b/lib/ui/widgets/game/game_question.dart index e123272..c51c6e0 100644 --- a/lib/ui/widgets/game/game_question.dart +++ b/lib/ui/widgets/game/game_question.dart @@ -3,9 +3,9 @@ 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/models/game/game.dart'; +import 'package:sortgame/ui/helpers/outlined_text_widget.dart'; import 'package:sortgame/ui/widgets/game/buttons_yes_no.dart'; -import 'package:sortgame/ui/widgets/helpers/outlined_text_widget.dart'; class GameQuestionWidget extends StatelessWidget { const GameQuestionWidget({super.key}); diff --git a/lib/ui/widgets/game/game_top_indicator.dart b/lib/ui/widgets/game/game_top_indicator.dart deleted file mode 100644 index b6840f8..0000000 --- a/lib/ui/widgets/game/game_top_indicator.dart +++ /dev/null @@ -1,27 +0,0 @@ -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/indicator_position.dart'; -import 'package:sortgame/ui/widgets/game/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/game/indicator_position.dart b/lib/ui/widgets/game/indicator_position.dart deleted file mode 100644 index 335cad7..0000000 --- a/lib/ui/widgets/game/indicator_position.dart +++ /dev/null @@ -1,42 +0,0 @@ -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/game/indicator_score.dart b/lib/ui/widgets/game/indicator_score.dart deleted file mode 100644 index 2ab1774..0000000 --- a/lib/ui/widgets/game/indicator_score.dart +++ /dev/null @@ -1,21 +0,0 @@ -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/ui/widgets/global_app_bar.dart b/lib/ui/widgets/global_app_bar.dart index 1ec5af7..7636205 100644 --- a/lib/ui/widgets/global_app_bar.dart +++ b/lib/ui/widgets/global_app_bar.dart @@ -4,8 +4,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:sortgame/config/menu.dart'; import 'package:sortgame/cubit/game_cubit.dart'; import 'package:sortgame/cubit/nav_cubit.dart'; -import 'package:sortgame/models/game.dart'; -import 'package:sortgame/ui/widgets/helpers/app_title.dart'; +import 'package:sortgame/models/game/game.dart'; +import 'package:sortgame/ui/helpers/app_titles.dart'; class GlobalAppBar extends StatelessWidget implements PreferredSizeWidget { const GlobalAppBar({super.key}); @@ -20,16 +20,15 @@ class GlobalAppBar extends StatelessWidget implements PreferredSizeWidget { final List<Widget> menuActions = []; - if (currentGame.isRunning) { + if (currentGame.isRunning && !currentGame.isFinished) { menuActions.add(TextButton( child: const Image( - image: AssetImage('assets/icons/button_back.png'), + image: AssetImage('assets/ui/button_back.png'), fit: BoxFit.fill, ), onPressed: () {}, onLongPress: () { - final GameCubit gameCubit = BlocProvider.of<GameCubit>(context); - gameCubit.quitGame(); + BlocProvider.of<GameCubit>(context).quitGame(); }, )); } else { @@ -70,7 +69,7 @@ class GlobalAppBar extends StatelessWidget implements PreferredSizeWidget { } return AppBar( - title: const AppTitle(text: 'app_name'), + title: const AppHeader(text: 'app_name'), actions: menuActions, ); }, diff --git a/lib/ui/widgets/helpers/app_header.dart b/lib/ui/widgets/helpers/app_header.dart deleted file mode 100644 index b5c5be0..0000000 --- a/lib/ui/widgets/helpers/app_header.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -class AppHeader extends StatelessWidget { - const AppHeader({super.key, required this.text}); - - final String text; - - @override - Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - tr(text), - textAlign: TextAlign.start, - style: Theme.of(context).textTheme.headlineSmall!.apply(fontWeightDelta: 2), - ), - const SizedBox(height: 8), - ], - ); - } -} diff --git a/lib/ui/widgets/helpers/app_title.dart b/lib/ui/widgets/helpers/app_title.dart deleted file mode 100644 index 7cbbb20..0000000 --- a/lib/ui/widgets/helpers/app_title.dart +++ /dev/null @@ -1,17 +0,0 @@ -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/indicators/indicator_position.dart b/lib/ui/widgets/indicators/indicator_position.dart new file mode 100644 index 0000000..3966dd0 --- /dev/null +++ b/lib/ui/widgets/indicators/indicator_position.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:sortgame/cubit/game_cubit.dart'; +import 'package:sortgame/models/game/game.dart'; +import 'package:sortgame/ui/helpers/outlined_text_widget.dart'; +import 'package:sortgame/utils/color_extensions.dart'; + +class PositionIndicator extends StatelessWidget { + const PositionIndicator({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder<GameCubit, GameState>( + builder: (BuildContext context, GameState gameState) { + final Game currentGame = gameState.currentGame; + + // Normalized [0..1] value + final double barValue = + currentGame.position / int.parse(currentGame.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: '${currentGame.position}/${currentGame.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 0000000..22fed09 --- /dev/null +++ b/lib/ui/widgets/indicators/indicator_score.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/ui/helpers/outlined_text_widget.dart'; +import 'package:sortgame/utils/color_extensions.dart'; + +class ScoreIndicator extends StatelessWidget { + const ScoreIndicator({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder<GameCubit, GameState>( + builder: (BuildContext context, GameState gameState) { + const Color baseColor = Color.fromARGB(255, 218, 218, 218); + final Color outlineColor = baseColor.darken(); + + return OutlinedText( + text: gameState.currentGame.score.toString(), + fontSize: 70, + textColor: baseColor, + outlineColor: outlineColor, + ); + }, + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 25b5be0..d18e64e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -61,10 +61,10 @@ packages: dependency: "direct main" description: name: easy_localization - sha256: "432698c31a488dd64c56d4759f20d04844baba5e9e4f2cb1abb9676257918b17" + sha256: fa59bcdbbb911a764aa6acf96bbb6fa7a5cf8234354fc45ec1a43a0349ef0201 url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.0.7" easy_logger: dependency: transitive description: @@ -114,10 +114,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" + sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "4.0.0" flutter_localizations: dependency: transitive description: flutter @@ -164,18 +164,18 @@ packages: dependency: transitive description: name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.18.1" + version: "0.19.0" lints: dependency: transitive description: name: lints - sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "4.0.0" material_color_utilities: dependency: transitive description: @@ -188,10 +188,10 @@ packages: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.12.0" nested: dependency: transitive description: @@ -236,18 +236,18 @@ packages: dependency: transitive description: name: path_provider_android - sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d + sha256: "9c96da072b421e98183f9ea7464898428e764bc0ce5567f27ec8693442e72514" url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.2.5" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.0" path_provider_linux: dependency: transitive description: @@ -308,18 +308,18 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2" + sha256: "93d0ec9dd902d85f326068e6a899487d1f65ffcd5798721a95330b26c8131577" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.3" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" + sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7" url: "https://pub.dev" source: hosted - version: "2.3.5" + version: "2.4.0" shared_preferences_linux: dependency: transitive description: @@ -425,10 +425,10 @@ packages: dependency: transitive description: name: win32 - sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" + sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4 url: "https://pub.dev" source: hosted - version: "5.5.0" + version: "5.5.1" xdg_directories: dependency: transitive description: @@ -438,5 +438,5 @@ packages: source: hosted version: "1.0.4" sdks: - dart: ">=3.3.0 <4.0.0" - flutter: ">=3.19.0" + dart: ">=3.4.0 <4.0.0" + flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index ac69ef5..ca06f93 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A sorting game application. publish_to: "none" -version: 0.0.26+26 +version: 0.1.0+27 environment: sdk: "^3.0.0" @@ -12,6 +12,7 @@ dependencies: flutter: sdk: flutter + # base easy_localization: ^3.0.1 equatable: ^2.0.5 flutter_bloc: ^8.1.1 @@ -21,13 +22,16 @@ dependencies: path_provider: ^2.0.11 unicons: ^2.1.1 + # specific + # (none) + dev_dependencies: - flutter_lints: ^3.0.1 + flutter_lints: ^4.0.0 flutter: uses-material-design: true assets: - - assets/icons/ + - assets/ui/ - assets/translations/ fonts: @@ -41,3 +45,4 @@ flutter: 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 98% rename from icons/build_application_icons.sh rename to resources/app/build_application_resources.sh index 27dbe26..6d67b8f 100755 --- a/icons/build_application_icons.sh +++ b/resources/app/build_application_resources.sh @@ -6,7 +6,7 @@ command -v scour >/dev/null 2>&1 || { echo >&2 "I require scour but it's not ins command -v optipng >/dev/null 2>&1 || { echo >&2 "I require optipng but it's not installed. Aborting."; exit 1; } CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" -BASE_DIR="$(dirname "${CURRENT_DIR}")" +BASE_DIR="$(dirname "$(dirname "${CURRENT_DIR}")")" SOURCE_ICON="${CURRENT_DIR}/icon.svg" SOURCE_FASTLANE="${CURRENT_DIR}/featureGraphic.svg" diff --git a/icons/featureGraphic.svg b/resources/app/featureGraphic.svg similarity index 100% rename from icons/featureGraphic.svg rename to resources/app/featureGraphic.svg diff --git a/icons/icon.svg b/resources/app/icon.svg similarity index 100% rename from icons/icon.svg rename to resources/app/icon.svg diff --git a/resources/build_resources.sh b/resources/build_resources.sh new file mode 100755 index 0000000..659697a --- /dev/null +++ b/resources/build_resources.sh @@ -0,0 +1,7 @@ +#! /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/scripts/data.json b/resources/data/data.json similarity index 100% rename from scripts/data.json rename to resources/data/data.json diff --git a/scripts/manage_data.php b/resources/data/manage_data.php similarity index 100% rename from scripts/manage_data.php rename to resources/data/manage_data.php diff --git a/scripts/manage_data.sh b/resources/data/manage_data.sh similarity index 100% rename from scripts/manage_data.sh rename to resources/data/manage_data.sh diff --git a/resources/ui/build_ui_resources.sh b/resources/ui/build_ui_resources.sh new file mode 100755 index 0000000..4f365ed --- /dev/null +++ b/resources/ui/build_ui_resources.sh @@ -0,0 +1,110 @@ +#! /bin/bash + +# Check dependencies +command -v inkscape >/dev/null 2>&1 || { echo >&2 "I require inkscape but it's not installed. Aborting."; exit 1; } +command -v scour >/dev/null 2>&1 || { echo >&2 "I require scour but it's not installed. Aborting."; exit 1; } +command -v optipng >/dev/null 2>&1 || { echo >&2 "I require optipng but it's not installed. Aborting."; exit 1; } + +CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" +BASE_DIR="$(dirname "$(dirname "${CURRENT_DIR}")")" +ASSETS_DIR="${BASE_DIR}/assets" + +OPTIPNG_OPTIONS="-preserve -quiet -o7" +ICON_SIZE=192 + +####################################################### + +# Game images (svg files found in `images` folder) +AVAILABLE_GAME_IMAGES="" +if [ -d "${CURRENT_DIR}/images" ]; then + AVAILABLE_GAME_IMAGES="$(find "${CURRENT_DIR}/images" -type f -name "*.svg" | awk -F/ '{print $NF}' | cut -d"." -f1 | sort)" +fi + +# Skins (subfolders found in `skins` folder) +AVAILABLE_SKINS="" +if [ -d "${CURRENT_DIR}/skins" ]; then + AVAILABLE_SKINS="$(find "${CURRENT_DIR}/skins" -mindepth 1 -type d | awk -F/ '{print $NF}')" +fi + +# Images per skin (svg files found recursively in `skins` folder and subfolders) +SKIN_IMAGES="" +if [ -d "${CURRENT_DIR}/skins" ]; then + SKIN_IMAGES="$(find "${CURRENT_DIR}/skins" -type f -name "*.svg" | awk -F/ '{print $NF}' | cut -d"." -f1 | sort | uniq)" +fi + +####################################################### + +# optimize svg +function optimize_svg() { + SOURCE="$1" + + cp ${SOURCE} ${SOURCE}.tmp + scour \ + --remove-descriptive-elements \ + --enable-id-stripping \ + --enable-viewboxing \ + --enable-comment-stripping \ + --nindent=4 \ + --quiet \ + -i ${SOURCE}.tmp \ + -o ${SOURCE} + rm ${SOURCE}.tmp +} + +# build icons +function build_image() { + SOURCE="$1" + TARGET="$2" + + echo "Building ${TARGET}" + + if [ ! -f "${SOURCE}" ]; then + echo "Missing file: ${SOURCE}" + exit 1 + fi + + optimize_svg "${SOURCE}" + + mkdir -p "$(dirname "${TARGET}")" + + inkscape \ + --export-width=${ICON_SIZE} \ + --export-height=${ICON_SIZE} \ + --export-filename=${TARGET} \ + "${SOURCE}" + + optipng ${OPTIPNG_OPTIONS} "${TARGET}" +} + +function build_image_for_skin() { + SKIN_CODE="$1" + + # skin images + for SKIN_IMAGE in ${SKIN_IMAGES} + do + build_image ${CURRENT_DIR}/skins/${SKIN_CODE}/${SKIN_IMAGE}.svg ${ASSETS_DIR}/skins/${SKIN_CODE}_${SKIN_IMAGE}.png + done +} + +####################################################### + +# Delete existing generated images +if [ -d "${ASSETS_DIR}/ui" ]; then + find ${ASSETS_DIR}/ui -type f -name "*.png" -delete +fi +if [ -d "${ASSETS_DIR}/skins" ]; then + find ${ASSETS_DIR}/skins -type f -name "*.png" -delete +fi + +# build game images +for GAME_IMAGE in ${AVAILABLE_GAME_IMAGES} +do + build_image ${CURRENT_DIR}/images/${GAME_IMAGE}.svg ${ASSETS_DIR}/ui/${GAME_IMAGE}.png +done + +# build skins images +for SKIN in ${AVAILABLE_SKINS} +do + build_image_for_skin "${SKIN}" +done + diff --git a/icons/button_back.svg b/resources/ui/images/button_back.svg similarity index 100% rename from icons/button_back.svg rename to resources/ui/images/button_back.svg diff --git a/resources/ui/images/button_delete_saved_game.svg b/resources/ui/images/button_delete_saved_game.svg new file mode 100644 index 0000000..ac7eefe --- /dev/null +++ b/resources/ui/images/button_delete_saved_game.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect x=".44662" y=".89101" width="92.772" height="91.894" ry="11.689" fill="#ee7d49" stroke="#fff" stroke-width=".238"/><path d="m61.07 35.601-1.7399 27.837c-0.13442 2.1535-1.9205 3.8312-4.0781 3.8312h-16.84c-2.1576 0-3.9437-1.6777-4.0781-3.8312l-1.7399-27.837h-2.6176c-0.84621 0-1.5323-0.68613-1.5323-1.5323 0-0.84655 0.68613-1.5323 1.5323-1.5323h33.711c0.84621 0 1.5323 0.68578 1.5323 1.5323 0 0.84621-0.68613 1.5323-1.5323 1.5323zm-3.2617 0h-21.953l1.4715 26.674c0.05985 1.0829 0.95531 1.9305 2.0403 1.9305h14.929c1.085 0 1.9804-0.84757 2.0403-1.9305zm-10.977 3.0647c0.78977 0 1.4301 0.6403 1.4301 1.4301v19.614c0 0.78977-0.6403 1.4301-1.4301 1.4301s-1.4301-0.6403-1.4301-1.4301v-19.614c0-0.78977 0.6403-1.4301 1.4301-1.4301zm-6.1293 0c0.80004 0 1.4588 0.62935 1.495 1.4286l0.89647 19.719c0.03182 0.70016-0.50998 1.2933-1.2101 1.3255-0.01915 7.02e-4 -0.03831 1e-3 -0.05781 1e-3 -0.74462 0-1.3596-0.58215-1.4003-1.3261l-1.0757-19.719c-0.0407-0.74701 0.53188-1.3852 1.2786-1.4259 0.02462-0.0014 0.04926-2e-3 0.07388-2e-3zm12.259 0c0.74804 0 1.3541 0.60609 1.3541 1.3541 0 0.02462-3.28e-4 0.04926-0.0017 0.07388l-1.0703 19.618c-0.04379 0.80106-0.70597 1.4281-1.5081 1.4281-0.74804 0-1.3541-0.60609-1.3541-1.3541 0-0.02462 3.49e-4 -0.04925 0.0017-0.07388l1.0703-19.618c0.04379-0.80106 0.70597-1.4281 1.5081-1.4281zm-10.216-12.259h8.1728c2.2567 0 4.086 1.8293 4.086 4.086v2.0433h-16.344v-2.0433c0-2.2567 1.8293-4.086 4.086-4.086zm0.20453 3.0647c-0.67725 0-1.2259 0.54863-1.2259 1.2259v1.8388h10.215v-1.8388c0-0.67725-0.54863-1.2259-1.2259-1.2259z" fill="#fff" fill-rule="evenodd" stroke="#bd4812" stroke-width=".75383"/></svg> diff --git a/resources/ui/images/button_resume_game.svg b/resources/ui/images/button_resume_game.svg new file mode 100644 index 0000000..6ad8b64 --- /dev/null +++ b/resources/ui/images/button_resume_game.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect x=".44662" y=".89101" width="92.772" height="91.894" ry="11.689" fill="#49a1ee" stroke="#fff" stroke-width=".238"/><path d="m39.211 31.236c-0.84086-0.84489-2.9911-0.84489-2.9911 0v34.329c0 0.84594 2.1554 0.84594 2.9993 0l28.178-15.637c0.84392-0.84086 0.85812-2.2091 0.01623-3.053z" fill="#fefeff" stroke="#105ca1" stroke-linecap="round" stroke-linejoin="round" stroke-width="6.1726"/><path d="m40.355 33.714c-0.71948-0.72294-2.5594-0.72294-2.5594 0v29.373c0 0.72383 1.8442 0.72383 2.5663 0l24.11-13.38c0.7221-0.71948 0.73426-1.8902 0.01389-2.6124z" fill="#fefeff" stroke="#feffff" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.225"/><path d="m28.369 66.919v-37.591" fill="#105ca2" stroke="#105ca2" stroke-linecap="round" stroke-width="4.0337"/></svg> diff --git a/icons/button_start.svg b/resources/ui/images/button_start.svg similarity index 100% rename from icons/button_start.svg rename to resources/ui/images/button_start.svg diff --git a/resources/ui/images/game_fail.svg b/resources/ui/images/game_fail.svg new file mode 100644 index 0000000..2922fd7 --- /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"><rect x=".44662" y=".89101" width="92.772" height="91.894" ry="11.689" fill="#d11717" stroke="#fff" stroke-width=".238"/><path d="m71.624 59.304c3.5089 3.5089 3.5089 9.0561 0 12.565-1.6976 1.6976-3.9623 2.6034-6.2261 2.6034s-4.5275-0.90569-6.2261-2.6034l-12.452-12.452-12.452 12.452c-1.6976 1.6976-3.9623 2.6034-6.2261 2.6034s-4.5275-0.90569-6.2261-2.6034c-3.5089-3.5089-3.5089-9.0561 0-12.565l12.452-12.452-12.452-12.452c-3.5089-3.5089-3.5089-9.0561 0-12.565s9.0561-3.5089 12.565 0l12.452 12.452 12.452-12.452c3.5089-3.5089 9.0561-3.5089 12.565 0s3.5089 9.0561 0 12.565l-12.452 12.452z" fill="#e7e7e7" stroke-width=".20213"/></svg> diff --git a/resources/ui/images/game_win.svg b/resources/ui/images/game_win.svg new file mode 100644 index 0000000..fe20923 --- /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/icons/placeholder.svg b/resources/ui/images/placeholder.svg similarity index 100% rename from icons/placeholder.svg rename to resources/ui/images/placeholder.svg -- GitLab