diff --git a/android/app/build.gradle b/android/app/build.gradle index 27a93e6dc9643f4306c4f44656ebb131510b20cb..2328355d7aa1d0dd7f1436cd5c0ed32b0159d6d1 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -37,7 +37,7 @@ if (keystorePropertiesFile.exists()) { } android { - compileSdkVersion 33 + compileSdkVersion 34 namespace "org.benoitharrault.twister" defaultConfig { diff --git a/android/gradle.properties b/android/gradle.properties index 3487476dc637023e34426ce50caf3343f91e3038..1913fd1742a8e1553517f28698109845b429435c 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.23 -app.versionCode=23 +app.versionName=0.1.0 +app.versionCode=24 diff --git a/assets/images/blank.png b/assets/images/blank.png deleted file mode 100644 index 71f46e2afe84d6655d32e85d164c463cdfc5dabc..0000000000000000000000000000000000000000 Binary files a/assets/images/blank.png and /dev/null differ diff --git a/assets/images/left-foot.png b/assets/images/left-foot.png deleted file mode 100644 index fd46c1a8ac871945c801a6a353ebb0b70639df7f..0000000000000000000000000000000000000000 Binary files a/assets/images/left-foot.png and /dev/null differ diff --git a/assets/images/left-hand.png b/assets/images/left-hand.png deleted file mode 100644 index 34d676bb3985fe6479e20d003c1be4370d20857d..0000000000000000000000000000000000000000 Binary files a/assets/images/left-hand.png and /dev/null differ diff --git a/assets/images/right-foot.png b/assets/images/right-foot.png deleted file mode 100644 index d0eff85495b7b8910389f948a712b02cde1909ef..0000000000000000000000000000000000000000 Binary files a/assets/images/right-foot.png and /dev/null differ diff --git a/assets/images/right-hand.png b/assets/images/right-hand.png deleted file mode 100644 index e730e5f4c58c8bdf3444ae7ff7ad45fe3e32e3e3..0000000000000000000000000000000000000000 Binary files a/assets/images/right-hand.png and /dev/null differ diff --git a/assets/translations/en.json b/assets/translations/en.json index 867a99439eb21af93fbddae51621e131d25c8aa5..7ff9b8cd6e5c2c5f802b8e884b6bfe6d27d5206f 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -6,13 +6,14 @@ "left_foot": "left foot", "right_foot": "right foot", - "bottom_nav_game": "Game", - "bottom_nav_settings": "Settings", - "settings_title": "Settings", "settings_label_theme": "Theme mode", - "settings_title_game": "Game settings", - "settings_label_game_timer_value": "Timer value: ", - "lang_prefix": "en" + "lang_prefix": "en", + + "about_title": "Informations", + "about_content": "Twister", + "about_version": "Version: {version}", + + "": "" } diff --git a/assets/translations/fr.json b/assets/translations/fr.json index c481788e703609e5007305c9051e498b7b15fef1..e2aafac3159564d30808cc8fd8e54a3040fd0504 100644 --- a/assets/translations/fr.json +++ b/assets/translations/fr.json @@ -6,13 +6,14 @@ "left_foot": "pied gauche", "right_foot": "pied droit", - "bottom_nav_game": "Jeu", - "bottom_nav_settings": "Réglages", - "settings_title": "Réglages", "settings_label_theme": "Thème de couleurs", - "settings_title_game": "Paramètres du jeu", - "settings_label_game_timer_value": "Durée du chrono : ", - "lang_prefix": "fr" + "lang_prefix": "fr", + + "about_title": "Informations", + "about_content": "Twister.", + "about_version": "Version : {version}", + + "": "" } diff --git a/assets/ui/button_back.png b/assets/ui/button_back.png new file mode 100644 index 0000000000000000000000000000000000000000..cc48ffb1dbb653d9a996f139dfbe02969724bfa5 Binary files /dev/null and b/assets/ui/button_back.png differ diff --git a/assets/ui/button_delete_saved_game.png b/assets/ui/button_delete_saved_game.png new file mode 100644 index 0000000000000000000000000000000000000000..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/ui/button_start.png b/assets/ui/button_start.png new file mode 100644 index 0000000000000000000000000000000000000000..6845e2f5c21598ab61f1684d2075aeec0334bf23 Binary files /dev/null and b/assets/ui/button_start.png differ diff --git a/assets/ui/game_end.png b/assets/ui/game_end.png new file mode 100644 index 0000000000000000000000000000000000000000..876334279c1711b349a62131a33607eecf924eb6 Binary files /dev/null and b/assets/ui/game_end.png differ diff --git a/assets/ui/move-blank.png b/assets/ui/move-blank.png new file mode 100644 index 0000000000000000000000000000000000000000..3a504ec4e9ab530b04d5324bcfb4cf187a3c1b86 Binary files /dev/null and b/assets/ui/move-blank.png differ diff --git a/assets/ui/move-left-foot.png b/assets/ui/move-left-foot.png new file mode 100644 index 0000000000000000000000000000000000000000..1d4a90c1b42d4da8a617c0b10cad5c5dc9329b32 Binary files /dev/null and b/assets/ui/move-left-foot.png differ diff --git a/assets/ui/move-left-hand.png b/assets/ui/move-left-hand.png new file mode 100644 index 0000000000000000000000000000000000000000..d8e288cc61afef47b8f6342fdf109098d321dde8 Binary files /dev/null and b/assets/ui/move-left-hand.png differ diff --git a/assets/ui/move-right-foot.png b/assets/ui/move-right-foot.png new file mode 100644 index 0000000000000000000000000000000000000000..f9fa2905b064ca539d6785851d89ae3e55428d4d Binary files /dev/null and b/assets/ui/move-right-foot.png differ diff --git a/assets/ui/move-right-hand.png b/assets/ui/move-right-hand.png new file mode 100644 index 0000000000000000000000000000000000000000..297d74dff520bba8e4ab33b893a1fe4f49fcf197 Binary files /dev/null and b/assets/ui/move-right-hand.png differ diff --git a/assets/ui/placeholder.png b/assets/ui/placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..814df31be6ddc4275ebe4490c79365578dbef1f0 Binary files /dev/null and b/assets/ui/placeholder.png differ diff --git a/fastlane/metadata/android/en-US/changelogs/24.txt b/fastlane/metadata/android/en-US/changelogs/24.txt new file mode 100644 index 0000000000000000000000000000000000000000..d4afd512e55b3fd8ffbfd795adb9b00832e5aaef --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/24.txt @@ -0,0 +1 @@ +Improve/normalize game architecture. diff --git a/fastlane/metadata/android/fr-FR/changelogs/24.txt b/fastlane/metadata/android/fr-FR/changelogs/24.txt new file mode 100644 index 0000000000000000000000000000000000000000..6a9871a5eb8eb3c6e9106520f1cbf1f39f9e5ef7 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/24.txt @@ -0,0 +1 @@ +Amélioration/normalisation de l'architecture du jeu. diff --git a/images/build_game_images.sh b/images/build_game_images.sh deleted file mode 100755 index 4d7f1be1f63651c82db63cbd8217cde60fa2bd3f..0000000000000000000000000000000000000000 --- a/images/build_game_images.sh +++ /dev/null @@ -1,80 +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=512 - -####################################################### - -# Game images -AVAILABLE_GAME_IMAGES=" - blank - left-hand - right-hand - left-foot - right-foot -" -####################################################### - -# 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 folder -mkdir -p ${ASSETS_DIR}/images - -# Delete existing generated images -find ${ASSETS_DIR}/images -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}/images/${GAME_IMAGE}.png -done - diff --git a/lib/config/color_theme.dart b/lib/config/color_theme.dart new file mode 100644 index 0000000000000000000000000000000000000000..6b0ae12adc50d6f20ed07bc6e17a6fa4288ae284 --- /dev/null +++ b/lib/config/color_theme.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class TwisterColors { + static const Color grey = Color.fromARGB(255, 0xF0, 0xE2, 0xE7); // F0E2E7 + + static const Color blue = Color.fromARGB(255, 0x3F, 0x84, 0xE5); // 3F84E5 + static const Color green = Color.fromARGB(255, 0x1D, 0xA2, 0x3C); // 1DA23C + static const Color red = Color.fromARGB(255, 0xE0, 0x0D, 0x3B); // E00D3B + static const Color yellow = Color.fromARGB(255, 0xE8, 0xD9, 0x17); // E8D917 +} diff --git a/lib/config/colors.dart b/lib/config/colors.dart deleted file mode 100644 index 988ca612285d28a0b3c4d68538708f8afc8f21ad..0000000000000000000000000000000000000000 --- a/lib/config/colors.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:flutter/material.dart'; - -class TwisterColors { - static const Color grey = Color.fromARGB(255, 0xF0, 0xE2, 0xE7); // F0E2E7 - - static const Color blue = Color.fromARGB(255, 0x3F, 0x84, 0xE5); // 3F84E5 - static const Color green = Color.fromARGB(255, 0x3F, 0x78, 0x4C); // 3F784C - static const Color red = Color.fromARGB(255, 0xB2, 0x0D, 0x30); // B20D30 - static const Color yellow = Color.fromARGB(255, 0xC1, 0x78, 0x17); // C17817 -} diff --git a/lib/config/default_game_settings.dart b/lib/config/default_game_settings.dart new file mode 100644 index 0000000000000000000000000000000000000000..d2d7d496e5f46f548b8efe0e89ce861589604c49 --- /dev/null +++ b/lib/config/default_game_settings.dart @@ -0,0 +1,33 @@ +import 'package:twister/utils/tools.dart'; + +class DefaultGameSettings { + // available global parameters codes + static const String parameterCodeTimerValue = 'timer'; + static const List<String> availableParameters = [ + parameterCodeTimerValue, + ]; + + // timer value: available values + static const String timerValueMedium = 'medium'; + static const List<String> allowedTimerValues = [ + timerValueMedium, + ]; + // timer value: default value + static const String defaultTimerValue = timerValueMedium; + + // available values from parameter code + static List<String> getAvailableValues(String parameterCode) { + switch (parameterCode) { + case parameterCodeTimerValue: + return DefaultGameSettings.allowedTimerValues; + } + + 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 new file mode 100644 index 0000000000000000000000000000000000000000..f090d539c1c414a1043a3f2b3adb4a7d390c9506 --- /dev/null +++ b/lib/config/default_global_settings.dart @@ -0,0 +1,33 @@ +import 'package:twister/utils/tools.dart'; + +class DefaultGlobalSettings { + // available global parameters codes + static const String parameterCodeSkin = 'skin'; + static const List<String> availableParameters = [ + parameterCodeSkin, + ]; + + // skin: available values + static const String skinValueColors = 'colors'; + static const List<String> allowedSkinValues = [ + skinValueColors, + ]; + // skin: default value + static const String defaultSkinValue = skinValueColors; + + // available values from parameter code + static List<String> getAvailableValues(String parameterCode) { + switch (parameterCode) { + case parameterCodeSkin: + return DefaultGlobalSettings.allowedSkinValues; + } + + printlog('Did not find any available value for global parameter "$parameterCode".'); + return []; + } + + // parameters displayed with assets (instead of painter) + static List<String> displayedWithAssets = [ + // + ]; +} diff --git a/lib/config/default_settings.dart b/lib/config/default_settings.dart deleted file mode 100644 index c3271480d414643f6c75e0b3d2cb283596c57b6f..0000000000000000000000000000000000000000 --- a/lib/config/default_settings.dart +++ /dev/null @@ -1,9 +0,0 @@ -class DefaultSettings { - static const int defaultTimerValue = 20; - - static const List<int> allowedTimerValues = [ - 10, - defaultTimerValue, - 30, - ]; -} diff --git a/lib/config/menu.dart b/lib/config/menu.dart new file mode 100644 index 0000000000000000000000000000000000000000..1af3bf32520e803d16618438ad5d3965936b723a --- /dev/null +++ b/lib/config/menu.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:unicons/unicons.dart'; + +import 'package:twister/ui/screens/page_about.dart'; +import 'package:twister/ui/screens/page_game.dart'; +import 'package:twister/ui/screens/page_settings.dart'; + +class MenuItem { + final Icon icon; + final Widget page; + + const MenuItem({ + required this.icon, + required this.page, + }); +} + +class Menu { + static const indexGame = 0; + static const menuItemGame = MenuItem( + icon: Icon(UniconsLine.home), + page: PageGame(), + ); + + static const indexSettings = 1; + static const menuItemSettings = MenuItem( + icon: Icon(UniconsLine.setting), + page: PageSettings(), + ); + + static const indexAbout = 2; + static const menuItemAbout = MenuItem( + icon: Icon(UniconsLine.info_circle), + page: PageAbout(), + ); + + static Map<int, MenuItem> items = { + indexGame: menuItemGame, + indexSettings: menuItemSettings, + indexAbout: menuItemAbout, + }; + + static bool isIndexAllowed(int pageIndex) { + return items.keys.contains(pageIndex); + } + + static Widget getPageWidget(int pageIndex) { + return items[pageIndex]?.page ?? menuItemGame.page; + } + + static int itemsCount = Menu.items.length; +} diff --git a/lib/config/theme.dart b/lib/config/theme.dart 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/bottom_nav_cubit.dart b/lib/cubit/bottom_nav_cubit.dart deleted file mode 100644 index c633c6515b0fcb4e19a521e45b579f178d605970..0000000000000000000000000000000000000000 --- a/lib/cubit/bottom_nav_cubit.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:hydrated_bloc/hydrated_bloc.dart'; - -class BottomNavCubit extends HydratedCubit<int> { - BottomNavCubit() : super(0); - - int pagesCount = 2; - - void updateIndex(int index) { - if (isIndexAllowed(index)) { - emit(index); - } else { - goToHomePage(); - } - } - - bool isIndexAllowed(int index) { - return (index >= 0) && (index < pagesCount); - } - - void goToHomePage() => emit(0); - - @override - int? fromJson(Map<String, dynamic> json) { - return 0; - } - - @override - Map<String, dynamic>? toJson(int state) { - return <String, int>{'pageIndex': state}; - } -} diff --git a/lib/cubit/game_cubit.dart b/lib/cubit/game_cubit.dart index 325c872e69f892da4db10b86182d8b343c10bead..f0eed2c2ac1f4b92c277e702a7dc4eda2f86b9b1 100644 --- a/lib/cubit/game_cubit.dart +++ b/lib/cubit/game_cubit.dart @@ -1,53 +1,118 @@ +import 'package:audioplayers/audioplayers.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; -import 'package:twister/models/move.dart'; +import 'package:twister/models/game/game.dart'; +import 'package:twister/models/game/move.dart'; +import 'package:twister/models/settings/settings_game.dart'; +import 'package:twister/models/settings/settings_global.dart'; part 'game_state.dart'; class GameCubit extends HydratedCubit<GameState> { - GameCubit() : super(const GameState()); + GameCubit() + : super(GameState( + currentGame: Game.createEmpty(), + )); - Move getMove() { - return state.move ?? Move.createNull(); + void updateState(Game game) { + emit(GameState( + currentGame: game, + )); } - void setValues({ - Move? move, + void refresh() { + final Game game = Game( + // Settings + gameSettings: state.currentGame.gameSettings, + globalSettings: state.currentGame.globalSettings, + // State + isRunning: state.currentGame.isRunning, + isStarted: state.currentGame.isStarted, + isFinished: state.currentGame.isFinished, + animationInProgress: state.currentGame.animationInProgress, + // Base data + move: state.currentGame.move, + // Game data + history: state.currentGame.history, + ); + // game.dump(); + + updateState(game); + } + + void startNewGame({ + required GameSettings gameSettings, + required GlobalSettings globalSettings, }) { - List<Move> history = state.history ?? []; - if (move != null) { - history.add(move); - } + final Game newGame = Game.createNew( + // Settings + gameSettings: gameSettings, + globalSettings: globalSettings, + ); - emit(GameState( - move: move ?? state.move, - history: history, - )); + newGame.dump(); + + updateState(newGame); + refresh(); + } + + void quitGame() { + state.currentGame.isRunning = false; + refresh(); + } + + void resumeSavedGame() { + state.currentGame.isRunning = true; + refresh(); + } + + void deleteSavedGame() { + state.currentGame.isRunning = false; + state.currentGame.isFinished = true; + refresh(); + } + + void setMove(Move move) { + state.currentGame.isStarted = true; + state.currentGame.move = move; + state.currentGame.history.add(move); + refresh(); } void deleteHistory() { - List<Move> history = []; - emit(GameState( - move: state.move, - history: history, - )); + state.currentGame.history = []; + state.currentGame.isStarted = false; + refresh(); + } + + void setAnimationIsRunning(bool isRunning) { + state.currentGame.animationInProgress = isRunning; + refresh(); + } + + void pickNewMove() { + Move newMove = Move.pickRandom(); + setMove(newMove); + + final player = AudioPlayer(); + player.play(AssetSource(newMove.toSoundAsset())); } @override GameState? fromJson(Map<String, dynamic> json) { - Move move = json['move'] as Move; + final Game currentGame = json['currentGame'] as Game; return GameState( - move: move, + currentGame: currentGame, ); } @override Map<String, dynamic>? toJson(GameState state) { return <String, dynamic>{ - 'move': state.move?.toJson(), + 'currentGame': state.currentGame.toJson(), }; } } diff --git a/lib/cubit/game_state.dart b/lib/cubit/game_state.dart index 204801effc156a3a761d130cfe37bcd50f81aa47..00e211668c3269255926939324355792abd61c41 100644 --- a/lib/cubit/game_state.dart +++ b/lib/cubit/game_state.dart @@ -3,21 +3,13 @@ part of 'game_cubit.dart'; @immutable class GameState extends Equatable { const GameState({ - this.move, - this.history, + required this.currentGame, }); - final Move? move; - final List<Move>? history; + final Game currentGame; @override List<dynamic> get props => <dynamic>[ - move, - history, + currentGame, ]; - - Map<String, dynamic> get values => <String, dynamic>{ - 'move': move, - 'history': history, - }; } diff --git a/lib/cubit/nav_cubit.dart b/lib/cubit/nav_cubit.dart new file mode 100644 index 0000000000000000000000000000000000000000..c998a4438fd478e9211b086f8a3390d220a1d94a --- /dev/null +++ b/lib/cubit/nav_cubit.dart @@ -0,0 +1,37 @@ +import 'package:hydrated_bloc/hydrated_bloc.dart'; + +import 'package:twister/config/menu.dart'; + +class NavCubit extends HydratedCubit<int> { + NavCubit() : super(0); + + void updateIndex(int index) { + if (Menu.isIndexAllowed(index)) { + emit(index); + } else { + goToGamePage(); + } + } + + void goToGamePage() { + emit(Menu.indexGame); + } + + void goToSettingsPage() { + emit(Menu.indexSettings); + } + + void goToAboutPage() { + emit(Menu.indexAbout); + } + + @override + int fromJson(Map<String, dynamic> json) { + return Menu.indexGame; + } + + @override + Map<String, dynamic>? toJson(int state) { + return <String, int>{'pageIndex': state}; + } +} diff --git a/lib/cubit/settings_cubit.dart b/lib/cubit/settings_cubit.dart deleted file mode 100644 index 200abf9d0734a0ea58fdda0c87207677e54c64c9..0000000000000000000000000000000000000000 --- a/lib/cubit/settings_cubit.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:flutter/material.dart'; -import 'package:hydrated_bloc/hydrated_bloc.dart'; - -import 'package:twister/config/default_settings.dart'; - -part 'settings_state.dart'; - -class SettingsCubit extends HydratedCubit<SettingsState> { - SettingsCubit() : super(const SettingsState()); - - int getTimerValue() { - return state.timerValue ?? DefaultSettings.defaultTimerValue; - } - - void setValues({ - int? timerValue, - }) { - emit(SettingsState( - timerValue: timerValue ?? state.timerValue, - )); - } - - @override - SettingsState? fromJson(Map<String, dynamic> json) { - int timerValue = json['timerValue'] as int; - - return SettingsState( - timerValue: timerValue, - ); - } - - @override - Map<String, dynamic>? toJson(SettingsState state) { - return <String, dynamic>{ - 'timerValue': state.timerValue ?? DefaultSettings.defaultTimerValue, - }; - } -} diff --git a/lib/cubit/settings_game_cubit.dart b/lib/cubit/settings_game_cubit.dart new file mode 100644 index 0000000000000000000000000000000000000000..e9a16c0f8c158bd34b380c11944e3904749a5823 --- /dev/null +++ b/lib/cubit/settings_game_cubit.dart @@ -0,0 +1,61 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; + +import 'package:twister/config/default_game_settings.dart'; +import 'package:twister/models/settings/settings_game.dart'; + +part 'settings_game_state.dart'; + +class GameSettingsCubit extends HydratedCubit<GameSettingsState> { + GameSettingsCubit() : super(GameSettingsState(settings: GameSettings.createDefault())); + + void setValues({ + String? timerValue, + }) { + emit( + GameSettingsState( + settings: GameSettings( + timerValue: timerValue ?? state.settings.timerValue, + ), + ), + ); + } + + String getParameterValue(String code) { + switch (code) { + case DefaultGameSettings.parameterCodeTimerValue: + return GameSettings.getTimerValueFromUnsafe(state.settings.timerValue); + } + + return ''; + } + + void setParameterValue(String code, String value) { + final String timerValue = code == DefaultGameSettings.parameterCodeTimerValue + ? value + : getParameterValue(DefaultGameSettings.parameterCodeTimerValue); + + setValues( + timerValue: timerValue, + ); + } + + @override + GameSettingsState? fromJson(Map<String, dynamic> json) { + final String timerValue = json[DefaultGameSettings.parameterCodeTimerValue] as String; + + return GameSettingsState( + settings: GameSettings( + timerValue: timerValue, + ), + ); + } + + @override + Map<String, dynamic>? toJson(GameSettingsState state) { + return <String, dynamic>{ + DefaultGameSettings.parameterCodeTimerValue: state.settings.timerValue, + }; + } +} diff --git a/lib/cubit/settings_game_state.dart b/lib/cubit/settings_game_state.dart new file mode 100644 index 0000000000000000000000000000000000000000..5acd85b44ba541e1c5e9c26af1c4be26a385b9ed --- /dev/null +++ b/lib/cubit/settings_game_state.dart @@ -0,0 +1,15 @@ +part of 'settings_game_cubit.dart'; + +@immutable +class GameSettingsState extends Equatable { + const GameSettingsState({ + required this.settings, + }); + + final GameSettings settings; + + @override + List<dynamic> get props => <dynamic>[ + settings, + ]; +} diff --git a/lib/cubit/settings_global_cubit.dart b/lib/cubit/settings_global_cubit.dart new file mode 100644 index 0000000000000000000000000000000000000000..1742cfb4e3e0e1a75ca2334115c1b16521ee6b09 --- /dev/null +++ b/lib/cubit/settings_global_cubit.dart @@ -0,0 +1,60 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; + +import 'package:twister/config/default_global_settings.dart'; +import 'package:twister/models/settings/settings_global.dart'; + +part 'settings_global_state.dart'; + +class GlobalSettingsCubit extends HydratedCubit<GlobalSettingsState> { + GlobalSettingsCubit() : super(GlobalSettingsState(settings: GlobalSettings.createDefault())); + + void setValues({ + String? skin, + }) { + emit( + GlobalSettingsState( + settings: GlobalSettings( + skin: skin ?? state.settings.skin, + ), + ), + ); + } + + String getParameterValue(String code) { + switch (code) { + case DefaultGlobalSettings.parameterCodeSkin: + return GlobalSettings.getSkinValueFromUnsafe(state.settings.skin); + } + return ''; + } + + void setParameterValue(String code, String value) { + final String skin = (code == DefaultGlobalSettings.parameterCodeSkin) + ? value + : getParameterValue(DefaultGlobalSettings.parameterCodeSkin); + + setValues( + skin: skin, + ); + } + + @override + GlobalSettingsState? fromJson(Map<String, dynamic> json) { + final String skin = json[DefaultGlobalSettings.parameterCodeSkin] as String; + + return GlobalSettingsState( + settings: GlobalSettings( + skin: skin, + ), + ); + } + + @override + Map<String, dynamic>? toJson(GlobalSettingsState state) { + return <String, dynamic>{ + DefaultGlobalSettings.parameterCodeSkin: state.settings.skin, + }; + } +} diff --git a/lib/cubit/settings_global_state.dart b/lib/cubit/settings_global_state.dart new file mode 100644 index 0000000000000000000000000000000000000000..ebcddd700f252257223ca8e16c85202b04f3ff24 --- /dev/null +++ b/lib/cubit/settings_global_state.dart @@ -0,0 +1,15 @@ +part of 'settings_global_cubit.dart'; + +@immutable +class GlobalSettingsState extends Equatable { + const GlobalSettingsState({ + required this.settings, + }); + + final GlobalSettings settings; + + @override + List<dynamic> get props => <dynamic>[ + settings, + ]; +} diff --git a/lib/cubit/settings_state.dart b/lib/cubit/settings_state.dart deleted file mode 100644 index 6048256f247aeb898ba1b3b10ca2daae3468a7c9..0000000000000000000000000000000000000000 --- a/lib/cubit/settings_state.dart +++ /dev/null @@ -1,19 +0,0 @@ -part of 'settings_cubit.dart'; - -@immutable -class SettingsState extends Equatable { - const SettingsState({ - this.timerValue, - }); - - final int? timerValue; - - @override - List<dynamic> get props => <dynamic>[ - timerValue, - ]; - - Map<String, dynamic> get values => <String, dynamic>{ - 'timerValue': timerValue, - }; -} diff --git a/lib/main.dart b/lib/main.dart index 78da985f11b9c86e0ff45fae91b2aaec7eed261d..d77b5291375acef85340d62ed350c5f0cbbed943 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,20 +2,22 @@ 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:twister/config/theme.dart'; -import 'package:twister/cubit/bottom_nav_cubit.dart'; import 'package:twister/cubit/game_cubit.dart'; -import 'package:twister/cubit/settings_cubit.dart'; +import 'package:twister/cubit/nav_cubit.dart'; +import 'package:twister/cubit/settings_game_cubit.dart'; +import 'package:twister/cubit/settings_global_cubit.dart'; import 'package:twister/cubit/theme_cubit.dart'; import 'package:twister/ui/skeleton.dart'; void main() async { - /// Initialize packages + // Initialize packages WidgetsFlutterBinding.ensureInitialized(); await EasyLocalization.ensureInitialized(); final Directory tmpDir = await getTemporaryDirectory(); @@ -24,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 { @@ -43,31 +44,62 @@ 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<BottomNavCubit>(create: (context) => BottomNavCubit()), - BlocProvider<GameCubit>(create: (context) => GameCubit()), - BlocProvider<SettingsCubit>(create: (context) => SettingsCubit()), + BlocProvider<NavCubit>(create: (context) => NavCubit()), BlocProvider<ThemeCubit>(create: (context) => ThemeCubit()), + BlocProvider<GameCubit>(create: (context) => GameCubit()), + BlocProvider<GlobalSettingsCubit>(create: (context) => GlobalSettingsCubit()), + BlocProvider<GameSettingsCubit>(create: (context) => GameSettingsCubit()), ], child: BlocBuilder<ThemeCubit, ThemeModeState>( - builder: (BuildContext context, ThemeModeState state) { - return MaterialApp( - title: 'Twister', - home: const SkeletonScreen(), + builder: (BuildContext context, ThemeModeState state) { + return MaterialApp( + title: 'Twister', + home: const SkeletonScreen(), - // Theme stuff - theme: lightTheme, - darkTheme: darkTheme, - themeMode: state.themeMode, + // Theme stuff + theme: lightTheme, + darkTheme: darkTheme, + themeMode: state.themeMode, - // Localization stuff - localizationsDelegates: context.localizationDelegates, - supportedLocales: context.supportedLocales, - locale: context.locale, - debugShowCheckedModeBanner: false, - ); - }), + // Localization stuff + localizationsDelegates: context.localizationDelegates, + supportedLocales: context.supportedLocales, + locale: context.locale, + debugShowCheckedModeBanner: false, + ); + }, + ), ); } + + List<String> getImagesAssets() { + final List<String> assets = []; + + const List<String> gameImages = [ + 'button_back', + 'button_delete_saved_game', + 'button_resume_game', + 'button_start', + 'game_end', + 'move-blank', + 'move-left-foot', + 'move-left-hand', + 'move-right-foot', + 'move-right-hand', + 'placeholder', + ]; + + for (String image in gameImages) { + assets.add('assets/ui/$image.png'); + } + + return assets; + } } diff --git a/lib/models/game/game.dart b/lib/models/game/game.dart new file mode 100644 index 0000000000000000000000000000000000000000..3ba1cd24665862989d7d57fe0d10d87b2bfe9f97 --- /dev/null +++ b/lib/models/game/game.dart @@ -0,0 +1,116 @@ +import 'package:twister/models/game/move.dart'; +import 'package:twister/models/settings/settings_game.dart'; +import 'package:twister/models/settings/settings_global.dart'; +import 'package:twister/utils/tools.dart'; + +class Game { + Game({ + // Settings + required this.gameSettings, + required this.globalSettings, + + // State + this.isRunning = false, + this.isStarted = false, + this.isFinished = false, + this.animationInProgress = false, + + // Base data + required this.move, + + // Game data + required this.history, + }); + + // Settings + final GameSettings gameSettings; + final GlobalSettings globalSettings; + + // State + bool isRunning; + bool isStarted; + bool isFinished; + bool animationInProgress; + + // Base data + Move? move; + + // Game data + List<Move> history; + + factory Game.createEmpty() { + return Game( + // Settings + gameSettings: GameSettings.createDefault(), + globalSettings: GlobalSettings.createDefault(), + // Base data + move: Move.createEmpty(), + // Game data + history: [], + ); + } + + factory Game.createNew({ + GameSettings? gameSettings, + GlobalSettings? globalSettings, + }) { + final GameSettings newGameSettings = gameSettings ?? GameSettings.createDefault(); + final GlobalSettings newGlobalSettings = globalSettings ?? GlobalSettings.createDefault(); + + return Game( + // Settings + gameSettings: newGameSettings, + globalSettings: newGlobalSettings, + // State + isRunning: true, + // Base data + move: Move.createEmpty(), + // Game data + history: [], + ); + } + + bool get canBeResumed => isStarted && !isFinished; + + void dump() { + printlog(''); + printlog('## Current game dump:'); + printlog(''); + printlog('$Game:'); + printlog(' Settings'); + gameSettings.dump(); + globalSettings.dump(); + printlog(' State'); + printlog(' isRunning: $isRunning'); + printlog(' isStarted: $isStarted'); + printlog(' isFinished: $isFinished'); + printlog(' animationInProgress: $animationInProgress'); + printlog(' Base data'); + printlog(' move: $move'); + printlog(' Game data'); + printlog(' history: $history'); + printlog(''); + } + + @override + String toString() { + return '$Game(${toJson()})'; + } + + Map<String, dynamic>? toJson() { + return <String, dynamic>{ + // Settings + 'gameSettings': gameSettings.toJson(), + 'globalSettings': globalSettings.toJson(), + // State + 'isRunning': isRunning, + 'isStarted': isStarted, + 'isFinished': isFinished, + 'animationInProgress': animationInProgress, + // Base data + 'move': move, + // Game data + 'history': history, + }; + } +} diff --git a/lib/models/move.dart b/lib/models/game/move.dart similarity index 84% rename from lib/models/move.dart rename to lib/models/game/move.dart index 0d47b0f03892a1434f703125799e181fc81d642c..a026fbccd8c23f3946cc4c10b5eb191be2e33d87 100644 --- a/lib/models/move.dart +++ b/lib/models/game/move.dart @@ -1,6 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; -import 'package:twister/models/twister_color.dart'; -import 'package:twister/models/twister_member.dart'; + +import 'package:twister/models/game/twister_color.dart'; +import 'package:twister/models/game/twister_member.dart'; class Move { final TwisterColor? color; @@ -11,7 +12,7 @@ class Move { required this.member, }); - factory Move.createNull() { + factory Move.createEmpty() { return Move( color: null, member: null, @@ -34,7 +35,7 @@ class Move { @override String toString() { - return 'Move(${toJson()})'; + return '$Move(${toJson()})'; } String toSoundAsset() { diff --git a/lib/models/twister_color.dart b/lib/models/game/twister_color.dart similarity index 100% rename from lib/models/twister_color.dart rename to lib/models/game/twister_color.dart diff --git a/lib/models/twister_member.dart b/lib/models/game/twister_member.dart similarity index 100% rename from lib/models/twister_member.dart rename to lib/models/game/twister_member.dart diff --git a/lib/models/settings/settings_game.dart b/lib/models/settings/settings_game.dart new file mode 100644 index 0000000000000000000000000000000000000000..36331fff5aa8a2dcc27f892df35252aff5b2b0d6 --- /dev/null +++ b/lib/models/settings/settings_game.dart @@ -0,0 +1,41 @@ +import 'package:twister/config/default_game_settings.dart'; +import 'package:twister/utils/tools.dart'; + +class GameSettings { + final String timerValue; + + GameSettings({ + required this.timerValue, + }); + + static String getTimerValueFromUnsafe(String timerValue) { + if (DefaultGameSettings.allowedTimerValues.contains(timerValue)) { + return timerValue; + } + + return DefaultGameSettings.defaultTimerValue; + } + + factory GameSettings.createDefault() { + return GameSettings( + timerValue: DefaultGameSettings.defaultTimerValue, + ); + } + + void dump() { + printlog('$GameSettings:'); + printlog(' ${DefaultGameSettings.parameterCodeTimerValue}: $timerValue'); + printlog(''); + } + + @override + String toString() { + return '$GameSettings(${toJson()})'; + } + + Map<String, dynamic>? toJson() { + return <String, dynamic>{ + DefaultGameSettings.parameterCodeTimerValue: timerValue, + }; + } +} diff --git a/lib/models/settings/settings_global.dart b/lib/models/settings/settings_global.dart new file mode 100644 index 0000000000000000000000000000000000000000..8805c92e17340be871f9127cfd3035ee203828cc --- /dev/null +++ b/lib/models/settings/settings_global.dart @@ -0,0 +1,41 @@ +import 'package:twister/config/default_global_settings.dart'; +import 'package:twister/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/ui/game/game_end.dart b/lib/ui/game/game_end.dart new file mode 100644 index 0000000000000000000000000000000000000000..ea2b71bfb00916b904d7fe36752568144083868f --- /dev/null +++ b/lib/ui/game/game_end.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:twister/cubit/game_cubit.dart'; +import 'package:twister/models/game/game.dart'; +import 'package:twister/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; + + const Image decorationImage = Image( + image: AssetImage('assets/ui/game_end.png'), + fit: BoxFit.fill, + ); + + return Container( + margin: const EdgeInsets.all(2), + padding: const EdgeInsets.all(2), + child: Table( + defaultColumnWidth: const IntrinsicColumnWidth(), + children: [ + TableRow( + children: [ + const Column( + children: [decorationImage], + ), + Column( + children: [ + currentGame.animationInProgress == true + ? decorationImage + : const QuitGameButton() + ], + ), + const Column( + children: [decorationImage], + ), + ], + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/ui/widgets/app_titles.dart b/lib/ui/helpers/app_titles.dart similarity index 52% rename from lib/ui/widgets/app_titles.dart rename to lib/ui/helpers/app_titles.dart index 93541243613b0f20e76485520a275d1527a6fcc9..b98107b12fabc3114ebfbec994166b588abcf1ad 100644 --- a/lib/ui/widgets/app_titles.dart +++ b/lib/ui/helpers/app_titles.dart @@ -1,8 +1,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -class AppTitle extends StatelessWidget { - const AppTitle({super.key, required this.text}); +class AppHeader extends StatelessWidget { + const AppHeader({super.key, required this.text}); final String text; @@ -11,37 +11,22 @@ class AppTitle extends StatelessWidget { return Text( tr(text), textAlign: TextAlign.start, - style: Theme.of(context).textTheme.headlineLarge!.apply(fontWeightDelta: 2), + style: Theme.of(context).textTheme.headlineMedium!.apply(fontWeightDelta: 2), ); } } -class AppTitle1 extends StatelessWidget { - const AppTitle1({super.key, required this.text}); +class AppTitle extends StatelessWidget { + const AppTitle({super.key, required this.text}); final String text; @override Widget build(BuildContext context) { return Text( - text, + tr(text), textAlign: TextAlign.start, style: Theme.of(context).textTheme.titleLarge!.apply(fontWeightDelta: 2), ); } } - -class AppTitle2 extends StatelessWidget { - const AppTitle2({super.key, required this.text}); - - final String text; - - @override - Widget build(BuildContext context) { - return Text( - text, - textAlign: TextAlign.start, - style: Theme.of(context).textTheme.titleMedium!.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 0000000000000000000000000000000000000000..dedfb1b71763d45e0be793cbd6fddd10c3848ed8 --- /dev/null +++ b/lib/ui/helpers/outlined_text_widget.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; + +import 'package:twister/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 0000000000000000000000000000000000000000..0c6cf4707c051884b87acb362914632da18414fa --- /dev/null +++ b/lib/ui/layouts/game_layout.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:twister/cubit/game_cubit.dart'; +import 'package:twister/models/game/game.dart'; +import 'package:twister/ui/game/game_end.dart'; +import 'package:twister/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 GameBoardWidget(), + const Expanded(child: SizedBox.shrink()), + currentGame.isFinished ? const GameEndWidget() : const SizedBox.shrink(), + ], + ), + ); + }, + ); + } +} diff --git a/lib/ui/layouts/parameters_layout.dart b/lib/ui/layouts/parameters_layout.dart new file mode 100644 index 0000000000000000000000000000000000000000..7ccf9b8e8c630c0618c3c1dbfeac837224e99347 --- /dev/null +++ b/lib/ui/layouts/parameters_layout.dart @@ -0,0 +1,154 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:twister/config/default_game_settings.dart'; +import 'package:twister/config/default_global_settings.dart'; +import 'package:twister/cubit/settings_game_cubit.dart'; +import 'package:twister/cubit/settings_global_cubit.dart'; +import 'package:twister/ui/parameters/parameter_image.dart'; +import 'package:twister/ui/parameters/parameter_painter.dart'; +import 'package:twister/ui/widgets/actions/button_delete_saved_game.dart'; +import 'package:twister/ui/widgets/actions/button_game_start_new.dart'; +import 'package:twister/ui/widgets/actions/button_resume_saved_game.dart'; + +class ParametersLayout extends StatelessWidget { + const ParametersLayout({super.key, required this.canResume}); + + final bool canResume; + + final double separatorHeight = 8.0; + + @override + Widget build(BuildContext context) { + final List<Widget> lines = []; + + // Game settings + for (String code in DefaultGameSettings.availableParameters) { + lines.add(Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: buildParametersLine( + code: code, + isGlobal: false, + ), + )); + + lines.add(SizedBox(height: separatorHeight)); + } + + lines.add(SizedBox(height: separatorHeight)); + + 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 + for (String code in DefaultGlobalSettings.availableParameters) { + lines.add(Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: buildParametersLine( + code: code, + isGlobal: true, + ), + )); + + lines.add(SizedBox(height: separatorHeight)); + } + + return Column( + children: lines, + ); + } + + List<Widget> buildParametersLine({ + required String code, + required bool isGlobal, + }) { + final List<Widget> parameterButtons = []; + + final List<String> availableValues = isGlobal + ? DefaultGlobalSettings.getAvailableValues(code) + : DefaultGameSettings.getAvailableValues(code); + + if (availableValues.length <= 1) { + return []; + } + + for (String value in availableValues) { + final Widget parameterButton = BlocBuilder<GameSettingsCubit, GameSettingsState>( + builder: (BuildContext context, GameSettingsState gameSettingsState) { + return BlocBuilder<GlobalSettingsCubit, GlobalSettingsState>( + builder: (BuildContext context, GlobalSettingsState globalSettingsState) { + final GameSettingsCubit gameSettingsCubit = + BlocProvider.of<GameSettingsCubit>(context); + final GlobalSettingsCubit globalSettingsCubit = + BlocProvider.of<GlobalSettingsCubit>(context); + + final String currentValue = isGlobal + ? globalSettingsCubit.getParameterValue(code) + : gameSettingsCubit.getParameterValue(code); + + final bool isActive = (value == currentValue); + + 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: 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); + }, + ); + }, + ); + }, + ); + + parameterButtons.add(parameterButton); + } + + return parameterButtons; + } +} 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/parameters/parameter_painter.dart b/lib/ui/parameters/parameter_painter.dart new file mode 100644 index 0000000000000000000000000000000000000000..774c21dffbdbf04d2f8003359d746cb1655d7a07 --- /dev/null +++ b/lib/ui/parameters/parameter_painter.dart @@ -0,0 +1,90 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +import 'package:twister/models/settings/settings_game.dart'; +import 'package:twister/models/settings/settings_global.dart'; +import 'package:twister/utils/tools.dart'; + +class ParameterPainter extends CustomPainter { + const ParameterPainter({ + required this.code, + required this.value, + required this.isSelected, + required this.gameSettings, + required this.globalSettings, + }); + + final String code; + final String value; + final bool isSelected; + final GameSettings gameSettings; + final GlobalSettings globalSettings; + + @override + void paint(Canvas canvas, Size size) { + // force square + final double canvasSize = min(size.width, size.height); + + const Color borderColorEnabled = Colors.blue; + const Color borderColorDisabled = Colors.white; + + // "enabled/disabled" border + final paint = Paint(); + paint.style = PaintingStyle.stroke; + paint.color = isSelected ? borderColorEnabled : borderColorDisabled; + paint.strokeJoin = StrokeJoin.round; + paint.strokeWidth = 10; + canvas.drawRect( + Rect.fromPoints(const Offset(0, 0), Offset(canvasSize, canvasSize)), paint); + + // content + switch (code) { + default: + printlog('Unknown parameter: $code/$value'); + paintUnknownParameterItem(value, canvas, canvasSize); + } + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return false; + } + + // "unknown" parameter -> simple block with text + void paintUnknownParameterItem( + final String value, + final Canvas canvas, + final double size, + ) { + final paint = Paint(); + paint.strokeJoin = StrokeJoin.round; + paint.strokeWidth = 3; + + paint.color = Colors.grey; + paint.style = PaintingStyle.fill; + canvas.drawRect(Rect.fromPoints(const Offset(0, 0), Offset(size, size)), paint); + + final textSpan = TextSpan( + text: '?\n$value', + style: const TextStyle( + color: Colors.black, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ); + final textPainter = TextPainter( + text: textSpan, + textDirection: TextDirection.ltr, + textAlign: TextAlign.center, + ); + textPainter.layout(); + textPainter.paint( + canvas, + Offset( + (size - textPainter.width) * 0.5, + (size - textPainter.height) * 0.5, + ), + ); + } +} diff --git a/lib/ui/screens/home.dart b/lib/ui/screens/home.dart deleted file mode 100644 index f9adc5a400ff233aeb14dde37d097048037ba2df..0000000000000000000000000000000000000000 --- a/lib/ui/screens/home.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:twister/ui/widgets/game.dart'; -import 'package:unicons/unicons.dart'; - -class ScreenHome extends StatelessWidget { - const ScreenHome({super.key}); - - static Icon navBarIcon = const Icon(UniconsLine.home); - static String navBarText = 'bottom_nav_game'; - - @override - Widget build(BuildContext context) { - return Material( - color: Theme.of(context).colorScheme.background, - child: ListView( - padding: const EdgeInsets.symmetric(horizontal: 4), - physics: const BouncingScrollPhysics(), - children: const <Widget>[ - SizedBox(height: 8), - Game(), - SizedBox(height: 36), - ], - ), - ); - } -} diff --git a/lib/ui/screens/page_about.dart b/lib/ui/screens/page_about.dart new file mode 100644 index 0000000000000000000000000000000000000000..65c1e5da608aab8417d60db738d86d07cd3418e8 --- /dev/null +++ b/lib/ui/screens/page_about.dart @@ -0,0 +1,41 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +import 'package:twister/ui/helpers/app_titles.dart'; + +class PageAbout extends StatelessWidget { + const PageAbout({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: <Widget>[ + const SizedBox(height: 8), + const AppTitle(text: 'about_title'), + const Text('about_content').tr(), + FutureBuilder<PackageInfo>( + future: PackageInfo.fromPlatform(), + builder: (context, snapshot) { + switch (snapshot.connectionState) { + case ConnectionState.done: + return const Text('about_version').tr( + namedArgs: { + 'version': snapshot.data!.version, + }, + ); + default: + return const SizedBox(); + } + }, + ), + ], + ), + ); + } +} diff --git a/lib/ui/screens/page_game.dart b/lib/ui/screens/page_game.dart new file mode 100644 index 0000000000000000000000000000000000000000..6ad44e4c3ca7b5e669428681dc2bab87764a2b53 --- /dev/null +++ b/lib/ui/screens/page_game.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:twister/cubit/game_cubit.dart'; +import 'package:twister/models/game/game.dart'; +import 'package:twister/ui/layouts/game_layout.dart'; +import 'package:twister/ui/layouts/parameters_layout.dart'; + +class PageGame extends StatelessWidget { + const PageGame({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder<GameCubit, GameState>( + builder: (BuildContext context, GameState gameState) { + final Game currentGame = gameState.currentGame; + + return currentGame.isRunning + ? const GameLayout() + : ParametersLayout(canResume: currentGame.canBeResumed); + }, + ); + } +} diff --git a/lib/ui/screens/page_settings.dart b/lib/ui/screens/page_settings.dart new file mode 100644 index 0000000000000000000000000000000000000000..aa5bcc79ac4d4ce4aece824fe1c557158d1b9091 --- /dev/null +++ b/lib/ui/screens/page_settings.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +import 'package:twister/ui/helpers/app_titles.dart'; +import 'package:twister/ui/settings/settings_form.dart'; + +class PageSettings extends StatelessWidget { + const PageSettings({super.key}); + + @override + Widget build(BuildContext context) { + return const Padding( + padding: EdgeInsets.symmetric(horizontal: 8), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: <Widget>[ + SizedBox(height: 8), + AppTitle(text: 'settings_title'), + SizedBox(height: 8), + SettingsForm(), + ], + ), + ); + } +} diff --git a/lib/ui/screens/settings.dart b/lib/ui/screens/settings.dart deleted file mode 100644 index b0ecbd5132f60407d9a434f960a2423ecc10c296..0000000000000000000000000000000000000000 --- a/lib/ui/screens/settings.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -import 'package:twister/ui/widgets/app_titles.dart'; -import 'package:twister/ui/widgets/settings_form.dart'; -import 'package:unicons/unicons.dart'; - -class ScreenSettings extends StatelessWidget { - const ScreenSettings({super.key}); - - static Icon navBarIcon = const Icon(UniconsLine.setting); - static String navBarText = 'bottom_nav_settings'; - - @override - Widget build(BuildContext context) { - return Material( - color: Theme.of(context).colorScheme.background, - child: ListView( - padding: const EdgeInsets.symmetric(horizontal: 4), - physics: const BouncingScrollPhysics(), - children: <Widget>[ - const SizedBox(height: 8), - AppTitle1(text: tr('settings_title')), - const SettingsForm(), - ], - ), - ); - } -} diff --git a/lib/ui/settings/settings_form.dart b/lib/ui/settings/settings_form.dart new file mode 100644 index 0000000000000000000000000000000000000000..e29459055cea1d394791f6dffddb4c54987af8c3 --- /dev/null +++ b/lib/ui/settings/settings_form.dart @@ -0,0 +1,63 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:unicons/unicons.dart'; + +import 'package:twister/ui/settings/theme_card.dart'; + +class SettingsForm extends StatefulWidget { + const SettingsForm({super.key}); + + @override + State<SettingsForm> createState() => _SettingsFormState(); +} + +class _SettingsFormState extends State<SettingsForm> { + @override + void dispose() { + super.dispose(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: <Widget>[ + // Light/dark theme + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: <Widget>[ + const Text('settings_label_theme').tr(), + const Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ThemeCard( + mode: ThemeMode.system, + icon: UniconsLine.cog, + ), + ThemeCard( + mode: ThemeMode.light, + icon: UniconsLine.sun, + ), + ThemeCard( + mode: ThemeMode.dark, + icon: UniconsLine.moon, + ) + ], + ), + ], + ), + + const SizedBox(height: 16), + ], + ); + } +} diff --git a/lib/ui/settings/theme_card.dart b/lib/ui/settings/theme_card.dart new file mode 100644 index 0000000000000000000000000000000000000000..15f343d405a9c5977f872598b9534853ada91d9e --- /dev/null +++ b/lib/ui/settings/theme_card.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:twister/cubit/theme_cubit.dart'; + +class ThemeCard extends StatelessWidget { + const ThemeCard({ + super.key, + required this.mode, + required this.icon, + }); + + final IconData icon; + final ThemeMode mode; + + @override + Widget build(BuildContext context) { + return BlocBuilder<ThemeCubit, ThemeModeState>( + builder: (BuildContext context, ThemeModeState state) { + return Card( + elevation: 2, + shadowColor: Theme.of(context).colorScheme.shadow, + color: state.themeMode == mode + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.surface, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + margin: const EdgeInsets.all(5), + child: InkWell( + onTap: () => BlocProvider.of<ThemeCubit>(context).getTheme( + ThemeModeState(themeMode: mode), + ), + borderRadius: const BorderRadius.all(Radius.circular(12)), + child: Icon( + icon, + size: 32, + color: state.themeMode != mode + ? Theme.of(context).colorScheme.primary + : Colors.white, + ), + ), + ); + }, + ); + } +} diff --git a/lib/ui/skeleton.dart b/lib/ui/skeleton.dart index ecc81e1c3f615625b6a7303ecd08a84ef6d03bba..3238f96a07b26708eacb7ffe643cbbd8593d993a 100644 --- a/lib/ui/skeleton.dart +++ b/lib/ui/skeleton.dart @@ -1,40 +1,34 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:twister/cubit/bottom_nav_cubit.dart'; -import 'package:twister/ui/screens/home.dart'; -import 'package:twister/ui/screens/settings.dart'; -import 'package:twister/ui/widgets/app_bar.dart'; -import 'package:twister/ui/widgets/bottom_nav_bar.dart'; +import 'package:twister/config/menu.dart'; +import 'package:twister/cubit/nav_cubit.dart'; +import 'package:twister/ui/widgets/global_app_bar.dart'; -class SkeletonScreen extends StatefulWidget { +class SkeletonScreen extends StatelessWidget { const SkeletonScreen({super.key}); - @override - State<SkeletonScreen> createState() => _SkeletonScreenState(); -} - -class _SkeletonScreenState extends State<SkeletonScreen> { @override Widget build(BuildContext context) { - List<Widget> pageNavigation = <Widget>[ - const ScreenHome(), - const ScreenSettings(), - ]; - return Scaffold( - appBar: const StandardAppBar(), + appBar: const GlobalAppBar(), extendBodyBehindAppBar: false, - body: BlocBuilder<BottomNavCubit, int>( - builder: (BuildContext context, int state) { - return AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: pageNavigation.elementAt(state), - ); - }, + body: Material( + color: Theme.of(context).colorScheme.surface, + child: BlocBuilder<NavCubit, int>( + builder: (BuildContext context, int pageIndex) { + return Padding( + padding: const EdgeInsets.only( + top: 8, + left: 2, + right: 2, + ), + child: Menu.getPageWidget(pageIndex), + ); + }, + ), ), - backgroundColor: Theme.of(context).colorScheme.background, - bottomNavigationBar: const BottomNavBar(), + 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..4036c2897361be391fe5776f9a851fef870c250b --- /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:twister/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..58cde3ebf3da0f79515b024969e3cb9ff6c546e4 --- /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:twister/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/actions/button_game_start_new.dart b/lib/ui/widgets/actions/button_game_start_new.dart new file mode 100644 index 0000000000000000000000000000000000000000..fd246a8b19ac62f25580fbd7471e7cd128ed8b06 --- /dev/null +++ b/lib/ui/widgets/actions/button_game_start_new.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:twister/cubit/game_cubit.dart'; +import 'package:twister/cubit/settings_game_cubit.dart'; +import 'package:twister/cubit/settings_global_cubit.dart'; + +class StartNewGameButton extends StatelessWidget { + const StartNewGameButton({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder<GameSettingsCubit, GameSettingsState>( + builder: (BuildContext context, GameSettingsState gameSettingsState) { + return BlocBuilder<GlobalSettingsCubit, GlobalSettingsState>( + builder: (BuildContext context, GlobalSettingsState globalSettingsState) { + return TextButton( + child: const Image( + image: AssetImage('assets/ui/button_start.png'), + fit: BoxFit.fill, + ), + 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..45dc8ffcb32c315e069ddea47af100ffc347695d --- /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:twister/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/app_bar.dart b/lib/ui/widgets/app_bar.dart deleted file mode 100644 index b777c21f0cfbd5e5abf11e28e89f4cbb0b22c6ee..0000000000000000000000000000000000000000 --- a/lib/ui/widgets/app_bar.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:twister/ui/widgets/app_titles.dart'; - -class StandardAppBar extends StatelessWidget implements PreferredSizeWidget { - const StandardAppBar({super.key}); - - @override - Widget build(BuildContext context) { - return AppBar( - title: const AppTitle(text: 'app_name'), - actions: const [], - ); - } - - @override - Size get preferredSize => const Size.fromHeight(50); -} diff --git a/lib/ui/widgets/bottom_nav_bar.dart b/lib/ui/widgets/bottom_nav_bar.dart deleted file mode 100644 index 24e9a838033600975936ce691d5368be7ec999f5..0000000000000000000000000000000000000000 --- a/lib/ui/widgets/bottom_nav_bar.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'package:twister/cubit/bottom_nav_cubit.dart'; -import 'package:twister/ui/screens/settings.dart'; -import 'package:twister/ui/screens/home.dart'; - -class BottomNavBar extends StatelessWidget { - const BottomNavBar({super.key}); - - @override - Widget build(BuildContext context) { - return Card( - margin: const EdgeInsets.only(top: 1, right: 4, left: 4), - elevation: 4, - shadowColor: Theme.of(context).colorScheme.shadow, - color: Theme.of(context).colorScheme.surfaceVariant, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), - ), - ), - child: BlocBuilder<BottomNavCubit, int>(builder: (BuildContext context, int state) { - return BottomNavigationBar( - currentIndex: state, - onTap: (int index) => context.read<BottomNavCubit>().updateIndex(index), - type: BottomNavigationBarType.fixed, - elevation: 0, - backgroundColor: Colors.transparent, - selectedItemColor: Theme.of(context).colorScheme.primary, - unselectedItemColor: Theme.of(context).textTheme.bodySmall!.color, - items: <BottomNavigationBarItem>[ - BottomNavigationBarItem( - icon: ScreenHome.navBarIcon, - label: tr(ScreenHome.navBarText), - ), - BottomNavigationBarItem( - icon: ScreenSettings.navBarIcon, - label: tr(ScreenSettings.navBarText), - ), - ], - ); - }), - ); - } -} diff --git a/lib/ui/widgets/game.dart b/lib/ui/widgets/game.dart deleted file mode 100644 index fadc9253e4151c0d040b76d86c77cd72e86dc8e9..0000000000000000000000000000000000000000 --- a/lib/ui/widgets/game.dart +++ /dev/null @@ -1,159 +0,0 @@ -import 'dart:async'; -import 'dart:math'; - -import 'package:audioplayers/audioplayers.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'package:twister/cubit/game_cubit.dart'; -import 'package:twister/models/move.dart'; -import 'package:twister/models/twister_color.dart'; -import 'package:twister/models/twister_member.dart'; -import 'package:twister/ui/widgets/show_move.dart'; - -class Game extends StatefulWidget { - const Game({super.key}); - - @override - State<Game> createState() => _GameState(); -} - -class _GameState extends State<Game> { - final player = AudioPlayer(); - bool shuffling = false; - Move? shuffledMove; - - void animate() { - const interval = Duration(milliseconds: 200); - int iterationsLeft = 10; - - shuffling = true; - shuffledMove = null; - - setState(() {}); - Timer.periodic( - interval, - (Timer timer) { - if (iterationsLeft > 1) { - shuffledMove = Move.pickRandom(); - } - - if (iterationsLeft == 1) { - shuffledMove = null; - } - - if (iterationsLeft == 0) { - shuffledMove = null; - pickNewMove(); - - timer.cancel(); - shuffling = false; - } - - setState(() {}); - iterationsLeft--; - }, - ); - } - - void pickNewMove() { - Move newMove = Move.pickRandom(); - - BlocProvider.of<GameCubit>(context).setValues( - move: newMove, - ); - - player.play(AssetSource(newMove.toSoundAsset())); - } - - Widget buildCurrentStateWidget(List<Move>? history) { - Map<String, TwisterColor?> currentState = {}; - - history?.forEach((move) { - currentState[move.member.toString()] = move.color; - }); - - TwisterMember leftHand = TwisterMember(value: TwisterAllowedMembers.leftHand); - TwisterMember rightHand = TwisterMember(value: TwisterAllowedMembers.rightHand); - TwisterMember leftFoot = TwisterMember(value: TwisterAllowedMembers.leftFoot); - TwisterMember rightFoot = TwisterMember(value: TwisterAllowedMembers.rightFoot); - - Move leftHandMove = Move.createFrom( - member: leftHand, - color: currentState[leftHand.toString()], - ); - Move rightHandMove = Move.createFrom( - member: rightHand, - color: currentState[rightHand.toString()], - ); - Move leftFootMove = Move.createFrom( - member: leftFoot, - color: currentState[leftFoot.toString()], - ); - Move rightFootMove = Move.createFrom( - member: rightFoot, - color: currentState[rightFoot.toString()], - ); - - return Padding( - padding: const EdgeInsets.all(30), - child: Table( - children: [ - TableRow( - children: [ - Padding( - padding: const EdgeInsets.all(10), - child: ShowMove(move: leftHandMove), - ), - Padding( - padding: const EdgeInsets.all(10), - child: ShowMove(move: rightHandMove), - ), - ], - ), - TableRow( - children: [ - Padding( - padding: const EdgeInsets.all(10), - child: ShowMove(move: leftFootMove), - ), - Padding( - padding: const EdgeInsets.all(10), - child: ShowMove(move: rightFootMove), - ), - ], - ) - ], - ), - ); - } - - @override - Widget build(BuildContext context) { - return BlocBuilder<GameCubit, GameState>( - builder: (BuildContext context, GameState gameState) { - return Column( - children: [ - GestureDetector( - child: shuffling - ? Transform.rotate( - angle: 2 * pi * Random().nextDouble(), - child: ShowMove(move: shuffledMove ?? Move.createNull()), - ) - : ShowMove(move: gameState.move ?? Move.createNull()), - onTap: () { - animate(); - }, - ), - GestureDetector( - child: buildCurrentStateWidget(gameState.history), - onTap: () { - BlocProvider.of<GameCubit>(context).deleteHistory(); - }, - ), - ], - ); - }, - ); - } -} diff --git a/lib/ui/widgets/game/game_board.dart b/lib/ui/widgets/game/game_board.dart new file mode 100644 index 0000000000000000000000000000000000000000..0fcdef29991b9fa9811978d0de8a07d134387fa9 --- /dev/null +++ b/lib/ui/widgets/game/game_board.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +import 'package:twister/ui/widgets/game/game_current_move.dart'; +import 'package:twister/ui/widgets/game/game_moves_history.dart'; + +class GameBoardWidget extends StatelessWidget { + const GameBoardWidget({super.key}); + + @override + Widget build(BuildContext context) { + return const Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + GameCurrentMoveWidget(), + GameMovesHistoryWidget(), + ], + ); + } +} diff --git a/lib/ui/widgets/game/game_current_move.dart b/lib/ui/widgets/game/game_current_move.dart new file mode 100644 index 0000000000000000000000000000000000000000..ae0c7c40c8caca693c9f1f04a98760a909b1d87c --- /dev/null +++ b/lib/ui/widgets/game/game_current_move.dart @@ -0,0 +1,80 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:twister/cubit/game_cubit.dart'; +import 'package:twister/models/game/move.dart'; +import 'package:twister/ui/widgets/game/move.dart'; + +class GameCurrentMoveWidget extends StatefulWidget { + const GameCurrentMoveWidget({super.key}); + + @override + State<GameCurrentMoveWidget> createState() => _GameCurrentMoveWidgetState(); +} + +class _GameCurrentMoveWidgetState extends State<GameCurrentMoveWidget> { + Move? shuffledMove; + + void animate() { + final GameCubit gameCubit = BlocProvider.of<GameCubit>(context); + if (gameCubit.state.currentGame.animationInProgress == true) { + return; + } + + const interval = Duration(milliseconds: 200); + int iterationsLeft = 10; + + gameCubit.setAnimationIsRunning(true); + shuffledMove = null; + + setState(() {}); + Timer.periodic( + interval, + (Timer timer) { + // animate with random move + if (iterationsLeft > 1) { + shuffledMove = Move.pickRandom(); + } + + // last iteration => clear + if (iterationsLeft == 1) { + shuffledMove = null; + } + + // end: pick and keep random move + if (iterationsLeft == 0) { + shuffledMove = null; + timer.cancel(); + + gameCubit.pickNewMove(); + gameCubit.setAnimationIsRunning(false); + } + + setState(() {}); + iterationsLeft--; + }, + ); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder<GameCubit, GameState>( + builder: (BuildContext context, GameState gameState) { + return GestureDetector( + child: gameState.currentGame.animationInProgress + ? Transform.rotate( + angle: 2 * pi * Random().nextDouble(), + child: MoveWidget(move: shuffledMove ?? Move.createEmpty()), + ) + : MoveWidget(move: gameState.currentGame.move ?? Move.createEmpty()), + onTap: () { + animate(); + }, + ); + }, + ); + } +} diff --git a/lib/ui/widgets/game/game_moves_history.dart b/lib/ui/widgets/game/game_moves_history.dart new file mode 100644 index 0000000000000000000000000000000000000000..67eb3e7a04f4a0e59fcbaad493b100ba9a44d72e --- /dev/null +++ b/lib/ui/widgets/game/game_moves_history.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:twister/cubit/game_cubit.dart'; +import 'package:twister/models/game/move.dart'; +import 'package:twister/models/game/twister_color.dart'; +import 'package:twister/models/game/twister_member.dart'; +import 'package:twister/ui/widgets/game/move.dart'; + +class GameMovesHistoryWidget extends StatelessWidget { + const GameMovesHistoryWidget({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder<GameCubit, GameState>( + builder: (BuildContext context, GameState gameState) { + return GestureDetector( + child: currentGlobalState(gameState.currentGame.history), + onTap: () { + BlocProvider.of<GameCubit>(context).deleteHistory(); + }, + ); + }, + ); + } + + Widget currentGlobalState(List<Move>? history) { + Map<String, TwisterColor?> currentState = {}; + + history?.forEach((move) { + currentState[move.member.toString()] = move.color; + }); + + TwisterMember leftHand = TwisterMember(value: TwisterAllowedMembers.leftHand); + TwisterMember rightHand = TwisterMember(value: TwisterAllowedMembers.rightHand); + TwisterMember leftFoot = TwisterMember(value: TwisterAllowedMembers.leftFoot); + TwisterMember rightFoot = TwisterMember(value: TwisterAllowedMembers.rightFoot); + + Move leftHandMove = Move.createFrom( + member: leftHand, + color: currentState[leftHand.toString()], + ); + Move rightHandMove = Move.createFrom( + member: rightHand, + color: currentState[rightHand.toString()], + ); + Move leftFootMove = Move.createFrom( + member: leftFoot, + color: currentState[leftFoot.toString()], + ); + Move rightFootMove = Move.createFrom( + member: rightFoot, + color: currentState[rightFoot.toString()], + ); + + return Padding( + padding: const EdgeInsets.all(30), + child: Table( + children: [ + TableRow( + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: MoveWidget(move: leftHandMove), + ), + Padding( + padding: const EdgeInsets.all(10), + child: MoveWidget(move: rightHandMove), + ), + ], + ), + TableRow( + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: MoveWidget(move: leftFootMove), + ), + Padding( + padding: const EdgeInsets.all(10), + child: MoveWidget(move: rightFootMove), + ), + ], + ) + ], + ), + ); + } +} diff --git a/lib/ui/widgets/show_move.dart b/lib/ui/widgets/game/move.dart similarity index 54% rename from lib/ui/widgets/show_move.dart rename to lib/ui/widgets/game/move.dart index c40dd3a6db62039285ae784c6683094bfc1224f8..48846a90524e8c667ffb6d4e31f2d2d2a9d94595 100644 --- a/lib/ui/widgets/show_move.dart +++ b/lib/ui/widgets/game/move.dart @@ -1,14 +1,14 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:twister/config/colors.dart'; -import 'package:twister/models/move.dart'; -import 'package:twister/models/twister_color.dart'; -import 'package:twister/models/twister_member.dart'; +import 'package:twister/config/color_theme.dart'; +import 'package:twister/models/game/move.dart'; +import 'package:twister/models/game/twister_color.dart'; +import 'package:twister/models/game/twister_member.dart'; import 'package:twister/utils/color_extensions.dart'; -class ShowMove extends StatelessWidget { - const ShowMove({super.key, required this.move}); +class MoveWidget extends StatelessWidget { + const MoveWidget({super.key, required this.move}); final Move move; @@ -27,12 +27,6 @@ class ShowMove extends StatelessWidget { } } - Widget getImageWidget(Move move) { - String imageAsset = 'assets/images/${move.member?.toString() ?? 'blank'}.png'; - - return Image.asset(imageAsset); - } - Widget getTextWidget(Move move) { TextStyle style = const TextStyle( color: Colors.black, @@ -54,38 +48,34 @@ class ShowMove extends StatelessWidget { } } - Widget buildWidget(Move move, double maxWidth) { - Color color = getColor(move); - - double containerSize = maxWidth * 0.8; - - return AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - transitionBuilder: (Widget child, Animation<double> animation) { - return ScaleTransition(scale: animation, child: child); - }, - child: Container( - width: containerSize, - height: containerSize, - decoration: BoxDecoration( - color: color, - borderRadius: BorderRadius.all(Radius.circular(containerSize)), - border: Border.all( - color: color.darken(15), - width: 15, - ), - ), - child: getImageWidget(move), - ), - ); - } - @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { final double maxWidth = constraints.maxWidth; - return buildWidget(move, maxWidth); + Color color = getColor(move); + + double containerSize = maxWidth * 0.8; + + return AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + transitionBuilder: (Widget child, Animation<double> animation) { + return ScaleTransition(scale: animation, child: child); + }, + child: Container( + width: containerSize, + height: containerSize, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.all(Radius.circular(containerSize)), + border: Border.all( + color: color.darken(15), + width: 15, + ), + ), + child: Image.asset('assets/ui/move-${move.member?.toString() ?? 'blank'}.png'), + ), + ); }, ); } diff --git a/lib/ui/widgets/global_app_bar.dart b/lib/ui/widgets/global_app_bar.dart new file mode 100644 index 0000000000000000000000000000000000000000..6b6c07efb0ede67ab7a37ec509214873b9baebcf --- /dev/null +++ b/lib/ui/widgets/global_app_bar.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:twister/config/menu.dart'; +import 'package:twister/cubit/game_cubit.dart'; +import 'package:twister/cubit/nav_cubit.dart'; +import 'package:twister/models/game/game.dart'; +import 'package:twister/ui/helpers/app_titles.dart'; + +class GlobalAppBar extends StatelessWidget implements PreferredSizeWidget { + const GlobalAppBar({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder<GameCubit, GameState>( + builder: (BuildContext context, GameState gameState) { + return BlocBuilder<NavCubit, int>( + builder: (BuildContext context, int pageIndex) { + final Game currentGame = gameState.currentGame; + + final List<Widget> menuActions = []; + + if (currentGame.isRunning && !currentGame.isFinished) { + menuActions.add(TextButton( + child: const Image( + image: AssetImage('assets/ui/button_back.png'), + fit: BoxFit.fill, + ), + onPressed: () {}, + onLongPress: () { + BlocProvider.of<GameCubit>(context).quitGame(); + }, + )); + } else { + if (pageIndex == Menu.indexGame) { + // go to Settings page + menuActions.add(ElevatedButton( + onPressed: () { + context.read<NavCubit>().goToSettingsPage(); + }, + style: ElevatedButton.styleFrom( + shape: const CircleBorder(), + ), + child: Menu.menuItemSettings.icon, + )); + + // go to About page + menuActions.add(ElevatedButton( + onPressed: () { + context.read<NavCubit>().goToAboutPage(); + }, + style: ElevatedButton.styleFrom( + shape: const CircleBorder(), + ), + child: Menu.menuItemAbout.icon, + )); + } else { + // back to Home page + menuActions.add(ElevatedButton( + onPressed: () { + context.read<NavCubit>().goToGamePage(); + }, + style: ElevatedButton.styleFrom( + shape: const CircleBorder(), + ), + child: Menu.menuItemGame.icon, + )); + } + } + + return AppBar( + title: const AppHeader(text: 'app_name'), + actions: menuActions, + ); + }, + ); + }, + ); + } + + @override + Size get preferredSize => const Size.fromHeight(50); +} diff --git a/lib/ui/widgets/settings_form.dart b/lib/ui/widgets/settings_form.dart deleted file mode 100644 index 81fac9e07e4174ad613b3d84e0047f6aaead6ba4..0000000000000000000000000000000000000000 --- a/lib/ui/widgets/settings_form.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:unicons/unicons.dart'; - -import 'package:twister/config/default_settings.dart'; -import 'package:twister/cubit/settings_cubit.dart'; -import 'package:twister/ui/widgets/app_titles.dart'; -import 'package:twister/ui/widgets/theme_card.dart'; - -class SettingsForm extends StatefulWidget { - const SettingsForm({super.key}); - - @override - State<SettingsForm> createState() => _SettingsFormState(); -} - -class _SettingsFormState extends State<SettingsForm> { - int timerValue = DefaultSettings.defaultTimerValue; - - List<bool> _selectedTimerValue = []; - - @override - void didChangeDependencies() { - SettingsCubit settings = BlocProvider.of<SettingsCubit>(context); - - timerValue = settings.getTimerValue(); - - _selectedTimerValue = - DefaultSettings.allowedTimerValues.map((e) => (e == timerValue)).toList(); - - super.didChangeDependencies(); - } - - @override - void dispose() { - super.dispose(); - } - - @override - Widget build(BuildContext context) { - void saveSettings() { - BlocProvider.of<SettingsCubit>(context).setValues( - timerValue: timerValue, - ); - } - - return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - children: <Widget>[ - const SizedBox(height: 8), - - // Light/dark theme - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: <Widget>[ - const Text('settings_label_theme').tr(), - const Row( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - ThemeCard( - mode: ThemeMode.system, - icon: UniconsLine.cog, - ), - ThemeCard( - mode: ThemeMode.light, - icon: UniconsLine.sun, - ), - ThemeCard( - mode: ThemeMode.dark, - icon: UniconsLine.moon, - ) - ], - ), - ], - ), - - const SizedBox(height: 16), - - AppTitle2(text: tr('settings_title_game')), - - // Timer value - Row( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Text('settings_label_game_timer_value').tr(), - ToggleButtons( - onPressed: (int index) { - setState(() { - timerValue = DefaultSettings.allowedTimerValues[index]; - for (int i = 0; i < _selectedTimerValue.length; i++) { - _selectedTimerValue[i] = i == index; - } - }); - saveSettings(); - }, - borderRadius: const BorderRadius.all(Radius.circular(8)), - constraints: const BoxConstraints(minHeight: 30.0, minWidth: 30.0), - isSelected: _selectedTimerValue, - children: - DefaultSettings.allowedTimerValues.map((e) => Text(e.toString())).toList(), - ), - ], - ), - ], - ); - } -} diff --git a/lib/ui/widgets/theme_card.dart b/lib/ui/widgets/theme_card.dart deleted file mode 100644 index bd935ab73dd26ff465d628f7fb82f3b1bdbad85c..0000000000000000000000000000000000000000 --- a/lib/ui/widgets/theme_card.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'package:twister/cubit/theme_cubit.dart'; - -class ThemeCard extends StatelessWidget { - const ThemeCard({ - super.key, - required this.mode, - required this.icon, - }); - - final IconData icon; - final ThemeMode mode; - - @override - Widget build(BuildContext context) { - return BlocBuilder<ThemeCubit, ThemeModeState>( - builder: (BuildContext context, ThemeModeState state) { - return Card( - elevation: 2, - shadowColor: Theme.of(context).colorScheme.shadow, - color: state.themeMode == mode - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.surface, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(12)), - ), - margin: const EdgeInsets.all(5), - child: InkWell( - onTap: () => BlocProvider.of<ThemeCubit>(context).getTheme( - ThemeModeState(themeMode: mode), - ), - borderRadius: const BorderRadius.all(Radius.circular(12)), - child: Icon( - icon, - size: 32, - color: - state.themeMode != mode ? Theme.of(context).colorScheme.primary : Colors.white, - ), - ), - ); - }); - } -} diff --git a/pubspec.lock b/pubspec.lock index fe15305da7d49aa628fe3a2241790bfa31038787..fc3ef495ce9e62261fac63f9fe041f119618b637 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.5.0" async: dependency: transitive description: @@ -21,66 +21,66 @@ packages: dependency: "direct main" description: name: audioplayers - sha256: c05c6147124cd63e725e861335a8b4d57300b80e6e92cea7c145c739223bbaef + sha256: "752039d6aa752597c98ec212e9759519061759e402e7da59a511f39d43aa07d2" url: "https://pub.dev" source: hosted - version: "5.2.1" + version: "6.0.0" audioplayers_android: dependency: transitive description: name: audioplayers_android - sha256: b00e1a0e11365d88576320ec2d8c192bc21f1afb6c0e5995d1c57ae63156acb5 + sha256: de576b890befe27175c2f511ba8b742bec83765fa97c3ce4282bba46212f58e4 url: "https://pub.dev" source: hosted - version: "4.0.3" + version: "5.0.0" audioplayers_darwin: dependency: transitive description: name: audioplayers_darwin - sha256: "3034e99a6df8d101da0f5082dcca0a2a99db62ab1d4ddb3277bed3f6f81afe08" + sha256: e507887f3ff18d8e5a10a668d7bedc28206b12e10b98347797257c6ae1019c3b url: "https://pub.dev" source: hosted - version: "5.0.2" + version: "6.0.0" audioplayers_linux: dependency: transitive description: name: audioplayers_linux - sha256: "60787e73fefc4d2e0b9c02c69885402177e818e4e27ef087074cf27c02246c9e" + sha256: "3d3d244c90436115417f170426ce768856d8fe4dfc5ed66a049d2890acfa82f9" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "4.0.0" audioplayers_platform_interface: dependency: transitive description: name: audioplayers_platform_interface - sha256: "365c547f1bb9e77d94dd1687903a668d8f7ac3409e48e6e6a3668a1ac2982adb" + sha256: "6834dd48dfb7bc6c2404998ebdd161f79cd3774a7e6779e1348d54a3bfdcfaa5" url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "7.0.0" audioplayers_web: dependency: transitive description: name: audioplayers_web - sha256: "22cd0173e54d92bd9b2c80b1204eb1eb159ece87475ab58c9788a70ec43c2a62" + sha256: db8fc420dadf80da18e2286c18e746fb4c3b2c5adbf0c963299dde046828886d url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "5.0.0" audioplayers_windows: dependency: transitive description: name: audioplayers_windows - sha256: "9536812c9103563644ada2ef45ae523806b0745f7a78e89d1b5fb1951de90e1a" + sha256: "8605762dddba992138d476f6a0c3afd9df30ac5b96039929063eceed416795c2" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "4.0.0" bloc: dependency: transitive description: name: bloc - sha256: f53a110e3b48dcd78136c10daa5d51512443cea5e1348c9d80a320095fa2db9e + sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" url: "https://pub.dev" source: hosted - version: "8.1.3" + version: "8.1.4" characters: dependency: transitive description: @@ -117,10 +117,10 @@ packages: dependency: "direct main" description: name: easy_localization - sha256: c145aeb6584aedc7c862ab8c737c3277788f47488bfdf9bae0fe112bd0a4789c + sha256: fa59bcdbbb911a764aa6acf96bbb6fa7a5cf8234354fc45ec1a43a0349ef0201 url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.7" easy_logger: dependency: transitive description: @@ -170,18 +170,18 @@ packages: dependency: "direct main" description: name: flutter_bloc - sha256: "87325da1ac757fcc4813e6b34ed5dd61169973871fdf181d6c2109dd6935ece1" + sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a url: "https://pub.dev" source: hosted - version: "8.1.4" + version: "8.1.6" flutter_lints: dependency: "direct dev" description: name: flutter_lints - sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7 + sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "4.0.0" flutter_localizations: dependency: transitive description: flutter @@ -220,34 +220,26 @@ packages: dependency: "direct main" description: name: hydrated_bloc - sha256: "00a2099680162e74b5a836b8a7f446e478520a9cae9f6032e028ad8129f4432d" + sha256: af35b357739fe41728df10bec03aad422cdc725a1e702e03af9d2a41ea05160c url: "https://pub.dev" source: hosted - version: "9.1.4" + version: "9.1.5" intl: dependency: transitive description: name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.18.1" - js: - dependency: transitive - description: - name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 - url: "https://pub.dev" - source: hosted - version: "0.6.7" + 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: @@ -260,10 +252,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: @@ -272,6 +264,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: b93d8b4d624b4ea19b0a5a208b2d6eff06004bc3ce74c06040b120eeadd00ce0 + url: "https://pub.dev" + source: hosted + version: "8.0.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: f49918f3433a3146047372f9d4f1f847511f2acd5cd030e1f44fe5a50036b70e + url: "https://pub.dev" + source: hosted + version: "3.0.0" path: dependency: transitive description: @@ -284,26 +292,26 @@ packages: dependency: "direct main" description: name: path_provider - sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b + sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" + sha256: "9c96da072b421e98183f9ea7464898428e764bc0ce5567f27ec8693442e72514" url: "https://pub.dev" source: hosted - version: "2.2.2" + 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: @@ -332,10 +340,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: @@ -356,26 +364,26 @@ packages: dependency: transitive description: name: shared_preferences - sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" + sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + sha256: "93d0ec9dd902d85f326068e6a899487d1f65ffcd5798721a95330b26c8131577" url: "https://pub.dev" source: hosted - version: "2.2.1" + 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: @@ -473,10 +481,10 @@ packages: dependency: transitive description: name: uuid - sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8 + sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8" url: "https://pub.dev" source: hosted - version: "4.3.3" + version: "4.4.0" vector_math: dependency: transitive description: @@ -489,18 +497,18 @@ packages: dependency: transitive description: name: web - sha256: "1d9158c616048c38f712a6646e317a3426da10e884447626167240d45209cbad" + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.5.1" win32: dependency: transitive description: name: win32 - sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" + sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4 url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.5.1" xdg_directories: dependency: transitive description: @@ -510,5 +518,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 4efd07ac9876b2eaaba71abd4e1e91cbfc531db4..16ca4fa71f59c870321d1572528a85c75a16b4eb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,33 +1,37 @@ name: twister -description: twister game companion +description: Twister game companion -publish_to: 'none' +publish_to: "none" -version: 0.0.23+23 +version: 0.1.0+24 environment: - sdk: '^3.0.0' + sdk: "^3.0.0" dependencies: flutter: sdk: flutter - audioplayers: ^5.2.1 + # base easy_localization: ^3.0.1 equatable: ^2.0.5 flutter_bloc: ^8.1.1 hive: ^2.2.3 - path_provider: ^2.0.11 hydrated_bloc: ^9.0.0 + package_info_plus: ^8.0.0 + path_provider: ^2.0.11 unicons: ^2.1.1 + # specific + audioplayers: ^6.0.0 + dev_dependencies: - flutter_lints: ^3.0.1 + flutter_lints: ^4.0.0 flutter: - uses-material-design: false + uses-material-design: true assets: - - assets/images/ + - assets/ui/ - assets/translations/ - assets/voices/ @@ -42,3 +46,4 @@ flutter: weight: 400 - asset: assets/fonts/Nunito-Light.ttf weight: 300 + diff --git a/icons/build_repository_icons.sh b/resources/app/build_application_resources.sh similarity index 98% rename from icons/build_repository_icons.sh rename to resources/app/build_application_resources.sh index 27dbe2647fe4e6d562fbd99451716d1b7d448570..6d67b8f4f9eca701d1aed7331ef41dfb0bd44f20 100755 --- a/icons/build_repository_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..68bc1d0e887903cab672cd6466b1e8c503f36148 --- /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 +${CURRENT_DIR}/tts/generate_sounds.sh diff --git a/tts/generate_sounds.sh b/resources/tts/generate_sounds.sh similarity index 98% rename from tts/generate_sounds.sh rename to resources/tts/generate_sounds.sh index 660e16f0120363d85d126f85c4a751010b843b7a..a5fe84c770936cea126c2562c6be08e6b2f8e079 100755 --- a/tts/generate_sounds.sh +++ b/resources/tts/generate_sounds.sh @@ -4,7 +4,7 @@ command -v pico2wave >/dev/null 2>&1 || { echo >&2 "I require pico2wave (libttspico-utils) 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}")")" OUTPUT_BASE_FOLDER="${BASE_DIR}/assets/voices" mkdir -p "${OUTPUT_BASE_FOLDER}" diff --git a/resources/ui/build_ui_resources.sh b/resources/ui/build_ui_resources.sh new file mode 100755 index 0000000000000000000000000000000000000000..93344c8ec66c5b8ea5d9a08a9cc0ca8f657700d1 --- /dev/null +++ b/resources/ui/build_ui_resources.sh @@ -0,0 +1,111 @@ +#! /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=512 +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/resources/ui/images/button_back.svg b/resources/ui/images/button_back.svg new file mode 100644 index 0000000000000000000000000000000000000000..2622a578dba53ce582afabfc587c2a85a1fb6eaa --- /dev/null +++ b/resources/ui/images/button_back.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect x=".44662" y=".89101" width="92.772" height="91.894" ry="11.689" fill="#e41578" stroke="#fff" stroke-width=".238"/><path d="m59.387 71.362c1.1248 1.1302 4.0012 1.1302 4.0012 0v-45.921c0-1.1316-2.8832-1.1316-4.0121 0l-37.693 20.918c-1.1289 1.1248-1.1479 2.9551-0.02171 4.084z" fill="#fefeff" stroke="#930e4e" stroke-linecap="round" stroke-linejoin="round" stroke-width="8.257"/><path d="m57.857 68.048c0.96243 0.96706 3.4236 0.96706 3.4236 0v-39.292c0-0.96825-2.467-0.96825-3.4329 0l-32.252 17.898c-0.96594 0.96243-0.9822 2.5285-0.01858 3.4945z" fill="#fefeff" stroke="#feffff" stroke-linecap="round" stroke-linejoin="round" stroke-width="4.314"/></svg> diff --git a/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/resources/ui/images/button_start.svg b/resources/ui/images/button_start.svg new file mode 100644 index 0000000000000000000000000000000000000000..e9d49d2172b9a0305db82779971e3c1e12f34a70 --- /dev/null +++ b/resources/ui/images/button_start.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect x=".44662" y=".89101" width="92.772" height="91.894" ry="11.689" fill="#49a1ee" stroke="#fff" stroke-width=".238"/><path d="m34.852 25.44c-1.1248-1.1302-4.0012-1.1302-4.0012 0v45.921c0 1.1316 2.8832 1.1316 4.0121 0l37.693-20.918c1.1289-1.1248 1.1479-2.9551 0.02171-4.084z" fill="#fefeff" stroke="#105ca1" stroke-linecap="round" stroke-linejoin="round" stroke-width="8.257"/><path d="m36.382 28.754c-0.96243-0.96706-3.4236-0.96706-3.4236 0v39.292c0 0.96825 2.467 0.96825 3.4329 0l32.252-17.898c0.96594-0.96243 0.9822-2.5285 0.01858-3.4945z" fill="#fefeff" stroke="#feffff" stroke-linecap="round" stroke-linejoin="round" stroke-width="4.314"/></svg> diff --git a/resources/ui/images/game_end.svg b/resources/ui/images/game_end.svg new file mode 100644 index 0000000000000000000000000000000000000000..fe20923864d0c5d39168eced03038b65106a596b --- /dev/null +++ b/resources/ui/images/game_end.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/images/blank.svg b/resources/ui/images/move-blank.svg similarity index 100% rename from images/blank.svg rename to resources/ui/images/move-blank.svg diff --git a/images/left-foot.svg b/resources/ui/images/move-left-foot.svg similarity index 100% rename from images/left-foot.svg rename to resources/ui/images/move-left-foot.svg diff --git a/images/left-hand.svg b/resources/ui/images/move-left-hand.svg similarity index 100% rename from images/left-hand.svg rename to resources/ui/images/move-left-hand.svg diff --git a/images/right-foot.svg b/resources/ui/images/move-right-foot.svg similarity index 100% rename from images/right-foot.svg rename to resources/ui/images/move-right-foot.svg diff --git a/images/right-hand.svg b/resources/ui/images/move-right-hand.svg similarity index 100% rename from images/right-hand.svg rename to resources/ui/images/move-right-hand.svg diff --git a/resources/ui/images/placeholder.svg b/resources/ui/images/placeholder.svg new file mode 100644 index 0000000000000000000000000000000000000000..23ace81fbb82a8409cc0710c0f7bddd6381f7256 --- /dev/null +++ b/resources/ui/images/placeholder.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 102 102" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"/>