diff --git a/android/gradle.properties b/android/gradle.properties index f0fcf6cdd8fc72bc76b35e7be0dc622b20c03b0f..c7c59fb1a34268f8dcc640e2c52dc7a784cee5bc 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 5b07e64e4c095f89d771416ac16c434d2a32d8e6..ea0863cfe20d575f0292684d30b16489e735793f 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 0cd4ca768ce7ab83e72b3aa9b6c4c285b10968c5..69c7164b7d4de3fab203042a87de98d00e71d04d 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 Binary files /dev/null and b/assets/ui/button_delete_saved_game.png differ diff --git a/assets/ui/button_resume_game.png b/assets/ui/button_resume_game.png new file mode 100644 index 0000000000000000000000000000000000000000..b2ea0a02d05e42377eb551a4b51428b511a32f5d Binary files /dev/null and b/assets/ui/button_resume_game.png differ diff --git a/assets/icons/button_start.png b/assets/ui/button_start.png similarity index 100% rename from assets/icons/button_start.png rename to assets/ui/button_start.png diff --git a/assets/ui/game_fail.png b/assets/ui/game_fail.png new file mode 100644 index 0000000000000000000000000000000000000000..93f2801f9d6bb2ce508e1293cd64d6ff2e9970ec Binary files /dev/null and b/assets/ui/game_fail.png differ diff --git a/assets/ui/game_win.png b/assets/ui/game_win.png new file mode 100644 index 0000000000000000000000000000000000000000..876334279c1711b349a62131a33607eecf924eb6 Binary files /dev/null and b/assets/ui/game_win.png differ diff --git a/assets/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 0000000000000000000000000000000000000000..d4afd512e55b3fd8ffbfd795adb9b00832e5aaef --- /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 0000000000000000000000000000000000000000..6a9871a5eb8eb3c6e9106520f1cbf1f39f9e5ef7 --- /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 7368dc4543ff5b92568d683f3d0fb144364bbe04..0000000000000000000000000000000000000000 --- 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 dd16dbfa75ae0c374fe62d078d595da655e82dc7..e43bea3ee72483278e2bdfb56fce5537bc586919 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 8b0d15cae4ca4f0dbaa902ac35fdd0c865b91e5a..8ec660964563b37090b0e4d7e59023f4827aeb8e 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 674e6fe59520d98919c76593ca21a8125729a903..f161e45df9e2de6fbc2345b2ee7b4b495f84e5a9 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 be390348c7868e7c63387df13e13c46de43f8a23..74f532fd5abf693979118609564d29167e902009 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 b21f2af80a8f8747194058ceaad30fa0c2ca6828..ec76c9a454cef0928451d07f27c794160dc4153a 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 3fd161a0915313722b7a15c55c7cf538a7e3b6e1..00e211668c3269255926939324355792abd61c41 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 c411ae194035e0dece266f3434a7358cc3c55c19..5278e899570395e9b1e9682341d9bc7e9c8d0b54 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 b773dc69be12673b158e880e2d7e6e7bec465506..5acd85b44ba541e1c5e9c26af1c4be26a385b9ed 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 36b4b20ed5e0c18f5509e0214986fecb86df4d3a..9a404fe2c8c5e7df4da059078c5b314271f116be 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 4e4fbdf707b4e805f2092d0ca6a68a2de1c957c6..ebcddd700f252257223ca8e16c85202b04f3ff24 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 7585817d83de7532471009f4afbb5badc38163b3..405199e8b8d710a4df72366bd155135f61a42014 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 667de3572c35e2fda6e10a21b40c7103887ca707..9f5c58297d53b53321ae63063c179800dd318c5b 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 74e9d0254eafc47b90eaa2ebb3a97e9a724332bb..bada93d62b7d1c8c73911ccc3676ffd586add488 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 3528fa4b15bfde064fb15d132c027e53e4127e8c..c8eac0da0e73b18bd8f19fcae069ac021fd7da60 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 0000000000000000000000000000000000000000..5442540ccc3b0ef40cc59e924b50d6a2578903db --- /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 1da37395f09b1c71c656274c5248a2f6d0edae95..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..468138c30c1c25c450e9e7d1e5ec17aa4376d62f --- /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 0000000000000000000000000000000000000000..acaa7377a67329b09ecaf75d85c0ccea295e070a --- /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 0000000000000000000000000000000000000000..b98107b12fabc3114ebfbec994166b588abcf1ad --- /dev/null +++ b/lib/ui/helpers/app_titles.dart @@ -0,0 +1,32 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; + +class AppHeader extends StatelessWidget { + const AppHeader({super.key, required this.text}); + + final String text; + + @override + Widget build(BuildContext context) { + return Text( + tr(text), + textAlign: TextAlign.start, + style: Theme.of(context).textTheme.headlineMedium!.apply(fontWeightDelta: 2), + ); + } +} + +class AppTitle extends StatelessWidget { + const AppTitle({super.key, required this.text}); + + final String text; + + @override + Widget build(BuildContext context) { + return Text( + tr(text), + textAlign: TextAlign.start, + style: Theme.of(context).textTheme.titleLarge!.apply(fontWeightDelta: 2), + ); + } +} diff --git a/lib/ui/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 0000000000000000000000000000000000000000..0f3d33b6a72762a79f26aee11f2c431f7852c7e1 --- /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 800dce1c646a1160eb81a227a1141cb64088c0eb..52a22be1ae72fc340f846457f5d4c7774c712613 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 0000000000000000000000000000000000000000..fc4b576f85b01158b74548400d11a4d027c57fbe --- /dev/null +++ b/lib/ui/parameters/parameter_image.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +class ParameterImage extends StatelessWidget { + const ParameterImage({ + super.key, + required this.code, + required this.value, + required this.isSelected, + }); + + final String code; + final String value; + final bool isSelected; + + static const Color buttonBackgroundColor = Colors.white; + static const Color buttonBorderColorActive = Colors.blue; + static const Color buttonBorderColorInactive = Colors.white; + static const double buttonBorderWidth = 8.0; + static const double buttonBorderRadius = 8.0; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: buttonBackgroundColor, + borderRadius: BorderRadius.circular(buttonBorderRadius), + border: Border.all( + color: isSelected ? buttonBorderColorActive : buttonBorderColorInactive, + width: buttonBorderWidth, + ), + ), + child: Image( + image: AssetImage('assets/ui/${code}_$value.png'), + fit: BoxFit.fill, + ), + ); + } +} diff --git a/lib/ui/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 2f14e73f109a0249312edec5c23c09e74bd1cc1e..8aa964dc0dd61699c84519368e877a2a9676b0ac 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 ff9f710a7f1caa1e94ad18f4e44b521417f9fcee..080fcaf3c367e0698c8e9e6a37d5e975090c9a44 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 2f459d33d6680fc4ec42df025666f90dd2322cc7..059977016b565a490329b35f141ab08f19cb6cd7 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 14fbe4d0dcefedc48f622315a7a0e6ba7d399347..508116c08a9343a72cef7b617754d3a4f48b8979 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 2e5d8acc1fa076da07ee523aebb735ee2153de62..b9871e42582bfbf0ad8b910420869effaa368e9f 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 285eeae0574626a4d391116bdb654f9385b12a44..5f76b700c9e81f02334adad48255ba83d746a294 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 0000000000000000000000000000000000000000..d09733adba7356e412f5029fa8aaef3bdc0693a0 --- /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 0000000000000000000000000000000000000000..7e8d2ecf9422477f38e43623308049e39676dd07 --- /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 e302ac59e856450b3a766b1898d8ed5d4560966a..dcefbd78e9d9edd48e6f2a0db6eec1004251c196 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 0000000000000000000000000000000000000000..20dada724712e0995804df922f90ea426b8330f2 --- /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 f218354029d6428f5d0aa4f6ee056a8dd1a68df5..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..97b206b951594ae7c49044a377286fed014a8756 --- /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 2af6424a0a7633602a6ba6b3ba53db9877ff4017..0000000000000000000000000000000000000000 --- 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 e12327239099db12677735681389a8511cdd65ca..c51c6e07fb2f323461511db3d6df072fdba005f3 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 b6840f8a9723185e2d329eb4fd42cd9b3810b892..0000000000000000000000000000000000000000 --- 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 335cad77e235f59b0363cf2fe3f28dc016fdfba5..0000000000000000000000000000000000000000 --- 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 2ab1774f25e476312a5b6b9683ebd531299e6273..0000000000000000000000000000000000000000 --- 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 1ec5af7a7dfda7d623563a4f40c78fdd073c7c49..7636205f42f4fdd35aec6c70ccd5d3974904e1a4 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 b5c5be05f6636cf488dcdb5bbc4d6f049b98de11..0000000000000000000000000000000000000000 --- 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 7cbbb2030419047b3dcf093a2195a498bd8e8ce9..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..3966dd034e354f543d4d0041433f9f38b216315a --- /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 0000000000000000000000000000000000000000..22fed0981228a39d312caf037039e91eed4b0ebd --- /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 25b5be0248780b099e0f72fc5dab70bd5ce13f92..d18e64ef53a7374d49ad9f56cb9fc6da139f38d9 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 ac69ef59c1daf7043a94d478b5221bbc5cf83cb9..ca06f93135f5c8ea370bf0bf0177d4d08ecf3f5c 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 27dbe2647fe4e6d562fbd99451716d1b7d448570..6d67b8f4f9eca701d1aed7331ef41dfb0bd44f20 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 0000000000000000000000000000000000000000..659697a1c043cfe1c7654635cfaec3e4a0ff8a1a --- /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 0000000000000000000000000000000000000000..4f365ede7d83140ce6309a3083580f2662b30990 --- /dev/null +++ b/resources/ui/build_ui_resources.sh @@ -0,0 +1,110 @@ +#! /bin/bash + +# Check dependencies +command -v inkscape >/dev/null 2>&1 || { echo >&2 "I require inkscape but it's not installed. Aborting."; exit 1; } +command -v scour >/dev/null 2>&1 || { echo >&2 "I require scour but it's not installed. Aborting."; exit 1; } +command -v optipng >/dev/null 2>&1 || { echo >&2 "I require optipng but it's not installed. Aborting."; exit 1; } + +CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" +BASE_DIR="$(dirname "$(dirname "${CURRENT_DIR}")")" +ASSETS_DIR="${BASE_DIR}/assets" + +OPTIPNG_OPTIONS="-preserve -quiet -o7" +ICON_SIZE=192 + +####################################################### + +# Game images (svg files found in `images` folder) +AVAILABLE_GAME_IMAGES="" +if [ -d "${CURRENT_DIR}/images" ]; then + AVAILABLE_GAME_IMAGES="$(find "${CURRENT_DIR}/images" -type f -name "*.svg" | awk -F/ '{print $NF}' | cut -d"." -f1 | sort)" +fi + +# Skins (subfolders found in `skins` folder) +AVAILABLE_SKINS="" +if [ -d "${CURRENT_DIR}/skins" ]; then + AVAILABLE_SKINS="$(find "${CURRENT_DIR}/skins" -mindepth 1 -type d | awk -F/ '{print $NF}')" +fi + +# Images per skin (svg files found recursively in `skins` folder and subfolders) +SKIN_IMAGES="" +if [ -d "${CURRENT_DIR}/skins" ]; then + SKIN_IMAGES="$(find "${CURRENT_DIR}/skins" -type f -name "*.svg" | awk -F/ '{print $NF}' | cut -d"." -f1 | sort | uniq)" +fi + +####################################################### + +# optimize svg +function optimize_svg() { + SOURCE="$1" + + cp ${SOURCE} ${SOURCE}.tmp + scour \ + --remove-descriptive-elements \ + --enable-id-stripping \ + --enable-viewboxing \ + --enable-comment-stripping \ + --nindent=4 \ + --quiet \ + -i ${SOURCE}.tmp \ + -o ${SOURCE} + rm ${SOURCE}.tmp +} + +# build icons +function build_image() { + SOURCE="$1" + TARGET="$2" + + echo "Building ${TARGET}" + + if [ ! -f "${SOURCE}" ]; then + echo "Missing file: ${SOURCE}" + exit 1 + fi + + optimize_svg "${SOURCE}" + + mkdir -p "$(dirname "${TARGET}")" + + inkscape \ + --export-width=${ICON_SIZE} \ + --export-height=${ICON_SIZE} \ + --export-filename=${TARGET} \ + "${SOURCE}" + + optipng ${OPTIPNG_OPTIONS} "${TARGET}" +} + +function build_image_for_skin() { + SKIN_CODE="$1" + + # skin images + for SKIN_IMAGE in ${SKIN_IMAGES} + do + build_image ${CURRENT_DIR}/skins/${SKIN_CODE}/${SKIN_IMAGE}.svg ${ASSETS_DIR}/skins/${SKIN_CODE}_${SKIN_IMAGE}.png + done +} + +####################################################### + +# Delete existing generated images +if [ -d "${ASSETS_DIR}/ui" ]; then + find ${ASSETS_DIR}/ui -type f -name "*.png" -delete +fi +if [ -d "${ASSETS_DIR}/skins" ]; then + find ${ASSETS_DIR}/skins -type f -name "*.png" -delete +fi + +# build game images +for GAME_IMAGE in ${AVAILABLE_GAME_IMAGES} +do + build_image ${CURRENT_DIR}/images/${GAME_IMAGE}.svg ${ASSETS_DIR}/ui/${GAME_IMAGE}.png +done + +# build skins images +for SKIN in ${AVAILABLE_SKINS} +do + build_image_for_skin "${SKIN}" +done + diff --git a/icons/button_back.svg b/resources/ui/images/button_back.svg similarity index 100% rename from icons/button_back.svg rename to resources/ui/images/button_back.svg diff --git a/resources/ui/images/button_delete_saved_game.svg b/resources/ui/images/button_delete_saved_game.svg new file mode 100644 index 0000000000000000000000000000000000000000..ac7eefef476f761903fe781b8c86d0c94323550a --- /dev/null +++ b/resources/ui/images/button_delete_saved_game.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect x=".44662" y=".89101" width="92.772" height="91.894" ry="11.689" fill="#ee7d49" stroke="#fff" stroke-width=".238"/><path d="m61.07 35.601-1.7399 27.837c-0.13442 2.1535-1.9205 3.8312-4.0781 3.8312h-16.84c-2.1576 0-3.9437-1.6777-4.0781-3.8312l-1.7399-27.837h-2.6176c-0.84621 0-1.5323-0.68613-1.5323-1.5323 0-0.84655 0.68613-1.5323 1.5323-1.5323h33.711c0.84621 0 1.5323 0.68578 1.5323 1.5323 0 0.84621-0.68613 1.5323-1.5323 1.5323zm-3.2617 0h-21.953l1.4715 26.674c0.05985 1.0829 0.95531 1.9305 2.0403 1.9305h14.929c1.085 0 1.9804-0.84757 2.0403-1.9305zm-10.977 3.0647c0.78977 0 1.4301 0.6403 1.4301 1.4301v19.614c0 0.78977-0.6403 1.4301-1.4301 1.4301s-1.4301-0.6403-1.4301-1.4301v-19.614c0-0.78977 0.6403-1.4301 1.4301-1.4301zm-6.1293 0c0.80004 0 1.4588 0.62935 1.495 1.4286l0.89647 19.719c0.03182 0.70016-0.50998 1.2933-1.2101 1.3255-0.01915 7.02e-4 -0.03831 1e-3 -0.05781 1e-3 -0.74462 0-1.3596-0.58215-1.4003-1.3261l-1.0757-19.719c-0.0407-0.74701 0.53188-1.3852 1.2786-1.4259 0.02462-0.0014 0.04926-2e-3 0.07388-2e-3zm12.259 0c0.74804 0 1.3541 0.60609 1.3541 1.3541 0 0.02462-3.28e-4 0.04926-0.0017 0.07388l-1.0703 19.618c-0.04379 0.80106-0.70597 1.4281-1.5081 1.4281-0.74804 0-1.3541-0.60609-1.3541-1.3541 0-0.02462 3.49e-4 -0.04925 0.0017-0.07388l1.0703-19.618c0.04379-0.80106 0.70597-1.4281 1.5081-1.4281zm-10.216-12.259h8.1728c2.2567 0 4.086 1.8293 4.086 4.086v2.0433h-16.344v-2.0433c0-2.2567 1.8293-4.086 4.086-4.086zm0.20453 3.0647c-0.67725 0-1.2259 0.54863-1.2259 1.2259v1.8388h10.215v-1.8388c0-0.67725-0.54863-1.2259-1.2259-1.2259z" fill="#fff" fill-rule="evenodd" stroke="#bd4812" stroke-width=".75383"/></svg> diff --git a/resources/ui/images/button_resume_game.svg b/resources/ui/images/button_resume_game.svg new file mode 100644 index 0000000000000000000000000000000000000000..6ad8b64202d0e70f898c16c520e756fe8a934add --- /dev/null +++ b/resources/ui/images/button_resume_game.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect x=".44662" y=".89101" width="92.772" height="91.894" ry="11.689" fill="#49a1ee" stroke="#fff" stroke-width=".238"/><path d="m39.211 31.236c-0.84086-0.84489-2.9911-0.84489-2.9911 0v34.329c0 0.84594 2.1554 0.84594 2.9993 0l28.178-15.637c0.84392-0.84086 0.85812-2.2091 0.01623-3.053z" fill="#fefeff" stroke="#105ca1" stroke-linecap="round" stroke-linejoin="round" stroke-width="6.1726"/><path d="m40.355 33.714c-0.71948-0.72294-2.5594-0.72294-2.5594 0v29.373c0 0.72383 1.8442 0.72383 2.5663 0l24.11-13.38c0.7221-0.71948 0.73426-1.8902 0.01389-2.6124z" fill="#fefeff" stroke="#feffff" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.225"/><path d="m28.369 66.919v-37.591" fill="#105ca2" stroke="#105ca2" stroke-linecap="round" stroke-width="4.0337"/></svg> diff --git a/icons/button_start.svg b/resources/ui/images/button_start.svg similarity index 100% rename from icons/button_start.svg rename to resources/ui/images/button_start.svg diff --git a/resources/ui/images/game_fail.svg b/resources/ui/images/game_fail.svg new file mode 100644 index 0000000000000000000000000000000000000000..2922fd7adc2bd2e813836c728f095376c73d4143 --- /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 0000000000000000000000000000000000000000..fe20923864d0c5d39168eced03038b65106a596b --- /dev/null +++ b/resources/ui/images/game_win.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(.17604 0 0 .17604 7.9341 1.7716)"><path d="m101.92 496.35c-1.8555 0-3.7109-0.69532-5.1484-2.0898-2.9297-2.8438-3-7.5234-0.15234-10.453l9.1875-9.4648c2.8438-2.9297 7.5234-3 10.453-0.15625s3 7.5234 0.15625 10.453l-9.1914 9.4648c-1.4492 1.4961-3.375 2.2461-5.3047 2.2461z" fill="#ff4e61"/><path d="m201.65 133.26c-1.8516 0-3.7109-0.69531-5.1445-2.0898-2.9297-2.8438-3-7.5234-0.15625-10.449l9.1914-9.4688c2.8438-2.9297 7.5195-3 10.449-0.15625s3 7.5234 0.15625 10.453l-9.1914 9.4688c-1.4492 1.4922-3.375 2.2422-5.3047 2.2422z" fill="#ff4e61"/><path d="m413.8 100.39c-1.8555 0-3.7109-0.69141-5.1484-2.0859-2.9297-2.8438-3-7.5234-0.15625-10.453l9.1914-9.4688c2.8438-2.9258 7.5234-2.9961 10.453-0.15234 2.9297 2.8398 3 7.5234 0.15625 10.449l-9.1914 9.4688c-1.4492 1.4922-3.375 2.2422-5.3047 2.2422z" fill="#5c73bc"/><path d="m413.8 463.77c-1.8555 0-3.7109-0.69532-5.1484-2.0859-2.9297-2.8438-3-7.5234-0.15625-10.453l9.1914-9.4688c2.8438-2.9258 7.5234-3 10.453-0.15625s3 7.5234 0.15625 10.453l-9.1914 9.4688c-1.4492 1.4922-3.375 2.2422-5.3047 2.2422z" fill="#fa0"/><path d="m63.07 112.91c-1.8516 0-3.7109-0.69141-5.1445-2.0859-2.9297-2.8438-3-7.5234-0.15625-10.453l9.1914-9.4687c2.8438-2.9258 7.5234-2.9961 10.453-0.15234 2.9258 2.8438 2.9961 7.5234 0.15234 10.449l-9.1914 9.4688c-1.4492 1.4922-3.375 2.2422-5.3047 2.2422z" fill="#fa0"/><path d="m12.309 278.82c-1.8516 0-3.7109-0.69141-5.1445-2.0859-2.9297-2.8438-3-7.5234-0.15625-10.453l9.1875-9.4688c2.8438-2.9297 7.5234-3 10.453-0.15625 2.9297 2.8438 3 7.5234 0.15625 10.453l-9.1914 9.4688c-1.4453 1.4922-3.375 2.2422-5.3047 2.2422z" fill="#2dc471"/><path d="m216.29 278.49-23.996 12.996c-6.2226 3.3711-13.496-2.0742-12.309-9.2148l4.582-27.523c0.47266-2.8359-0.4375-5.7266-2.4375-7.7344l-19.414-19.496c-5.0352-5.0547-2.2578-13.863 4.7031-14.906l26.824-4.0156c2.7656-0.41407 5.1524-2.1992 6.3867-4.7812l12-25.043c3.1133-6.4922 12.102-6.4922 15.215 0l11.996 25.043c1.2383 2.582 3.625 4.3672 6.3867 4.7812l26.828 4.0156c6.957 1.043 9.7344 9.8516 4.6992 14.906l-19.41 19.496c-2 2.0078-2.9141 4.8984-2.4414 7.7344l4.582 27.523c1.1914 7.1406-6.082 12.586-12.305 9.2148l-23.996-12.996c-2.4727-1.3398-5.4258-1.3398-7.8945 0z" fill="#ffd02f"/><path d="m220.24 512c-4.082 0-7.3906-3.3086-7.3906-7.3906v-115.59c0-4.082 3.3086-7.3945 7.3906-7.3945s7.3906 3.3125 7.3906 7.3945v115.59c0 4.082-3.3086 7.3906-7.3906 7.3906z" fill="#5c73bc"/><path d="m220.3 357.42h-0.11328c-4.082 0-7.3945-3.3125-7.3945-7.3945s3.3086-7.3906 7.3945-7.3906h0.11328c4.082 0 7.3906 3.3086 7.3906 7.3906s-3.3086 7.3945-7.3906 7.3945z" fill="#5c73bc"/><path d="m220.3 332h-0.14838c-4.082-0.0156-7.375-3.3398-7.3594-7.4219 0.0195-4.0742 3.3242-7.3594 7.3906-7.3594h0.14848c4.082 0.0156 7.375 3.3398 7.3594 7.4219-0.0156 4.0703-3.3242 7.3594-7.3906 7.3594z" fill="#fa0"/><path d="m87.234 230.89c-1.9297 0-3.8555-0.75-5.3047-2.2422l-79.34-81.738c-2.8438-2.9297-2.7773-7.6094 0.15234-10.449 2.9297-2.8438 7.6094-2.7734 10.453 0.15235l79.344 81.738c2.8438 2.9258 2.7734 7.6094-0.15625 10.449-1.4375 1.3945-3.293 2.0898-5.1484 2.0898z" fill="#ff4e61"/><path d="m113.95 258.5c-1.8633 0-3.7266-0.69922-5.1641-2.1055-2.9219-2.8516-2.9766-7.5312-0.125-10.453l0.082-0.082c2.8516-2.918 7.5312-2.9766 10.453-0.12109 2.9219 2.8516 2.9766 7.5312 0.12109 10.453l-0.0781 0.082c-1.4492 1.4805-3.3672 2.2266-5.2891 2.2266z" fill="#fa0"/><path d="m131.4 276.48c-1.8555 0-3.7109-0.69531-5.1484-2.0898-2.9258-2.8438-2.9961-7.5234-0.15235-10.449l0.0781-0.0859c2.8476-2.9297 7.5273-2.9961 10.453-0.15235 2.9297 2.8438 3 7.5234 0.15625 10.453l-0.082 0.082c-1.4492 1.4922-3.375 2.2422-5.3047 2.2422z" fill="#5c73bc"/><path d="m353.24 227.99c-1.8555 0-3.7109-0.69141-5.1445-2.0859-2.9297-2.8438-3-7.5234-0.15625-10.453l79.34-81.734c2.8438-2.9297 7.5234-3 10.453-0.15625 2.9297 2.8438 3 7.5234 0.15625 10.453l-79.344 81.734c-1.4492 1.4922-3.375 2.2422-5.3047 2.2422z" fill="#fa0"/><path d="m326.52 255.6c-1.9141 0-3.8242-0.73828-5.2695-2.2109l-0.082-0.082c-2.8633-2.9141-2.8203-7.5938 0.0899-10.453 2.9141-2.8633 7.5938-2.8203 10.453 0.0898l0.082 0.082c2.8594 2.9141 2.8203 7.5938-0.0937 10.453-1.4375 1.4141-3.3086 2.1211-5.1797 2.1211z" fill="#ff4e61"/><path d="m309.07 273.58c-1.9297 0-3.8555-0.75-5.3047-2.2422l-0.082-0.082c-2.8398-2.9297-2.7734-7.6094 0.15625-10.453s7.6094-2.7734 10.453 0.15234l0.082 0.082c2.8398 2.9297 2.7734 7.6094-0.15625 10.453-1.4375 1.3945-3.293 2.0898-5.1484 2.0898z" fill="#fa0"/><path d="m300.65 116.69c-1.2422 0-2.5-0.3125-3.6523-0.97266-3.5469-2.0234-4.7812-6.5391-2.7578-10.082l56.863-99.652c2.0234-3.543 6.5352-4.7773 10.082-2.7539 3.5469 2.0234 4.7812 6.5391 2.7578 10.082l-56.863 99.652c-1.3633 2.3867-3.8594 3.7266-6.4297 3.7266z" fill="#62d38f"/><path d="m281.52 150.33c-1.293 0-2.5977-0.33593-3.7891-1.0469l-0.0976-0.0586c-3.5-2.0938-4.6445-6.6328-2.5469-10.137 2.0938-3.5078 6.6328-4.6445 10.137-2.5508l0.0977 0.0586c3.5039 2.0938 4.6445 6.6328 2.5508 10.137-1.3867 2.3164-3.8359 3.5976-6.3516 3.5976z" fill="#fa0"/><path d="m269.02 172.25c-1.3008 0-2.6172-0.34375-3.8086-1.0625l-0.0977-0.0586c-3.4961-2.1094-4.6211-6.6523-2.5156-10.148 2.1094-3.4961 6.6523-4.6172 10.148-2.5117l0.0976 0.0586c3.4961 2.1094 4.6211 6.6523 2.5117 10.148-1.3867 2.3008-3.832 3.5742-6.3359 3.5742z" fill="#2dc471"/><path d="m139.96 116.69c-2.5703 0-5.0664-1.3398-6.4297-3.7305l-56.863-99.648c-2.0234-3.5469-0.78906-8.0586 2.7539-10.082 3.5469-2.0234 8.0625-0.79297 10.086 2.7539l56.863 99.648c2.0234 3.5469 0.78906 8.0625-2.7539 10.086-1.1562 0.66016-2.4141 0.97266-3.6562 0.97266z" fill="#5c73bc"/><path d="m159.09 150.33c-2.5078 0-4.957-1.2773-6.3438-3.582-2.1016-3.5-0.96875-8.043 2.5273-10.145l0.10157-0.0586c3.5-2.1016 8.0391-0.97266 10.141 2.5273 2.1055 3.5 0.97266 8.0391-2.5273 10.145l-0.0977 0.0586c-1.1914 0.71484-2.5039 1.0547-3.8008 1.0547z" fill="#ff4e61"/><path d="m171.6 172.25c-2.5 0-4.9375-1.2656-6.3281-3.5625-2.1172-3.4922-1-8.0352 2.4883-10.152l0.0977-0.0586c3.4961-2.1133 8.0391-1 10.156 2.4922 2.1133 3.4922 1 8.0352-2.4922 10.152l-0.0977 0.0586c-1.1992 0.72656-2.5195 1.0703-3.8242 1.0703z" fill="#fa0"/><path d="m402.14 357.28-15.523 11.602c-4.0234 3.0117-9.6523-0.043-9.5234-5.1641l0.5039-19.75c0.0508-2.0352-0.87109-3.9648-2.4688-5.1641l-15.508-11.621c-4.0234-3.0156-2.9453-9.4726 1.8242-10.93l18.391-5.6094c1.8906-0.57812 3.3906-2.082 4-4.0156l5.9375-18.785c1.5391-4.875 7.8359-5.8125 10.652-1.5898l10.863 16.285c1.1211 1.6758 2.9688 2.6797 4.9414 2.6797l19.18 0.0117c4.9766 4e-3 7.7891 5.8828 4.7578 9.9492l-11.676 15.672c-1.2031 1.6172-1.5586 3.7383-0.94922 5.6719l5.918 18.797c1.5312 4.875-3.0273 9.4453-7.7148 7.7344l-18.078-6.5977c-1.8594-0.67969-3.9258-0.37109-5.5273 0.82422z" fill="#ffd02f"/><path d="m261.51 512c-4.082 0-7.3906-3.3086-7.3906-7.3906 0-57.23 22.832-95.922 41.984-118.3 20.828-24.332 41.613-35.023 42.488-35.469 3.6406-1.8477 8.0898-0.39063 9.9336 3.2539 1.8438 3.6367 0.39453 8.0781-3.2422 9.9297-0.3125 0.16016-19.5 10.164-38.367 32.395-25.227 29.719-38.016 66.121-38.016 108.2 0 4.082-3.3086 7.3906-7.3906 7.3906z" fill="#ff4e61"/><path d="m102.86 397.35 11.766 15.605c3.0547 4.0469 9.2852 2.7305 10.547-2.2266l4.8633-19.113c0.5-1.9648 1.9102-3.5547 3.7695-4.2461l18.039-6.707c4.6797-1.7383 5.3906-8.25 1.207-11.016l-16.141-10.672c-1.6602-1.1016-2.6914-2.9726-2.7578-5.0039l-0.61719-19.75c-0.15625-5.1211-5.9492-7.832-9.7969-4.5859l-14.84 12.516c-1.5312 1.2891-3.5781 1.7227-5.4726 1.1562l-18.422-5.5c-4.7773-1.4258-9.0703 3.4102-7.2617 8.1836l6.9688 18.41c0.71875 1.8945 0.48438 4.0352-0.625 5.7188l-10.77 16.348c-2.793 4.2422 0.34375 9.9375 5.3125 9.6445l19.145-1.1406c1.9727-0.11719 3.875 0.77343 5.0859 2.3789z" fill="#ffd02f"/><path d="m179.02 512c-4.082 0-7.3906-3.3086-7.3906-7.3906 0-30.059-6.6797-57.559-19.852-81.734-1.9531-3.5859-0.62891-8.0742 2.957-10.027 3.5859-1.9531 8.0742-0.62891 10.027 2.9531 14.363 26.375 21.648 56.254 21.648 88.809 0 4.082-3.3086 7.3906-7.3906 7.3906z" fill="#fa0"/><path d="m268.93 55.898c0-11.285-8.8828-20.434-19.836-20.434-10.957 0-19.836 9.1484-19.836 20.434 0 11.285 8.8789 20.438 19.836 20.438 10.953 0 19.836-9.1523 19.836-20.438z" fill="#ffd02f"/><path d="m373.08 446.81c0-11.285-8.8789-20.434-19.832-20.434-10.957 0-19.836 9.1484-19.836 20.434s8.8789 20.434 19.836 20.434c10.953 0 19.832-9.1484 19.832-20.434z" fill="#5c73bc"/><path d="m44.129 450.86c0-9.0508-7.1211-16.387-15.91-16.387-8.7852 0-15.906 7.3359-15.906 16.387 0 9.0547 7.1211 16.391 15.906 16.391 8.7891 0 15.91-7.3359 15.91-16.391z" fill="#62d38f"/><path d="m88.172 288.35c0-9.0508-7.1211-16.387-15.91-16.387-8.7852 0-15.906 7.3359-15.906 16.387s7.1211 16.391 15.906 16.391c8.7891 0 15.91-7.3398 15.91-16.391z" fill="#5c73bc"/><g fill="#ff4e61"><path d="m210.84 16.391c0-9.0547-7.1211-16.391-15.906-16.391-8.7891 0-15.91 7.3359-15.91 16.391 0 9.0508 7.1211 16.387 15.91 16.387 8.7852 0 15.906-7.3359 15.906-16.387z"/><path d="m365.23 152.88c0-9.0508-7.125-16.391-15.91-16.391-8.7852 0-15.91 7.3398-15.91 16.391s7.125 16.387 15.91 16.387c8.7852 0 15.91-7.3359 15.91-16.387z"/><path d="m139.96 32.746c-1.8555 0-3.7109-0.69141-5.1484-2.0898-2.9297-2.8438-3-7.5195-0.15625-10.449l9.1914-9.4688c2.8438-2.9297 7.5234-3 10.449-0.15625 2.9297 2.8438 3 7.5234 0.15625 10.453l-9.1875 9.4688c-1.4492 1.4922-3.3789 2.2422-5.3047 2.2422z"/></g></g></svg> diff --git a/icons/placeholder.svg b/resources/ui/images/placeholder.svg similarity index 100% rename from icons/placeholder.svg rename to resources/ui/images/placeholder.svg