From 9451f9d580e91c4700af12982665e4f117af98cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Harrault?= <benoit@harrault.fr> Date: Wed, 15 May 2024 11:42:08 +0200 Subject: [PATCH] Normalize game architecture --- README.md | 2 + android/gradle.properties | 4 +- assets/translations/en.json | 8 +- assets/translations/fr.json | 8 +- assets/{icons => ui}/button_back.png | Bin assets/ui/button_delete_saved_game.png | Bin 0 -> 5813 bytes assets/ui/button_resume_game.png | Bin 0 -> 3659 bytes assets/{icons => ui}/button_start.png | Bin assets/ui/game_fail.png | Bin 0 -> 3647 bytes assets/{icons => ui}/game_win.png | Bin assets/{icons => ui}/placeholder.png | Bin .../metadata/android/en-US/changelogs/19.txt | 1 + .../metadata/android/fr-FR/changelogs/19.txt | 1 + lib/config/default_game_settings.dart | 8 +- lib/config/default_global_settings.dart | 8 +- lib/config/menu.dart | 5 - lib/config/theme.dart | 10 +- lib/cubit/game_cubit.dart | 35 +++++-- lib/cubit/game_state.dart | 4 - lib/cubit/settings_game_cubit.dart | 11 +-- lib/cubit/settings_game_state.dart | 4 - lib/cubit/settings_global_cubit.dart | 2 +- lib/cubit/settings_global_state.dart | 4 - lib/main.dart | 62 +++++++++--- lib/models/{ => game}/board.dart | 6 +- lib/models/{ => game}/cell.dart | 4 +- lib/models/{ => game}/cell_location.dart | 2 +- lib/models/{ => game}/game.dart | 88 ++++++++++++------ lib/models/{ => settings}/settings_game.dart | 4 +- .../{ => settings}/settings_global.dart | 4 +- lib/models/types.dart | 0 lib/ui/game/game_end.dart | 52 +++++++++++ .../indicator_top.dart => game/game_top.dart} | 6 +- lib/ui/helpers/app_titles.dart | 32 +++++++ lib/ui/helpers/outlined_text_widget.dart | 51 ++++++++++ lib/ui/layouts/game_layout.dart | 38 ++++++++ .../parameters_layout.dart} | 77 +++++++++++---- lib/ui/parameters/parameter_image.dart | 38 ++++++++ .../parameter_painter.dart | 10 +- lib/ui/screens/page_about.dart | 4 +- lib/ui/screens/page_game.dart | 11 ++- lib/ui/screens/page_settings.dart | 6 +- .../{widgets => }/settings/settings_form.dart | 2 +- lib/ui/{widgets => }/settings/theme_card.dart | 0 lib/ui/skeleton.dart | 4 +- .../actions/button_delete_saved_game.dart | 21 +++++ lib/ui/widgets/actions/button_game_quit.dart | 21 +++++ .../{ => actions}/button_game_start_new.dart | 14 +-- .../actions/button_resume_saved_game.dart | 21 +++++ .../game/{tileset.dart => game_board.dart} | 6 +- lib/ui/widgets/game/game_widget.dart | 38 -------- lib/ui/widgets/game/message_game_end.dart | 56 ----------- lib/ui/widgets/game/tile_widget.dart | 8 +- lib/ui/widgets/global_app_bar.dart | 13 ++- lib/ui/widgets/helpers/app_header.dart | 24 ----- lib/ui/widgets/helpers/app_title.dart | 17 ---- lib/utils/color_extensions.dart | 33 +++++++ pubspec.lock | 32 +++---- pubspec.yaml | 9 +- .../app/build_application_resources.sh | 2 +- {icons => resources/app}/featureGraphic.svg | 0 {icons => resources/app}/icon.svg | 0 resources/build_resources.sh | 7 ++ .../ui/build_ui_resources.sh | 67 ++++++------- .../ui/images}/button_back.svg | 0 .../ui/images/button_delete_saved_game.svg | 2 + resources/ui/images/button_resume_game.svg | 2 + .../ui/images}/button_start.svg | 0 resources/ui/images/game_fail.svg | 2 + {icons => resources/ui/images}/game_win.svg | 0 .../ui/images}/placeholder.svg | 0 .../ui}/skins/default/board.svg | 0 .../ui}/skins/default/hole.svg | 0 {icons => resources/ui}/skins/default/peg.svg | 0 74 files changed, 657 insertions(+), 354 deletions(-) rename assets/{icons => ui}/button_back.png (100%) create mode 100644 assets/ui/button_delete_saved_game.png create mode 100644 assets/ui/button_resume_game.png rename assets/{icons => ui}/button_start.png (100%) create mode 100644 assets/ui/game_fail.png rename assets/{icons => ui}/game_win.png (100%) rename assets/{icons => ui}/placeholder.png (100%) create mode 100644 fastlane/metadata/android/en-US/changelogs/19.txt create mode 100644 fastlane/metadata/android/fr-FR/changelogs/19.txt rename lib/models/{ => game}/board.dart (96%) rename lib/models/{ => game}/cell.dart (86%) rename lib/models/{ => game}/cell_location.dart (92%) rename lib/models/{ => game}/game.dart (60%) rename lib/models/{ => settings}/settings_game.dart (91%) rename lib/models/{ => settings}/settings_global.dart (91%) delete mode 100644 lib/models/types.dart create mode 100644 lib/ui/game/game_end.dart rename lib/ui/{widgets/game/indicator_top.dart => game/game_top.dart} (91%) create mode 100644 lib/ui/helpers/app_titles.dart create mode 100644 lib/ui/helpers/outlined_text_widget.dart create mode 100644 lib/ui/layouts/game_layout.dart rename lib/ui/{widgets/parameters.dart => layouts/parameters_layout.dart} (55%) create mode 100644 lib/ui/parameters/parameter_image.dart rename lib/ui/{painters => parameters}/parameter_painter.dart (94%) rename lib/ui/{widgets => }/settings/settings_form.dart (96%) rename lib/ui/{widgets => }/settings/theme_card.dart (100%) create mode 100644 lib/ui/widgets/actions/button_delete_saved_game.dart create mode 100644 lib/ui/widgets/actions/button_game_quit.dart rename lib/ui/widgets/{ => actions}/button_game_start_new.dart (72%) create mode 100644 lib/ui/widgets/actions/button_resume_saved_game.dart rename lib/ui/widgets/game/{tileset.dart => game_board.dart} (90%) delete mode 100644 lib/ui/widgets/game/game_widget.dart delete mode 100644 lib/ui/widgets/game/message_game_end.dart delete mode 100644 lib/ui/widgets/helpers/app_header.dart delete mode 100644 lib/ui/widgets/helpers/app_title.dart create mode 100644 lib/utils/color_extensions.dart rename icons/build_application_icons.sh => resources/app/build_application_resources.sh (98%) rename {icons => resources/app}/featureGraphic.svg (100%) rename {icons => resources/app}/icon.svg (100%) create mode 100755 resources/build_resources.sh rename icons/build_game_icons.sh => resources/ui/build_ui_resources.sh (54%) rename {icons => resources/ui/images}/button_back.svg (100%) create mode 100644 resources/ui/images/button_delete_saved_game.svg create mode 100644 resources/ui/images/button_resume_game.svg rename {icons => resources/ui/images}/button_start.svg (100%) create mode 100644 resources/ui/images/game_fail.svg rename {icons => resources/ui/images}/game_win.svg (100%) rename {icons => resources/ui/images}/placeholder.svg (100%) rename {icons => resources/ui}/skins/default/board.svg (100%) rename {icons => resources/ui}/skins/default/hole.svg (100%) rename {icons => resources/ui}/skins/default/peg.svg (100%) diff --git a/README.md b/README.md index 594b8fe..5359b04 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ # Solitaire Game + +Simple, easy, with 4 available layouts. diff --git a/android/gradle.properties b/android/gradle.properties index 30298b3..a392afe 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.18 -app.versionCode=18 +app.versionName=0.1.0 +app.versionCode=19 diff --git a/assets/translations/en.json b/assets/translations/en.json index 13da5fc..daff9b7 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -1,14 +1,12 @@ { "app_name": "Solitaire", - "bottom_nav_home": "Game", - "bottom_nav_settings": "Settings", - "bottom_nav_about": "About", - "settings_title": "Settings", "settings_label_theme": "Theme mode", "about_title": "About", "about_content": "Solitaire.", - "about_version": "Version: {version}" + "about_version": "Version: {version}", + + "": "" } diff --git a/assets/translations/fr.json b/assets/translations/fr.json index 0d43876..ebc19d9 100644 --- a/assets/translations/fr.json +++ b/assets/translations/fr.json @@ -1,14 +1,12 @@ { "app_name": "Solitaire", - "bottom_nav_home": "Jeu", - "bottom_nav_settings": "Réglages", - "bottom_nav_about": "Infos", - "settings_title": "Réglages", "settings_label_theme": "Thème de couleurs", "about_title": "Informations", "about_content": "Solitaire.", - "about_version": "Version : {version}" + "about_version": "Version : {version}", + + "": "" } diff --git a/assets/icons/button_back.png b/assets/ui/button_back.png similarity index 100% rename from assets/icons/button_back.png rename to assets/ui/button_back.png diff --git a/assets/ui/button_delete_saved_game.png b/assets/ui/button_delete_saved_game.png new file mode 100644 index 0000000000000000000000000000000000000000..5e4f217689b11e444b7163557d7e5d68f3bbfe7d GIT binary patch literal 5813 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 literal 0 HcmV?d00001 diff --git a/assets/ui/button_resume_game.png b/assets/ui/button_resume_game.png new file mode 100644 index 0000000000000000000000000000000000000000..b2ea0a02d05e42377eb551a4b51428b511a32f5d GIT binary patch literal 3659 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 literal 0 HcmV?d00001 diff --git a/assets/icons/button_start.png b/assets/ui/button_start.png similarity index 100% rename from assets/icons/button_start.png rename to assets/ui/button_start.png diff --git a/assets/ui/game_fail.png b/assets/ui/game_fail.png new file mode 100644 index 0000000000000000000000000000000000000000..93f2801f9d6bb2ce508e1293cd64d6ff2e9970ec GIT binary patch literal 3647 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 a*}uy$dF>W!zRtkFz~JfX=d#Wzp$P!#%#zLk literal 0 HcmV?d00001 diff --git a/assets/icons/game_win.png b/assets/ui/game_win.png similarity index 100% rename from assets/icons/game_win.png rename to assets/ui/game_win.png 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/19.txt b/fastlane/metadata/android/en-US/changelogs/19.txt new file mode 100644 index 0000000..d4afd51 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/19.txt @@ -0,0 +1 @@ +Improve/normalize game architecture. diff --git a/fastlane/metadata/android/fr-FR/changelogs/19.txt b/fastlane/metadata/android/fr-FR/changelogs/19.txt new file mode 100644 index 0000000..6a9871a --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/19.txt @@ -0,0 +1 @@ +Amélioration/normalisation de l'architecture du jeu. diff --git a/lib/config/default_game_settings.dart b/lib/config/default_game_settings.dart index 6ac27dc..6548656 100644 --- a/lib/config/default_game_settings.dart +++ b/lib/config/default_game_settings.dart @@ -1,4 +1,4 @@ -import 'package:solitaire/utils/tools.dart'; +import 'package:solitaire/utils/tools.dart'; class DefaultGameSettings { // available game parameters codes @@ -21,6 +21,7 @@ class DefaultGameSettings { // layout: default value static const String defaultLayoutValue = layoutValueEnglish; + // available values from parameter code static List<String> getAvailableValues(String parameterCode) { switch (parameterCode) { case parameterCodeLayout: @@ -30,4 +31,9 @@ class DefaultGameSettings { 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 622a8b6..5e6ddc0 100644 --- a/lib/config/default_global_settings.dart +++ b/lib/config/default_global_settings.dart @@ -1,4 +1,4 @@ -import 'package:solitaire/utils/tools.dart'; +import 'package:solitaire/utils/tools.dart'; class DefaultGlobalSettings { // available global parameters codes @@ -15,6 +15,7 @@ class DefaultGlobalSettings { // skin: default value static const String defaultSkinValue = skinValueDefault; + // available values from parameter code static List<String> getAvailableValues(String parameterCode) { switch (parameterCode) { case parameterCodeSkin: @@ -24,4 +25,9 @@ class DefaultGlobalSettings { 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 3d2fcfa..dcb8a6f 100644 --- a/lib/config/menu.dart +++ b/lib/config/menu.dart @@ -6,12 +6,10 @@ import 'package:solitaire/ui/screens/page_game.dart'; import 'package:solitaire/ui/screens/page_settings.dart'; class MenuItem { - final String code; final Icon icon; final Widget page; const MenuItem({ - required this.code, required this.icon, required this.page, }); @@ -20,21 +18,18 @@ class MenuItem { class Menu { static const indexGame = 0; static const menuItemGame = MenuItem( - code: 'bottom_nav_game', icon: Icon(UniconsLine.home), page: PageGame(), ); static const indexSettings = 1; static const menuItemSettings = MenuItem( - code: 'bottom_nav_settings', icon: Icon(UniconsLine.setting), page: PageSettings(), ); static const indexAbout = 2; static const menuItemAbout = MenuItem( - code: 'bottom_nav_about', icon: Icon(UniconsLine.info_circle), page: PageAbout(), ); diff --git a/lib/config/theme.dart b/lib/config/theme.dart index be39034..74f532f 100644 --- a/lib/config/theme.dart +++ b/lib/config/theme.dart @@ -39,11 +39,9 @@ final ColorScheme lightColorScheme = ColorScheme.light( secondary: primarySwatch.shade500, onSecondary: Colors.white, error: errorColor, - background: textSwatch.shade200, - onBackground: textSwatch.shade500, onSurface: textSwatch.shade500, surface: textSwatch.shade50, - surfaceVariant: Colors.white, + surfaceContainerHighest: Colors.white, shadow: textSwatch.shade900.withOpacity(.1), ); @@ -52,11 +50,9 @@ final ColorScheme darkColorScheme = ColorScheme.dark( secondary: primarySwatch.shade500, onSecondary: Colors.white, error: errorColor, - background: const Color(0xFF171724), - onBackground: textSwatch.shade400, onSurface: textSwatch.shade300, surface: const Color(0xFF262630), - surfaceVariant: const Color(0xFF282832), + surfaceContainerHighest: const Color(0xFF282832), shadow: textSwatch.shade900.withOpacity(.2), ); @@ -192,5 +188,3 @@ final ThemeData darkTheme = lightTheme.copyWith( ), ), ); - -final ThemeData appTheme = darkTheme; diff --git a/lib/cubit/game_cubit.dart b/lib/cubit/game_cubit.dart index aba2472..1ce79e0 100644 --- a/lib/cubit/game_cubit.dart +++ b/lib/cubit/game_cubit.dart @@ -2,10 +2,10 @@ import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; -import 'package:solitaire/models/cell_location.dart'; -import 'package:solitaire/models/game.dart'; -import 'package:solitaire/models/settings_game.dart'; -import 'package:solitaire/models/settings_global.dart'; +import 'package:solitaire/models/game/cell_location.dart'; +import 'package:solitaire/models/game/game.dart'; +import 'package:solitaire/models/settings/settings_game.dart'; +import 'package:solitaire/models/settings/settings_global.dart'; import 'package:solitaire/utils/tools.dart'; part 'game_state.dart'; @@ -13,7 +13,7 @@ part 'game_state.dart'; class GameCubit extends HydratedCubit<GameState> { GameCubit() : super(GameState( - currentGame: Game.createNull(), + currentGame: Game.createEmpty(), )); void updateState(Game game) { @@ -24,11 +24,17 @@ class GameCubit extends HydratedCubit<GameState> { void refresh() { final Game game = Game( + // Settings gameSettings: state.currentGame.gameSettings, globalSettings: state.currentGame.globalSettings, - board: state.currentGame.board, + // State isRunning: state.currentGame.isRunning, + isStarted: state.currentGame.isStarted, isFinished: state.currentGame.isFinished, + animationInProgress: state.currentGame.animationInProgress, + // Base data + board: state.currentGame.board, + // Game data movesCount: state.currentGame.movesCount, remainingPegsCount: state.currentGame.remainingPegsCount, allowedMovesCount: state.currentGame.allowedMovesCount, @@ -42,7 +48,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, ); @@ -62,12 +69,24 @@ class GameCubit extends HydratedCubit<GameState> { refresh(); } + void resumeSavedGame() { + state.currentGame.isRunning = true; + refresh(); + } + + void deleteSavedGame() { + state.currentGame.isRunning = false; + state.currentGame.isFinished = true; + refresh(); + } + void updatePegValue(CellLocation location, bool hasPeg) { state.currentGame.board.cells[location.row][location.col].hasPeg = hasPeg; refresh(); } void incrementMovesCount() { + state.currentGame.isStarted = true; state.currentGame.movesCount++; refresh(); } @@ -111,7 +130,7 @@ class GameCubit extends HydratedCubit<GameState> { @override GameState? fromJson(Map<String, dynamic> json) { - Game currentGame = json['currentGame'] as Game; + final Game currentGame = json['currentGame'] as Game; return GameState( currentGame: currentGame, diff --git a/lib/cubit/game_state.dart b/lib/cubit/game_state.dart index 3fd161a..00e2116 100644 --- a/lib/cubit/game_state.dart +++ b/lib/cubit/game_state.dart @@ -12,8 +12,4 @@ class GameState extends Equatable { List<dynamic> get props => <dynamic>[ currentGame, ]; - - Map<String, dynamic> get values => <String, dynamic>{ - 'currentGame': currentGame, - }; } diff --git a/lib/cubit/settings_game_cubit.dart b/lib/cubit/settings_game_cubit.dart index 7399314..18848ab 100644 --- a/lib/cubit/settings_game_cubit.dart +++ b/lib/cubit/settings_game_cubit.dart @@ -3,8 +3,7 @@ import 'package:flutter/material.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:solitaire/config/default_game_settings.dart'; -import 'package:solitaire/models/settings_game.dart'; -import 'package:solitaire/utils/tools.dart'; +import 'package:solitaire/models/settings/settings_game.dart'; part 'settings_game_state.dart'; @@ -28,14 +27,12 @@ class GameSettingsCubit extends HydratedCubit<GameSettingsState> { case DefaultGameSettings.parameterCodeLayout: return GameSettings.getLayoutValueFromUnsafe(state.settings.layout); } + return ''; } void setParameterValue(String code, String value) { - printlog('GameSettingsCubit.setParameterValue'); - printlog('code: $code / value: $value'); - - String layout = code == DefaultGameSettings.parameterCodeLayout + final String layout = code == DefaultGameSettings.parameterCodeLayout ? value : getParameterValue(DefaultGameSettings.parameterCodeLayout); @@ -46,7 +43,7 @@ class GameSettingsCubit extends HydratedCubit<GameSettingsState> { @override GameSettingsState? fromJson(Map<String, dynamic> json) { - String layout = json[DefaultGameSettings.parameterCodeLayout] as String; + final String layout = json[DefaultGameSettings.parameterCodeLayout] as String; return GameSettingsState( settings: GameSettings( diff --git a/lib/cubit/settings_game_state.dart b/lib/cubit/settings_game_state.dart index b773dc6..5acd85b 100644 --- a/lib/cubit/settings_game_state.dart +++ b/lib/cubit/settings_game_state.dart @@ -12,8 +12,4 @@ class GameSettingsState extends Equatable { List<dynamic> get props => <dynamic>[ settings, ]; - - Map<String, dynamic> get values => <String, dynamic>{ - 'settings': settings, - }; } diff --git a/lib/cubit/settings_global_cubit.dart b/lib/cubit/settings_global_cubit.dart index 274c113..a404213 100644 --- a/lib/cubit/settings_global_cubit.dart +++ b/lib/cubit/settings_global_cubit.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:solitaire/config/default_global_settings.dart'; -import 'package:solitaire/models/settings_global.dart'; +import 'package:solitaire/models/settings/settings_global.dart'; part 'settings_global_state.dart'; diff --git a/lib/cubit/settings_global_state.dart b/lib/cubit/settings_global_state.dart index 4e4fbdf..ebcddd7 100644 --- a/lib/cubit/settings_global_state.dart +++ b/lib/cubit/settings_global_state.dart @@ -12,8 +12,4 @@ class GlobalSettingsState extends Equatable { List<dynamic> get props => <dynamic>[ settings, ]; - - Map<String, dynamic> get values => <String, dynamic>{ - 'settings': settings, - }; } diff --git a/lib/main.dart b/lib/main.dart index d507c2d..48897d6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,11 +2,13 @@ import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hive/hive.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:solitaire/config/default_global_settings.dart'; import 'package:solitaire/config/theme.dart'; import 'package:solitaire/cubit/game_cubit.dart'; import 'package:solitaire/cubit/nav_cubit.dart'; @@ -25,18 +27,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 +45,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 +79,36 @@ 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'); + } + + final List<String> skinImages = [ + 'board', + 'hole', + 'peg', + ]; + + for (String skin in DefaultGlobalSettings.allowedSkinValues) { + for (String image in skinImages) { + assets.add('assets/skins/${skin}_$image.png'); + } + } + + return assets; + } } diff --git a/lib/models/board.dart b/lib/models/game/board.dart similarity index 96% rename from lib/models/board.dart rename to lib/models/game/board.dart index eb1ce73..2834c6d 100644 --- a/lib/models/board.dart +++ b/lib/models/game/board.dart @@ -1,7 +1,7 @@ import 'package:solitaire/data/game_data.dart'; -import 'package:solitaire/models/cell.dart'; -import 'package:solitaire/models/cell_location.dart'; -import 'package:solitaire/models/settings_game.dart'; +import 'package:solitaire/models/game/cell.dart'; +import 'package:solitaire/models/game/cell_location.dart'; +import 'package:solitaire/models/settings/settings_game.dart'; import 'package:solitaire/utils/tools.dart'; typedef BoardCells = List<List<Cell>>; diff --git a/lib/models/cell.dart b/lib/models/game/cell.dart similarity index 86% rename from lib/models/cell.dart rename to lib/models/game/cell.dart index 127c300..91b6d5e 100644 --- a/lib/models/cell.dart +++ b/lib/models/game/cell.dart @@ -1,5 +1,5 @@ -import 'package:solitaire/models/cell_location.dart'; -import 'package:solitaire/utils/tools.dart'; +import 'package:solitaire/models/game/cell_location.dart'; +import 'package:solitaire/utils/tools.dart'; class Cell { Cell({ diff --git a/lib/models/cell_location.dart b/lib/models/game/cell_location.dart similarity index 92% rename from lib/models/cell_location.dart rename to lib/models/game/cell_location.dart index f9b9022..f0fc583 100644 --- a/lib/models/cell_location.dart +++ b/lib/models/game/cell_location.dart @@ -1,4 +1,4 @@ -import 'package:solitaire/utils/tools.dart'; +import 'package:solitaire/utils/tools.dart'; class CellLocation { final int col; diff --git a/lib/models/game.dart b/lib/models/game/game.dart similarity index 60% rename from lib/models/game.dart rename to lib/models/game/game.dart index 009ff4f..76f09e5 100644 --- a/lib/models/game.dart +++ b/lib/models/game/game.dart @@ -1,37 +1,54 @@ -import 'package:solitaire/models/board.dart'; -import 'package:solitaire/models/cell.dart'; -import 'package:solitaire/models/settings_game.dart'; -import 'package:solitaire/models/settings_global.dart'; +import 'package:solitaire/models/game/board.dart'; +import 'package:solitaire/models/game/cell.dart'; +import 'package:solitaire/models/settings/settings_game.dart'; +import 'package:solitaire/models/settings/settings_global.dart'; import 'package:solitaire/utils/tools.dart'; class Game { Game({ + // Settings required this.gameSettings, required this.globalSettings, - required this.board, + + // State this.isRunning = false, + this.isStarted = false, this.isFinished = false, + this.animationInProgress = false, + + // Base data + required this.board, + + // Game data this.movesCount = 0, this.remainingPegsCount = 0, this.allowedMovesCount = 0, }); + // Settings final GameSettings gameSettings; final GlobalSettings globalSettings; - bool isRunning = false; - bool isFinished = false; + // State + bool isRunning; + bool isStarted; + bool isFinished; + bool animationInProgress; - Board board; + // Base data + final Board board; - int movesCount = 0; - int remainingPegsCount = 0; - int allowedMovesCount = 0; + // Game data + int movesCount; + int remainingPegsCount; + int allowedMovesCount; - factory Game.createNull() { + factory Game.createEmpty() { return Game( + // Settings gameSettings: GameSettings.createDefault(), globalSettings: GlobalSettings.createDefault(), + // Base data board: Board.createEmpty(), ); } @@ -40,22 +57,25 @@ 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(); return Game( + // Settings gameSettings: newGameSettings, globalSettings: newGlobalSettings, - board: Board.createNew( - gameSettings: newGameSettings, - ), + // State isRunning: true, + // Base data + board: Board.createNew(gameSettings: newGameSettings), ); } - int get boardSize => board.boardSize; + bool get canBeResumed => isStarted && !isFinished; + + bool get gameWon => isRunning && isStarted && isFinished; - bool get gameWon => (isFinished && (remainingPegsCount == 1)); + int get boardSize => board.boardSize; List<Cell> listRemainingPegs() { final List<Cell> pegs = []; @@ -106,19 +126,21 @@ class Game { void dump() { printlog(''); printlog('## Current game dump:'); - printlog(''); + printlog('$Game:'); + printlog(' Settings'); gameSettings.dump(); globalSettings.dump(); - printlog(''); - printlog(''); - printlog('$Game: '); - printlog(' isRunning: $isRunning'); - printlog(' isFinished: $isFinished'); - printlog(' movesCount: $movesCount'); - printlog(' remainingPegsCount: $remainingPegsCount'); - printlog(' allowedMovesCount: $allowedMovesCount'); - printlog(''); + printlog(' State'); + printlog(' isRunning: $isRunning'); + printlog(' isStarted: $isStarted'); + printlog(' isFinished: $isFinished'); + printlog(' animationInProgress: $animationInProgress'); + printlog(' Base data'); board.dump(); + printlog(' Game data'); + printlog(' movesCount: $movesCount'); + printlog(' remainingPegsCount: $remainingPegsCount'); + printlog(' allowedMovesCount: $allowedMovesCount'); printlog(''); } @@ -129,11 +151,17 @@ class Game { Map<String, dynamic>? toJson() { return <String, dynamic>{ + // Settings 'gameSettings': gameSettings.toJson(), 'globalSettings': globalSettings.toJson(), - 'board': board.toJson(), + // State 'isRunning': isRunning, + 'isStarted': isStarted, 'isFinished': isFinished, + 'animationInProgress': animationInProgress, + // Base data + 'board': board.toJson(), + // Game data 'movesCount': movesCount, 'remainingPegsCount': remainingPegsCount, 'allowedMovesCount': allowedMovesCount, diff --git a/lib/models/settings_game.dart b/lib/models/settings/settings_game.dart similarity index 91% rename from lib/models/settings_game.dart rename to lib/models/settings/settings_game.dart index cb00bc9..006584e 100644 --- a/lib/models/settings_game.dart +++ b/lib/models/settings/settings_game.dart @@ -1,5 +1,5 @@ import 'package:solitaire/config/default_game_settings.dart'; -import 'package:solitaire/utils/tools.dart'; +import 'package:solitaire/utils/tools.dart'; class GameSettings { final String layout; @@ -23,7 +23,7 @@ class GameSettings { } void dump() { - printlog('$GameSettings: '); + printlog('$GameSettings:'); printlog(' ${DefaultGameSettings.parameterCodeLayout}: $layout'); printlog(''); } diff --git a/lib/models/settings_global.dart b/lib/models/settings/settings_global.dart similarity index 91% rename from lib/models/settings_global.dart rename to lib/models/settings/settings_global.dart index dd4b7c8..e67b92d 100644 --- a/lib/models/settings_global.dart +++ b/lib/models/settings/settings_global.dart @@ -1,5 +1,5 @@ import 'package:solitaire/config/default_global_settings.dart'; -import 'package:solitaire/utils/tools.dart'; +import 'package:solitaire/utils/tools.dart'; class GlobalSettings { String skin; @@ -23,7 +23,7 @@ class GlobalSettings { } void dump() { - printlog('$GlobalSettings: '); + printlog('$GlobalSettings:'); printlog(' ${DefaultGlobalSettings.parameterCodeSkin}: $skin'); printlog(''); } diff --git a/lib/models/types.dart b/lib/models/types.dart deleted file mode 100644 index e69de29..0000000 diff --git a/lib/ui/game/game_end.dart b/lib/ui/game/game_end.dart new file mode 100644 index 0000000..7f0d3f7 --- /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:solitaire/cubit/game_cubit.dart'; +import 'package:solitaire/models/game/game.dart'; +import 'package:solitaire/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/widgets/game/indicator_top.dart b/lib/ui/game/game_top.dart similarity index 91% rename from lib/ui/widgets/game/indicator_top.dart rename to lib/ui/game/game_top.dart index bc66f79..b6319e7 100644 --- a/lib/ui/widgets/game/indicator_top.dart +++ b/lib/ui/game/game_top.dart @@ -2,10 +2,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:solitaire/cubit/game_cubit.dart'; -import 'package:solitaire/models/game.dart'; +import 'package:solitaire/models/game/game.dart'; -class TopIndicator extends StatelessWidget { - const TopIndicator({super.key}); +class GameTopWidget extends StatelessWidget { + const GameTopWidget({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/ui/helpers/app_titles.dart b/lib/ui/helpers/app_titles.dart new file mode 100644 index 0000000..b98107b --- /dev/null +++ b/lib/ui/helpers/app_titles.dart @@ -0,0 +1,32 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; + +class AppHeader extends StatelessWidget { + const AppHeader({super.key, required this.text}); + + final String text; + + @override + Widget build(BuildContext context) { + return Text( + tr(text), + textAlign: TextAlign.start, + style: Theme.of(context).textTheme.headlineMedium!.apply(fontWeightDelta: 2), + ); + } +} + +class AppTitle extends StatelessWidget { + const AppTitle({super.key, required this.text}); + + final String text; + + @override + Widget build(BuildContext context) { + return Text( + tr(text), + textAlign: TextAlign.start, + style: Theme.of(context).textTheme.titleLarge!.apply(fontWeightDelta: 2), + ); + } +} diff --git a/lib/ui/helpers/outlined_text_widget.dart b/lib/ui/helpers/outlined_text_widget.dart new file mode 100644 index 0000000..0fbd955 --- /dev/null +++ b/lib/ui/helpers/outlined_text_widget.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; + +import 'package:solitaire/utils/color_extensions.dart'; + +class OutlinedText extends StatelessWidget { + const OutlinedText({ + super.key, + required this.text, + required this.fontSize, + required this.textColor, + this.outlineColor, + }); + + final String text; + final double fontSize; + final Color textColor; + final Color? outlineColor; + + @override + Widget build(BuildContext context) { + final double delta = fontSize / 30; + + return Text( + text, + style: TextStyle( + inherit: true, + fontSize: fontSize, + fontWeight: FontWeight.w600, + color: textColor, + shadows: [ + Shadow( + offset: Offset(-delta, -delta), + color: outlineColor ?? textColor.darken(), + ), + Shadow( + offset: Offset(delta, -delta), + color: outlineColor ?? textColor.darken(), + ), + Shadow( + offset: Offset(delta, delta), + color: outlineColor ?? textColor.darken(), + ), + Shadow( + offset: Offset(-delta, delta), + color: outlineColor ?? textColor.darken(), + ), + ], + ), + ); + } +} diff --git a/lib/ui/layouts/game_layout.dart b/lib/ui/layouts/game_layout.dart new file mode 100644 index 0000000..3635646 --- /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:solitaire/cubit/game_cubit.dart'; +import 'package:solitaire/models/game/game.dart'; +import 'package:solitaire/ui/game/game_end.dart'; +import 'package:solitaire/ui/game/game_top.dart'; +import 'package:solitaire/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 55% rename from lib/ui/widgets/parameters.dart rename to lib/ui/layouts/parameters_layout.dart index 11b080d..d052020 100644 --- a/lib/ui/widgets/parameters.dart +++ b/lib/ui/layouts/parameters_layout.dart @@ -5,11 +5,16 @@ import 'package:solitaire/config/default_game_settings.dart'; import 'package:solitaire/config/default_global_settings.dart'; import 'package:solitaire/cubit/settings_game_cubit.dart'; import 'package:solitaire/cubit/settings_global_cubit.dart'; -import 'package:solitaire/ui/painters/parameter_painter.dart'; -import 'package:solitaire/ui/widgets/button_game_start_new.dart'; +import 'package:solitaire/ui/parameters/parameter_image.dart'; +import 'package:solitaire/ui/parameters/parameter_painter.dart'; +import 'package:solitaire/ui/widgets/actions/button_delete_saved_game.dart'; +import 'package:solitaire/ui/widgets/actions/button_game_start_new.dart'; +import 'package:solitaire/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 @@ -85,22 +107,39 @@ class Parameters extends StatelessWidget { final double displayWidth = MediaQuery.of(context).size.width; final double itemWidth = displayWidth / availableValues.length - 26; + final bool displayedWithAssets = + DefaultGlobalSettings.displayedWithAssets.contains(code) || + DefaultGameSettings.displayedWithAssets.contains(code); + return TextButton( - 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: Container( + 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) - : gameSettingsCubit.setParameterValue(code, value), + onPressed: () { + isGlobal + ? globalSettingsCubit.setParameterValue(code, value) + : gameSettingsCubit.setParameterValue(code, value); + }, ); }, ); diff --git a/lib/ui/parameters/parameter_image.dart b/lib/ui/parameters/parameter_image.dart new file mode 100644 index 0000000..fc4b576 --- /dev/null +++ b/lib/ui/parameters/parameter_image.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +class ParameterImage extends StatelessWidget { + const ParameterImage({ + super.key, + required this.code, + required this.value, + required this.isSelected, + }); + + final String code; + final String value; + final bool isSelected; + + static const Color buttonBackgroundColor = Colors.white; + static const Color buttonBorderColorActive = Colors.blue; + static const Color buttonBorderColorInactive = Colors.white; + static const double buttonBorderWidth = 8.0; + static const double buttonBorderRadius = 8.0; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: buttonBackgroundColor, + borderRadius: BorderRadius.circular(buttonBorderRadius), + border: Border.all( + color: isSelected ? buttonBorderColorActive : buttonBorderColorInactive, + width: buttonBorderWidth, + ), + ), + child: Image( + image: AssetImage('assets/ui/${code}_$value.png'), + fit: BoxFit.fill, + ), + ); + } +} diff --git a/lib/ui/painters/parameter_painter.dart b/lib/ui/parameters/parameter_painter.dart similarity index 94% rename from lib/ui/painters/parameter_painter.dart rename to lib/ui/parameters/parameter_painter.dart index 4ed92d7..9b86f6f 100644 --- a/lib/ui/painters/parameter_painter.dart +++ b/lib/ui/parameters/parameter_painter.dart @@ -4,11 +4,11 @@ import 'package:flutter/material.dart'; import 'package:solitaire/config/default_game_settings.dart'; import 'package:solitaire/data/game_data.dart'; -import 'package:solitaire/models/board.dart'; -import 'package:solitaire/models/cell.dart'; -import 'package:solitaire/models/cell_location.dart'; -import 'package:solitaire/models/settings_game.dart'; -import 'package:solitaire/models/settings_global.dart'; +import 'package:solitaire/models/game/board.dart'; +import 'package:solitaire/models/game/cell.dart'; +import 'package:solitaire/models/game/cell_location.dart'; +import 'package:solitaire/models/settings/settings_game.dart'; +import 'package:solitaire/models/settings/settings_global.dart'; import 'package:solitaire/utils/tools.dart'; class ParameterPainter extends CustomPainter { diff --git a/lib/ui/screens/page_about.dart b/lib/ui/screens/page_about.dart index f63e153..d4e5947 100644 --- a/lib/ui/screens/page_about.dart +++ b/lib/ui/screens/page_about.dart @@ -2,7 +2,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:package_info_plus/package_info_plus.dart'; -import 'package:solitaire/ui/widgets/helpers/app_header.dart'; +import 'package:solitaire/ui/helpers/app_titles.dart'; class PageAbout extends StatelessWidget { const PageAbout({super.key}); @@ -17,7 +17,7 @@ class PageAbout extends StatelessWidget { mainAxisSize: MainAxisSize.max, children: <Widget>[ const SizedBox(height: 8), - const AppHeader(text: 'about_title'), + const AppTitle(text: 'about_title'), const Text('about_content').tr(), FutureBuilder<PackageInfo>( future: PackageInfo.fromPlatform(), diff --git a/lib/ui/screens/page_game.dart b/lib/ui/screens/page_game.dart index 287a383..2a5509f 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:solitaire/cubit/game_cubit.dart'; -import 'package:solitaire/ui/widgets/game/game_widget.dart'; -import 'package:solitaire/ui/widgets/parameters.dart'; +import 'package:solitaire/models/game/game.dart'; +import 'package:solitaire/ui/layouts/game_layout.dart'; +import 'package:solitaire/ui/layouts/parameters_layout.dart'; class PageGame extends StatelessWidget { const PageGame({super.key}); @@ -12,7 +13,11 @@ class PageGame extends StatelessWidget { Widget build(BuildContext context) { return BlocBuilder<GameCubit, GameState>( builder: (BuildContext context, GameState gameState) { - return gameState.currentGame.isRunning ? const GameWidget() : const Parameters(); + 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 ae65ab9..868ad1a 100644 --- a/lib/ui/screens/page_settings.dart +++ b/lib/ui/screens/page_settings.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:solitaire/ui/widgets/helpers/app_header.dart'; -import 'package:solitaire/ui/widgets/settings/settings_form.dart'; +import 'package:solitaire/ui/helpers/app_titles.dart'; +import 'package:solitaire/ui/settings/settings_form.dart'; class PageSettings extends StatelessWidget { const PageSettings({super.key}); @@ -16,7 +16,7 @@ class PageSettings extends StatelessWidget { mainAxisSize: MainAxisSize.max, children: <Widget>[ SizedBox(height: 8), - AppHeader(text: 'settings_title'), + 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 91e9fd5..5b03fe6 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:solitaire/ui/widgets/settings/theme_card.dart'; +import 'package:solitaire/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 c57f5c2..eb5336e 100644 --- a/lib/ui/skeleton.dart +++ b/lib/ui/skeleton.dart @@ -14,7 +14,7 @@ class SkeletonScreen extends StatelessWidget { appBar: const GlobalAppBar(), extendBodyBehindAppBar: false, body: Material( - color: Theme.of(context).colorScheme.background, + color: Theme.of(context).colorScheme.surface, child: BlocBuilder<NavCubit, int>( builder: (BuildContext context, int pageIndex) { return Padding( @@ -28,7 +28,7 @@ class SkeletonScreen extends StatelessWidget { }, ), ), - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: Theme.of(context).colorScheme.surface, ); } } diff --git a/lib/ui/widgets/actions/button_delete_saved_game.dart b/lib/ui/widgets/actions/button_delete_saved_game.dart new file mode 100644 index 0000000..df484d7 --- /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:solitaire/cubit/game_cubit.dart'; + +class DeleteSavedGameButton extends StatelessWidget { + const DeleteSavedGameButton({super.key}); + + @override + Widget build(BuildContext context) { + return TextButton( + child: const Image( + image: AssetImage('assets/ui/button_delete_saved_game.png'), + fit: BoxFit.fill, + ), + onPressed: () { + BlocProvider.of<GameCubit>(context).deleteSavedGame(); + }, + ); + } +} diff --git a/lib/ui/widgets/actions/button_game_quit.dart b/lib/ui/widgets/actions/button_game_quit.dart new file mode 100644 index 0000000..a90e6cb --- /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:solitaire/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 cd4b8f0..e4a4b59 100644 --- a/lib/ui/widgets/button_game_start_new.dart +++ b/lib/ui/widgets/actions/button_game_start_new.dart @@ -14,17 +14,17 @@ class StartNewGameButton extends StatelessWidget { builder: (BuildContext context, GameSettingsState gameSettingsState) { return BlocBuilder<GlobalSettingsCubit, GlobalSettingsState>( builder: (BuildContext context, GlobalSettingsState globalSettingsState) { - final GameCubit gameCubit = BlocProvider.of<GameCubit>(context); - return TextButton( child: const Image( - image: AssetImage('assets/icons/button_start.png'), + image: AssetImage('assets/ui/button_start.png'), fit: BoxFit.fill, ), - onPressed: () => gameCubit.startNewGame( - gameSettings: gameSettingsState.settings, - globalSettings: globalSettingsState.settings, - ), + onPressed: () { + BlocProvider.of<GameCubit>(context).startNewGame( + gameSettings: gameSettingsState.settings, + globalSettings: globalSettingsState.settings, + ); + }, ); }, ); diff --git a/lib/ui/widgets/actions/button_resume_saved_game.dart b/lib/ui/widgets/actions/button_resume_saved_game.dart new file mode 100644 index 0000000..d93fcd4 --- /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:solitaire/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/tileset.dart b/lib/ui/widgets/game/game_board.dart similarity index 90% rename from lib/ui/widgets/game/tileset.dart rename to lib/ui/widgets/game/game_board.dart index 5462a79..a05d73b 100644 --- a/lib/ui/widgets/game/tileset.dart +++ b/lib/ui/widgets/game/game_board.dart @@ -2,11 +2,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:solitaire/cubit/game_cubit.dart'; -import 'package:solitaire/models/board.dart'; +import 'package:solitaire/models/game/board.dart'; import 'package:solitaire/ui/widgets/game/tile_widget.dart'; -class Tileset extends StatelessWidget { - const Tileset({super.key}); +class GameBoardWidget extends StatelessWidget { + const GameBoardWidget({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/ui/widgets/game/game_widget.dart b/lib/ui/widgets/game/game_widget.dart deleted file mode 100644 index e2c7caf..0000000 --- a/lib/ui/widgets/game/game_widget.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'package:solitaire/cubit/game_cubit.dart'; -import 'package:solitaire/models/game.dart'; -import 'package:solitaire/ui/widgets/game/indicator_top.dart'; -import 'package:solitaire/ui/widgets/game/message_game_end.dart'; -import 'package:solitaire/ui/widgets/game/tileset.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; - - final bool gameIsFinished = currentGame.isFinished; - - return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const SizedBox(height: 8), - const TopIndicator(), - const SizedBox(height: 2), - const Expanded( - child: Tileset(), - ), - const SizedBox(height: 2), - gameIsFinished ? const EndGameMessage() : const SizedBox(height: 2), - ], - ); - }, - ); - } -} diff --git a/lib/ui/widgets/game/message_game_end.dart b/lib/ui/widgets/game/message_game_end.dart deleted file mode 100644 index b1b4faa..0000000 --- a/lib/ui/widgets/game/message_game_end.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'package:solitaire/cubit/game_cubit.dart'; -import 'package:solitaire/models/game.dart'; - -class EndGameMessage extends StatelessWidget { - const EndGameMessage({super.key}); - - @override - Widget build(BuildContext context) { - return BlocBuilder<GameCubit, GameState>( - builder: (BuildContext context, GameState gameState) { - final Game currentGame = gameState.currentGame; - - String decorationImageAssetName = ''; - if (currentGame.gameWon) { - decorationImageAssetName = 'assets/icons/game_win.png'; - } else { - decorationImageAssetName = 'assets/icons/placeholder.png'; - } - - final Image decorationImage = Image( - image: AssetImage(decorationImageAssetName), - 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]), - TextButton( - child: const Image( - image: AssetImage('assets/icons/button_back.png'), - fit: BoxFit.fill, - ), - onPressed: () { - final GameCubit gameCubit = BlocProvider.of<GameCubit>(context); - gameCubit.quitGame(); - }, - ), - Column(children: [decorationImage]), - ], - ), - ], - ), - ); - }, - ); - } -} diff --git a/lib/ui/widgets/game/tile_widget.dart b/lib/ui/widgets/game/tile_widget.dart index 7d71df5..ee6eb80 100644 --- a/lib/ui/widgets/game/tile_widget.dart +++ b/lib/ui/widgets/game/tile_widget.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:solitaire/cubit/game_cubit.dart'; -import 'package:solitaire/models/cell.dart'; -import 'package:solitaire/models/game.dart'; +import 'package:solitaire/models/game/cell.dart'; +import 'package:solitaire/models/game/game.dart'; class TileWidget extends StatelessWidget { const TileWidget({ @@ -21,12 +21,10 @@ class TileWidget extends StatelessWidget { builder: (BuildContext context, GameState gameState) { final Game currentGame = gameState.currentGame; - final GameCubit gameCubit = BlocProvider.of<GameCubit>(context); - if (tile.hasHole) { return render( currentGame: currentGame, - gameCubit: gameCubit, + gameCubit: BlocProvider.of<GameCubit>(context), ); } else { return Image( diff --git a/lib/ui/widgets/global_app_bar.dart b/lib/ui/widgets/global_app_bar.dart index 8212103..3543942 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:solitaire/config/menu.dart'; import 'package:solitaire/cubit/game_cubit.dart'; import 'package:solitaire/cubit/nav_cubit.dart'; -import 'package:solitaire/models/game.dart'; -import 'package:solitaire/ui/widgets/helpers/app_title.dart'; +import 'package:solitaire/models/game/game.dart'; +import 'package:solitaire/ui/helpers/app_titles.dart'; class GlobalAppBar extends StatelessWidget implements PreferredSizeWidget { const GlobalAppBar({super.key}); @@ -20,16 +20,15 @@ class GlobalAppBar extends StatelessWidget implements PreferredSizeWidget { final List<Widget> menuActions = []; - if (currentGame.isRunning) { + if (currentGame.isRunning && !currentGame.isFinished) { menuActions.add(TextButton( child: const Image( - image: AssetImage('assets/icons/button_back.png'), + image: AssetImage('assets/ui/button_back.png'), fit: BoxFit.fill, ), onPressed: () {}, onLongPress: () { - final GameCubit gameCubit = BlocProvider.of<GameCubit>(context); - gameCubit.quitGame(); + BlocProvider.of<GameCubit>(context).quitGame(); }, )); } else { @@ -70,7 +69,7 @@ class GlobalAppBar extends StatelessWidget implements PreferredSizeWidget { } return AppBar( - title: const AppTitle(text: 'app_name'), + title: const AppHeader(text: 'app_name'), actions: menuActions, ); }, diff --git a/lib/ui/widgets/helpers/app_header.dart b/lib/ui/widgets/helpers/app_header.dart deleted file mode 100644 index b5c5be0..0000000 --- a/lib/ui/widgets/helpers/app_header.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -class AppHeader extends StatelessWidget { - const AppHeader({super.key, required this.text}); - - final String text; - - @override - Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - tr(text), - textAlign: TextAlign.start, - style: Theme.of(context).textTheme.headlineSmall!.apply(fontWeightDelta: 2), - ), - const SizedBox(height: 8), - ], - ); - } -} diff --git a/lib/ui/widgets/helpers/app_title.dart b/lib/ui/widgets/helpers/app_title.dart deleted file mode 100644 index 7cbbb20..0000000 --- a/lib/ui/widgets/helpers/app_title.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -class AppTitle extends StatelessWidget { - const AppTitle({super.key, required this.text}); - - final String text; - - @override - Widget build(BuildContext context) { - return Text( - tr(text), - textAlign: TextAlign.start, - style: Theme.of(context).textTheme.headlineLarge!.apply(fontWeightDelta: 2), - ); - } -} diff --git a/lib/utils/color_extensions.dart b/lib/utils/color_extensions.dart new file mode 100644 index 0000000..4e55e33 --- /dev/null +++ b/lib/utils/color_extensions.dart @@ -0,0 +1,33 @@ +import 'dart:ui'; + +extension ColorExtension on Color { + Color darken([int percent = 40]) { + assert(1 <= percent && percent <= 100); + final value = 1 - percent / 100; + return Color.fromARGB( + alpha, + (red * value).round(), + (green * value).round(), + (blue * value).round(), + ); + } + + Color lighten([int percent = 40]) { + assert(1 <= percent && percent <= 100); + final value = percent / 100; + return Color.fromARGB( + alpha, + (red + ((255 - red) * value)).round(), + (green + ((255 - green) * value)).round(), + (blue + ((255 - blue) * value)).round(), + ); + } + + Color avg(Color other) { + final red = (this.red + other.red) ~/ 2; + final green = (this.green + other.green) ~/ 2; + final blue = (this.blue + other.blue) ~/ 2; + final alpha = (this.alpha + other.alpha) ~/ 2; + return Color.fromARGB(alpha, red, green, blue); + } +} diff --git a/pubspec.lock b/pubspec.lock index bdba0ef..e0ab96e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -106,10 +106,10 @@ packages: dependency: "direct main" description: name: flutter_bloc - sha256: f0ecf6e6eb955193ca60af2d5ca39565a86b8a142452c5b24d96fb477428f4d2 + sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a url: "https://pub.dev" source: hosted - version: "8.1.5" + version: "8.1.6" flutter_lints: dependency: "direct dev" description: @@ -164,10 +164,10 @@ 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: @@ -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,10 +236,10 @@ 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: @@ -276,10 +276,10 @@ packages: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.5" plugin_platform_interface: dependency: transitive description: @@ -308,10 +308,10 @@ 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: @@ -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 5c63879..4068df4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: Solitaire Game publish_to: "none" -version: 0.0.18+18 +version: 0.1.0+19 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,14 +22,17 @@ dependencies: path_provider: ^2.0.11 unicons: ^2.1.1 + # specific + # (none) + dev_dependencies: flutter_lints: ^4.0.0 flutter: uses-material-design: true assets: - - assets/icons/ - assets/skins/ + - assets/ui/ - assets/translations/ fonts: @@ -42,3 +46,4 @@ flutter: weight: 400 - asset: assets/fonts/Nunito-Light.ttf weight: 300 + diff --git a/icons/build_application_icons.sh b/resources/app/build_application_resources.sh similarity index 98% rename from icons/build_application_icons.sh rename to resources/app/build_application_resources.sh index 27dbe26..6d67b8f 100755 --- a/icons/build_application_icons.sh +++ b/resources/app/build_application_resources.sh @@ -6,7 +6,7 @@ command -v scour >/dev/null 2>&1 || { echo >&2 "I require scour but it's not ins command -v optipng >/dev/null 2>&1 || { echo >&2 "I require optipng but it's not installed. Aborting."; exit 1; } CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" -BASE_DIR="$(dirname "${CURRENT_DIR}")" +BASE_DIR="$(dirname "$(dirname "${CURRENT_DIR}")")" SOURCE_ICON="${CURRENT_DIR}/icon.svg" SOURCE_FASTLANE="${CURRENT_DIR}/featureGraphic.svg" diff --git a/icons/featureGraphic.svg b/resources/app/featureGraphic.svg similarity index 100% rename from icons/featureGraphic.svg rename to resources/app/featureGraphic.svg diff --git a/icons/icon.svg b/resources/app/icon.svg similarity index 100% rename from icons/icon.svg rename to resources/app/icon.svg diff --git a/resources/build_resources.sh b/resources/build_resources.sh new file mode 100755 index 0000000..659697a --- /dev/null +++ b/resources/build_resources.sh @@ -0,0 +1,7 @@ +#! /bin/bash + +CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" + +${CURRENT_DIR}/app/build_application_resources.sh +${CURRENT_DIR}/ui/build_ui_resources.sh + diff --git a/icons/build_game_icons.sh b/resources/ui/build_ui_resources.sh similarity index 54% rename from icons/build_game_icons.sh rename to resources/ui/build_ui_resources.sh index 2191261..4f365ed 100755 --- a/icons/build_game_icons.sh +++ b/resources/ui/build_ui_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}")")" ASSETS_DIR="${BASE_DIR}/assets" OPTIPNG_OPTIONS="-preserve -quiet -o7" @@ -14,25 +14,23 @@ ICON_SIZE=192 ####################################################### -# Game images -AVAILABLE_GAME_IMAGES=" - button_back - button_start - game_win - placeholder -" - -# Skins -AVAILABLE_SKINS=" - default -" - -# Images per skin -SKIN_IMAGES=" - board - hole - peg -" +# 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 ####################################################### @@ -54,7 +52,7 @@ function optimize_svg() { } # build icons -function build_icon() { +function build_image() { SOURCE="$1" TARGET="$2" @@ -67,43 +65,46 @@ function build_icon() { optimize_svg "${SOURCE}" + mkdir -p "$(dirname "${TARGET}")" + inkscape \ --export-width=${ICON_SIZE} \ --export-height=${ICON_SIZE} \ --export-filename=${TARGET} \ - ${SOURCE} + "${SOURCE}" - optipng ${OPTIPNG_OPTIONS} ${TARGET} + optipng ${OPTIPNG_OPTIONS} "${TARGET}" } -function build_icon_for_skin() { +function build_image_for_skin() { SKIN_CODE="$1" # skin images for SKIN_IMAGE in ${SKIN_IMAGES} do - build_icon ${CURRENT_DIR}/skins/${SKIN_CODE}/${SKIN_IMAGE}.svg ${ASSETS_DIR}/skins/${SKIN_CODE}_${SKIN_IMAGE}.png + build_image ${CURRENT_DIR}/skins/${SKIN_CODE}/${SKIN_IMAGE}.svg ${ASSETS_DIR}/skins/${SKIN_CODE}_${SKIN_IMAGE}.png done } ####################################################### -# Create output folders -mkdir -p ${ASSETS_DIR}/icons -mkdir -p ${ASSETS_DIR}/skins - # Delete existing generated images -find ${ASSETS_DIR}/icons -type f -name "*.png" -delete -find ${ASSETS_DIR}/skins -type f -name "*.png" -delete +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_icon ${CURRENT_DIR}/${GAME_IMAGE}.svg ${ASSETS_DIR}/icons/${GAME_IMAGE}.png + 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_icon_for_skin "${SKIN}" + build_image_for_skin "${SKIN}" done + diff --git a/icons/button_back.svg b/resources/ui/images/button_back.svg similarity index 100% rename from icons/button_back.svg rename to resources/ui/images/button_back.svg diff --git a/resources/ui/images/button_delete_saved_game.svg b/resources/ui/images/button_delete_saved_game.svg new file mode 100644 index 0000000..ac7eefe --- /dev/null +++ b/resources/ui/images/button_delete_saved_game.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect x=".44662" y=".89101" width="92.772" height="91.894" ry="11.689" fill="#ee7d49" stroke="#fff" stroke-width=".238"/><path d="m61.07 35.601-1.7399 27.837c-0.13442 2.1535-1.9205 3.8312-4.0781 3.8312h-16.84c-2.1576 0-3.9437-1.6777-4.0781-3.8312l-1.7399-27.837h-2.6176c-0.84621 0-1.5323-0.68613-1.5323-1.5323 0-0.84655 0.68613-1.5323 1.5323-1.5323h33.711c0.84621 0 1.5323 0.68578 1.5323 1.5323 0 0.84621-0.68613 1.5323-1.5323 1.5323zm-3.2617 0h-21.953l1.4715 26.674c0.05985 1.0829 0.95531 1.9305 2.0403 1.9305h14.929c1.085 0 1.9804-0.84757 2.0403-1.9305zm-10.977 3.0647c0.78977 0 1.4301 0.6403 1.4301 1.4301v19.614c0 0.78977-0.6403 1.4301-1.4301 1.4301s-1.4301-0.6403-1.4301-1.4301v-19.614c0-0.78977 0.6403-1.4301 1.4301-1.4301zm-6.1293 0c0.80004 0 1.4588 0.62935 1.495 1.4286l0.89647 19.719c0.03182 0.70016-0.50998 1.2933-1.2101 1.3255-0.01915 7.02e-4 -0.03831 1e-3 -0.05781 1e-3 -0.74462 0-1.3596-0.58215-1.4003-1.3261l-1.0757-19.719c-0.0407-0.74701 0.53188-1.3852 1.2786-1.4259 0.02462-0.0014 0.04926-2e-3 0.07388-2e-3zm12.259 0c0.74804 0 1.3541 0.60609 1.3541 1.3541 0 0.02462-3.28e-4 0.04926-0.0017 0.07388l-1.0703 19.618c-0.04379 0.80106-0.70597 1.4281-1.5081 1.4281-0.74804 0-1.3541-0.60609-1.3541-1.3541 0-0.02462 3.49e-4 -0.04925 0.0017-0.07388l1.0703-19.618c0.04379-0.80106 0.70597-1.4281 1.5081-1.4281zm-10.216-12.259h8.1728c2.2567 0 4.086 1.8293 4.086 4.086v2.0433h-16.344v-2.0433c0-2.2567 1.8293-4.086 4.086-4.086zm0.20453 3.0647c-0.67725 0-1.2259 0.54863-1.2259 1.2259v1.8388h10.215v-1.8388c0-0.67725-0.54863-1.2259-1.2259-1.2259z" fill="#fff" fill-rule="evenodd" stroke="#bd4812" stroke-width=".75383"/></svg> diff --git a/resources/ui/images/button_resume_game.svg b/resources/ui/images/button_resume_game.svg new file mode 100644 index 0000000..6ad8b64 --- /dev/null +++ b/resources/ui/images/button_resume_game.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect x=".44662" y=".89101" width="92.772" height="91.894" ry="11.689" fill="#49a1ee" stroke="#fff" stroke-width=".238"/><path d="m39.211 31.236c-0.84086-0.84489-2.9911-0.84489-2.9911 0v34.329c0 0.84594 2.1554 0.84594 2.9993 0l28.178-15.637c0.84392-0.84086 0.85812-2.2091 0.01623-3.053z" fill="#fefeff" stroke="#105ca1" stroke-linecap="round" stroke-linejoin="round" stroke-width="6.1726"/><path d="m40.355 33.714c-0.71948-0.72294-2.5594-0.72294-2.5594 0v29.373c0 0.72383 1.8442 0.72383 2.5663 0l24.11-13.38c0.7221-0.71948 0.73426-1.8902 0.01389-2.6124z" fill="#fefeff" stroke="#feffff" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.225"/><path d="m28.369 66.919v-37.591" fill="#105ca2" stroke="#105ca2" stroke-linecap="round" stroke-width="4.0337"/></svg> diff --git a/icons/button_start.svg b/resources/ui/images/button_start.svg similarity index 100% rename from icons/button_start.svg rename to resources/ui/images/button_start.svg diff --git a/resources/ui/images/game_fail.svg b/resources/ui/images/game_fail.svg new file mode 100644 index 0000000..2922fd7 --- /dev/null +++ b/resources/ui/images/game_fail.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect x=".44662" y=".89101" width="92.772" height="91.894" ry="11.689" fill="#d11717" stroke="#fff" stroke-width=".238"/><path d="m71.624 59.304c3.5089 3.5089 3.5089 9.0561 0 12.565-1.6976 1.6976-3.9623 2.6034-6.2261 2.6034s-4.5275-0.90569-6.2261-2.6034l-12.452-12.452-12.452 12.452c-1.6976 1.6976-3.9623 2.6034-6.2261 2.6034s-4.5275-0.90569-6.2261-2.6034c-3.5089-3.5089-3.5089-9.0561 0-12.565l12.452-12.452-12.452-12.452c-3.5089-3.5089-3.5089-9.0561 0-12.565s9.0561-3.5089 12.565 0l12.452 12.452 12.452-12.452c3.5089-3.5089 9.0561-3.5089 12.565 0s3.5089 9.0561 0 12.565l-12.452 12.452z" fill="#e7e7e7" stroke-width=".20213"/></svg> diff --git a/icons/game_win.svg b/resources/ui/images/game_win.svg similarity index 100% rename from icons/game_win.svg rename to resources/ui/images/game_win.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 diff --git a/icons/skins/default/board.svg b/resources/ui/skins/default/board.svg similarity index 100% rename from icons/skins/default/board.svg rename to resources/ui/skins/default/board.svg diff --git a/icons/skins/default/hole.svg b/resources/ui/skins/default/hole.svg similarity index 100% rename from icons/skins/default/hole.svg rename to resources/ui/skins/default/hole.svg diff --git a/icons/skins/default/peg.svg b/resources/ui/skins/default/peg.svg similarity index 100% rename from icons/skins/default/peg.svg rename to resources/ui/skins/default/peg.svg -- GitLab