From c7667f5fb179ad23beeff6f044d499f6308395ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Harrault?= <benoit@harrault.fr> Date: Wed, 15 May 2024 15:52:09 +0200 Subject: [PATCH] Normalize game architecture --- android/app/build.gradle | 2 +- android/gradle.properties | 4 +- assets/images/blank.png | Bin 197 -> 0 bytes assets/images/left-foot.png | Bin 6196 -> 0 bytes assets/images/left-hand.png | Bin 4383 -> 0 bytes assets/images/right-foot.png | Bin 6196 -> 0 bytes assets/images/right-hand.png | Bin 4325 -> 0 bytes assets/translations/en.json | 13 +- assets/translations/fr.json | 13 +- assets/ui/button_back.png | Bin 0 -> 3771 bytes assets/ui/button_delete_saved_game.png | Bin 0 -> 5813 bytes assets/ui/button_resume_game.png | Bin 0 -> 3659 bytes assets/ui/button_start.png | Bin 0 -> 3999 bytes assets/ui/game_end.png | Bin 0 -> 7937 bytes assets/ui/move-blank.png | Bin 0 -> 170 bytes assets/ui/move-left-foot.png | Bin 0 -> 2830 bytes assets/ui/move-left-hand.png | Bin 0 -> 1937 bytes assets/ui/move-right-foot.png | Bin 0 -> 2846 bytes assets/ui/move-right-hand.png | Bin 0 -> 1924 bytes assets/ui/placeholder.png | Bin 0 -> 170 bytes .../metadata/android/en-US/changelogs/24.txt | 1 + .../metadata/android/fr-FR/changelogs/24.txt | 1 + images/build_game_images.sh | 80 --------- lib/config/color_theme.dart | 10 ++ lib/config/colors.dart | 10 -- lib/config/default_game_settings.dart | 33 ++++ lib/config/default_global_settings.dart | 33 ++++ lib/config/default_settings.dart | 9 - lib/config/menu.dart | 52 ++++++ lib/config/theme.dart | 10 +- lib/cubit/bottom_nav_cubit.dart | 31 ---- lib/cubit/game_cubit.dart | 109 +++++++++--- lib/cubit/game_state.dart | 14 +- lib/cubit/nav_cubit.dart | 37 ++++ lib/cubit/settings_cubit.dart | 39 ----- lib/cubit/settings_game_cubit.dart | 61 +++++++ lib/cubit/settings_game_state.dart | 15 ++ lib/cubit/settings_global_cubit.dart | 60 +++++++ lib/cubit/settings_global_state.dart | 15 ++ lib/cubit/settings_state.dart | 19 --- lib/main.dart | 98 +++++++---- lib/models/game/game.dart | 116 +++++++++++++ lib/models/{ => game}/move.dart | 9 +- lib/models/{ => game}/twister_color.dart | 0 lib/models/{ => game}/twister_member.dart | 0 lib/models/settings/settings_game.dart | 41 +++++ lib/models/settings/settings_global.dart | 41 +++++ lib/ui/game/game_end.dart | 51 ++++++ lib/ui/{widgets => helpers}/app_titles.dart | 27 +-- lib/ui/helpers/outlined_text_widget.dart | 51 ++++++ lib/ui/layouts/game_layout.dart | 34 ++++ lib/ui/layouts/parameters_layout.dart | 154 +++++++++++++++++ lib/ui/parameters/parameter_image.dart | 38 +++++ lib/ui/parameters/parameter_painter.dart | 90 ++++++++++ lib/ui/screens/home.dart | 26 --- lib/ui/screens/page_about.dart | 41 +++++ lib/ui/screens/page_game.dart | 24 +++ lib/ui/screens/page_settings.dart | 26 +++ lib/ui/screens/settings.dart | 29 ---- lib/ui/settings/settings_form.dart | 63 +++++++ lib/ui/settings/theme_card.dart | 47 ++++++ lib/ui/skeleton.dart | 46 +++-- .../actions/button_delete_saved_game.dart | 21 +++ lib/ui/widgets/actions/button_game_quit.dart | 21 +++ .../actions/button_game_start_new.dart | 34 ++++ .../actions/button_resume_saved_game.dart | 21 +++ lib/ui/widgets/app_bar.dart | 18 -- lib/ui/widgets/bottom_nav_bar.dart | 48 ------ lib/ui/widgets/game.dart | 159 ------------------ lib/ui/widgets/game/game_board.dart | 20 +++ lib/ui/widgets/game/game_current_move.dart | 80 +++++++++ lib/ui/widgets/game/game_moves_history.dart | 88 ++++++++++ .../{show_move.dart => game/move.dart} | 68 ++++---- lib/ui/widgets/global_app_bar.dart | 83 +++++++++ lib/ui/widgets/settings_form.dart | 113 ------------- lib/ui/widgets/theme_card.dart | 45 ----- pubspec.lock | 132 ++++++++------- pubspec.yaml | 23 ++- .../app/build_application_resources.sh | 2 +- {icons => resources/app}/featureGraphic.svg | 0 {icons => resources/app}/icon.svg | 0 resources/build_resources.sh | 7 + {tts => resources/tts}/generate_sounds.sh | 2 +- resources/ui/build_ui_resources.sh | 111 ++++++++++++ resources/ui/images/button_back.svg | 2 + .../ui/images/button_delete_saved_game.svg | 2 + resources/ui/images/button_resume_game.svg | 2 + resources/ui/images/button_start.svg | 2 + resources/ui/images/game_end.svg | 2 + .../ui/images/move-blank.svg | 0 .../ui/images/move-left-foot.svg | 0 .../ui/images/move-left-hand.svg | 0 .../ui/images/move-right-foot.svg | 0 .../ui/images/move-right-hand.svg | 0 resources/ui/images/placeholder.svg | 2 + 95 files changed, 1953 insertions(+), 878 deletions(-) delete mode 100644 assets/images/blank.png delete mode 100644 assets/images/left-foot.png delete mode 100644 assets/images/left-hand.png delete mode 100644 assets/images/right-foot.png delete mode 100644 assets/images/right-hand.png create mode 100644 assets/ui/button_back.png create mode 100644 assets/ui/button_delete_saved_game.png create mode 100644 assets/ui/button_resume_game.png create mode 100644 assets/ui/button_start.png create mode 100644 assets/ui/game_end.png create mode 100644 assets/ui/move-blank.png create mode 100644 assets/ui/move-left-foot.png create mode 100644 assets/ui/move-left-hand.png create mode 100644 assets/ui/move-right-foot.png create mode 100644 assets/ui/move-right-hand.png create mode 100644 assets/ui/placeholder.png create mode 100644 fastlane/metadata/android/en-US/changelogs/24.txt create mode 100644 fastlane/metadata/android/fr-FR/changelogs/24.txt delete mode 100755 images/build_game_images.sh create mode 100644 lib/config/color_theme.dart delete mode 100644 lib/config/colors.dart create mode 100644 lib/config/default_game_settings.dart create mode 100644 lib/config/default_global_settings.dart delete mode 100644 lib/config/default_settings.dart create mode 100644 lib/config/menu.dart delete mode 100644 lib/cubit/bottom_nav_cubit.dart create mode 100644 lib/cubit/nav_cubit.dart delete mode 100644 lib/cubit/settings_cubit.dart create mode 100644 lib/cubit/settings_game_cubit.dart create mode 100644 lib/cubit/settings_game_state.dart create mode 100644 lib/cubit/settings_global_cubit.dart create mode 100644 lib/cubit/settings_global_state.dart delete mode 100644 lib/cubit/settings_state.dart create mode 100644 lib/models/game/game.dart rename lib/models/{ => game}/move.dart (84%) rename lib/models/{ => game}/twister_color.dart (100%) rename lib/models/{ => game}/twister_member.dart (100%) create mode 100644 lib/models/settings/settings_game.dart create mode 100644 lib/models/settings/settings_global.dart create mode 100644 lib/ui/game/game_end.dart rename lib/ui/{widgets => helpers}/app_titles.dart (52%) create mode 100644 lib/ui/helpers/outlined_text_widget.dart create mode 100644 lib/ui/layouts/game_layout.dart create mode 100644 lib/ui/layouts/parameters_layout.dart create mode 100644 lib/ui/parameters/parameter_image.dart create mode 100644 lib/ui/parameters/parameter_painter.dart delete mode 100644 lib/ui/screens/home.dart create mode 100644 lib/ui/screens/page_about.dart create mode 100644 lib/ui/screens/page_game.dart create mode 100644 lib/ui/screens/page_settings.dart delete mode 100644 lib/ui/screens/settings.dart create mode 100644 lib/ui/settings/settings_form.dart create mode 100644 lib/ui/settings/theme_card.dart create mode 100644 lib/ui/widgets/actions/button_delete_saved_game.dart create mode 100644 lib/ui/widgets/actions/button_game_quit.dart create mode 100644 lib/ui/widgets/actions/button_game_start_new.dart create mode 100644 lib/ui/widgets/actions/button_resume_saved_game.dart delete mode 100644 lib/ui/widgets/app_bar.dart delete mode 100644 lib/ui/widgets/bottom_nav_bar.dart delete mode 100644 lib/ui/widgets/game.dart create mode 100644 lib/ui/widgets/game/game_board.dart create mode 100644 lib/ui/widgets/game/game_current_move.dart create mode 100644 lib/ui/widgets/game/game_moves_history.dart rename lib/ui/widgets/{show_move.dart => game/move.dart} (54%) create mode 100644 lib/ui/widgets/global_app_bar.dart delete mode 100644 lib/ui/widgets/settings_form.dart delete mode 100644 lib/ui/widgets/theme_card.dart rename icons/build_repository_icons.sh => resources/app/build_application_resources.sh (98%) rename {icons => resources/app}/featureGraphic.svg (100%) rename {icons => resources/app}/icon.svg (100%) create mode 100755 resources/build_resources.sh rename {tts => resources/tts}/generate_sounds.sh (98%) create mode 100755 resources/ui/build_ui_resources.sh create mode 100644 resources/ui/images/button_back.svg create mode 100644 resources/ui/images/button_delete_saved_game.svg create mode 100644 resources/ui/images/button_resume_game.svg create mode 100644 resources/ui/images/button_start.svg create mode 100644 resources/ui/images/game_end.svg rename images/blank.svg => resources/ui/images/move-blank.svg (100%) rename images/left-foot.svg => resources/ui/images/move-left-foot.svg (100%) rename images/left-hand.svg => resources/ui/images/move-left-hand.svg (100%) rename images/right-foot.svg => resources/ui/images/move-right-foot.svg (100%) rename images/right-hand.svg => resources/ui/images/move-right-hand.svg (100%) create mode 100644 resources/ui/images/placeholder.svg diff --git a/android/app/build.gradle b/android/app/build.gradle index 27a93e6..2328355 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 3487476..1913fd1 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 GIT binary patch literal 0 HcmV?d00001 literal 197 zcmeAS@N?(olHy`uVBq!ia0y~yU}6AaMrH;EhI8B8b}}$9a29w(7BevL9R^{><M}I6 z7#J8NOI#yLg7ec#$`gxH8OqDc^)mCai<1)zQuXqS(r3T3kz!zAU=HvJasB`Q|MDZ! zCm0wQ7)yfuf*Bm1-AH3#U@-G^aSV}=e0z|Qk%57sX~DnsWgZ6@2!L~}O$-c-TiLVL PffRbW`njxgN@xNA7Y;A$ diff --git a/assets/images/left-foot.png b/assets/images/left-foot.png deleted file mode 100644 index fd46c1a8ac871945c801a6a353ebb0b70639df7f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6196 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 diff --git a/assets/images/left-hand.png b/assets/images/left-hand.png deleted file mode 100644 index 34d676bb3985fe6479e20d003c1be4370d20857d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4383 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 I=akR{0NP>|oB#j- diff --git a/assets/images/right-foot.png b/assets/images/right-foot.png deleted file mode 100644 index d0eff85495b7b8910389f948a712b02cde1909ef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6196 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 diff --git a/assets/images/right-hand.png b/assets/images/right-hand.png deleted file mode 100644 index e730e5f4c58c8bdf3444ae7ff7ad45fe3e32e3e3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4325 zcmeAS@N?(olHy`uVBq!ia0y~yU}6Aa4rT@hhQrHLPB1Vqa29w(7BevL9R^{><M}I6 z7#J8NOI#yLg7ec#$`gxH8OqDc^)mCai<1)zQuXqS(r3T3kz!zA`V!z1;`;ype}+*o z8UiCJ1dfWo_{+e+@Vg|)FPMRmiJ66!jh%y&i<^g+k6%DgNLWNvOk6@zN?Jx%PF_J# zNm)fzO<hA%OIt@*Pv5}M$k@cx%-q7#%G$=(&fdYv*~Qh(-NVz%+sD_>KOitDI3zSI zJR&kGIwm$QJ|QtFIVCkMJtH$KJ0~|Uzo4+FxTLhKyrQzIx~8_SzM-+HxuvzOy`!_M zr?+py#7UE<Or17;#>`o>=ggfqf5E~<i<c~2wtU6PRjb#mUAKP2#!Z{IY~8kf$Ie~5 z_w3!b|G>dRhmRaRcH-oz(`U|}JAdKgrOQ{YUb}wd=B?Xz?%uoq;NhdkPo6$|{^I4U z*Kgjwd;j6%r_W!$e*6C8=da&?{{GuGwbzG%fhXJ3#WAGf*4w+86{1%q4-~%l^*A)m zf#GJ0)~3swP9#Z(EC^^+a?;x3up!7xD?v0PRa;C;W80~H9U;mgt^&EI!Z@1*7I;h% z3*ec3q}A2IL&f1l(f$yZUm+?x)6bpRTmH=U|NZB|zir<CR^NI0%#${0l9O7j=`UaZ z`_ulu>kD6gSNOb%kFdGPW{nT_tS@W-oH?TK)NMXbjPh>zWJ9-he&*{F1G`VoNIbLj zK&kB8GdVmD=F4jMUNktSe80)zWf!}F{L5o(r;1jp3s2Hrx#A21Q~GNo<&&r6d00NO zIU9W!KXCn*vFc8(dO@28Tf1i}*E$-S^3Hg!`5wX)ad^&>T`OHw8y02xc&$)mRIQOx zxhk`jA<am0q1g)74V|8xTf=o5nti-h$ZTaeXP~*TZ#C<L^GnX~JxE&lzp<z^+I&TP zpQDtd!y{(z&HMk!EPSz8wOPy1=P;8;)!Gly`&GqE)8??TUP!rHZm%D6fW2)Q%LD1y zg1yP|>V|>JOd9()ZFzmX?z{&theBfUOZDZ(!5<l?oYr{#qM}X9w!t-Z{@(d3Pw_pt zr;uL!+A!=S;}Q11^Vg)Vc{g#w6?N0n2ePGexD&iSTt0c`uIQiGXDRV}A8Dt5>|ZRy zxu7WT!{z9id%2JF+7z5E|8+Q7hU0;`)^Tfte;u!DU+k{FdH-x3!|jm1{U`n?|2<!S ztyDbNX!V@446DRe%Gk@E<x*Oewle(p{@A;5412Gh+n&lkF(kkK<?54O8$=JdZ2io? zsfDBKe(bfpZ6O<k9cJD7DVd|#bm?^5xzlA#70z$}tj<v^s>^*^EVY_Z<DUD0vn>;Y z-{x$1E3<x?`~kPEpI_f}K4K$VW9#2;)4=mhK$G)%(|UFNMusE5)}E@9Q)Jj^6#DJ6 z9ivC+(tu1yUJi#9yRV9U@n#ap+H$KYO}HUo^Hs4evlv=}{;c_D%OsH1daLQ1U_-$A z$eJhIi~<f4%quQd-}0WldDFk2Z(fAi)Lz@caB6wpul=u2^8H_FcH2m^;RfgRf1#VY zi!0|eSTdIEyce%(VLPF9reH&Uk*2uteu2}ccsLZQGGF#9tUYk<2>S#5*M~bzZIb+I z7*gXGf8omKXk{08Q^|D4S;4wNO1&v|LD>AQo_q;m7Y^G5I2AE`<!+jvcl_QHwhv1V zy<GanH-{lm>pwedjjn=OgUZs!{2MJ+GWrBs?VEMflQ&`NrH_+!{IVF>ub%s?<dDVi z{K~n{5rzvHU8b*?`~KXs6t)1zw-pP_XB%5LRNs_gY+!I;P+$;X;9y{3U}C__RbXHm zn#jx@;YEYE-fy<QzrHTNu4~AWFz?y^=<+wOa=6OQyb^R~U|ad?!@KO~#oyMiox}TJ z@#^RPE9dHesV%PSzV);BzD&P5!|F%s&*T2z53yEf-VyL`_Khx4^ODa~KkxHj%X)1V zgN{_eqAil5`YYeXFf=BH_t~$iurc_1>gW7b^WIlI{ZqrVVacsko0gSqX)k7ca<}wn z$#?Em+`3W~KWk1WuX=8uUHJ3QdM}OxUDw)A%LSiQnIFu^(iLv=rC`^qI=$NlX)aNX zzdm-K@0Zx@`O}0k=GxEG3!1AGPpxFQ)5@^#l5TurP-Y#Iz^c@FmsX?+HC*8T*QaqV z%Yd=tOZnBvx%qw!OtuT`)~u~#n(+PSeNOJnQhp4NY!XwWmJ4$_%s91k+A-7V3@wtW zxiPM~oDS2z>z;e9%OtRL<I_e};Rcn*I{a@6WEeR%9s70Up94eD55K7S5p}gp0*kku zJ26eDL1O#so;6SWm?kXhzjo%{N8ttm<}Yn#EetIl7o~4~WMP`HaR0L_|C|_%O0La` za!h6tSh(`tjWEFmj*H@@&#yHzxZGH3y}kA+heK24t!8Og2B9yP<YMx4nFJObvV42e z)|){{=i0oOLTe_01$+O!F#FBn(7^j^^KVN=j~{{oi)?rMYY8<lXn$!ppUJ?oV)}a9 zJ)g~m8yJGWF5lk6pi(0?VX^%_KNf`p34cGn?NVZR|2g&5dHtyjEE&;jZ1)FuvM3zr z{>41Uk%8&TnX7UM55IFdG<5zFKBK_En8g30e}gz9#|PuTf^t2o42;)apZ^fZB(T7B z;WwVoS;7qr++W&%*f4TTsPL-pI4aBG&=B}5_|7|JhWFj~R@)yFVG>xd`k#dTS{DYU zHP_}pHenK2u<+a7MUCCOoDL1?U)Xm}VqjVEY3Dx`dqoDuAm(L?4f!9_1siT!O%rIi zIZe3XX3iZA7X~FCRff$scLsJasCX#CSQ`o#3ov#}aAQc@3}zWjWH=+Xc|OCLX+8{X z(f)AZ`3z^$9=j<r2_%Xz&d{ymXPmKCn^7XqlySz{W4%r+3WtnY61M$FW=Yr<!elVJ zUY&7<@4sFrmVj+fSrW`XnsW&{2n2H;$hnj+qSTNO#Bm_!5Qw!PPg{*)^HRZvn=bb? zTo}021R8FtfLL0SI1d!$3pL!FB`(x(^O<nN%~kUi88-K-Gi*NkRG{Hzn?S?OMc)J( zZdP^jJdhJx{YJNwu_S%{saxKA-1cx+hpK;+`e+@odY2oMy}-V?TcbXhS2CKUuNE_z zTwa-wyDjVJ>qkr-$t%OQO#J5dfXRAm+T0BceVc-^>r`|a=B~N2NOu!c#oXSglI?34 z(l!UKPO&vj)8XE#<r>29H*!U&;c->g3zJ%-%zQ8W>ps2N|MaVvNx}ydudPg6I)~xf z24CLTCoBe2b6?%iRb$ZOylUj+!%!Nrtj{aG_04~Y-B&g@<TIVQ7U;pyzroA*W({A) zHch2(^|#KenVO0eX5R1;TbaANg`sVed+uS?8sU&`+bnO959X}p8{DK>?kIk6-<IJV z|DOFqlkP2n;#04G|2OyEk~;A^v&4D6+?P*2{+)j35MS=c>OVURXRf)R*8kse=kJ>f ze7|<>+4J96^u~hN{~wANjBn1*wcYbaXoKYM&Hs%)EGTD=IQ>iJ!NdHP3Y!JOkNPT0 z|NsBE(N^8(Qx-$>jk(G~pZDzi&%gM_+>7#TyeY+eo)&M9{!RbPe86ztpVgae#Ew{s z+Wy!xq5h-T0ms98#2NfM%D+5zIB#p3W9eOdWv<+A_a`;XWzW{6-s4ITk}i%|t8epF z%wSrt<X@`^`Hgd)-+nUpul?~|P74C}g@3D8nU~vo$YXsB`xoId(OL0K*?R;T_}lrX zO51+y{E#<~N5;t3_5=IVeK}|5IUQ#XXX8opRuq}~@Y`Sg6i>y_$I0dI>$7>Rxz?IJ zzq9B6VjD4wC%<<5XnpXr?2Y=DV>`?KA3d3)E+e+fwBg|!HJv|td(9H0R&!3CVIKRF zZNh4Y!^<9-eSd4;o9@)Unc;e@%m$@dYg>Y!`jo%@FZxYBZR)+U%m2iSr6X<}T~~1a z-}6tu?U@$MD?Tmi5W_4dX;)rj8FN7VfZ*ru&lB(0$CZB*u=Et%@1ZkW{h;fW@ArD& z{JSiC!?EQ)&zY_#r}r<f_$go6tW?%{qEpW2KJP5o7uOH$lAgKO`iPBSqkC7p#NqAr z?=F6H*ztY;Ud`b0$)AF`4*fb`FC+hNx9{rmL)-sv<(;|xw~OtM%8G0AFHcY09XwTX zy3_uD#r>bt6Yi`&`ANXucTIs~?y@&oxlTU?8Xu+l&e-r;buHgT|HebAd%k{dlh@%r znBDlNb-PjI`4#qeAC|LNCJWD8wfx93i}Lpl7hk35{rb9K+2uZqqLas4cAdGj_|M#D z^=bLPf6w33Ygo0rQU2dg=DYL%+?LBe%AmR<^Mv=b^|EK;?(El^EWG66gUa0^_hp`? zoqG{Ixsy}ma@K?3m({0tO!;%>Tx$RQa&7S{j&SRj`z8iI(~aM5_`2CS{^s<l6aRgh mbG+?aR&?)8Qk(G#EB-URaR|%M|0pB}((CE!=d#Wzp$P!9)&%<i diff --git a/assets/translations/en.json b/assets/translations/en.json index 867a994..7ff9b8c 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 c481788..e2aafac 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 GIT binary patch literal 3771 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 literal 0 HcmV?d00001 diff --git a/assets/ui/button_delete_saved_game.png b/assets/ui/button_delete_saved_game.png new file mode 100644 index 0000000000000000000000000000000000000000..5e4f217689b11e444b7163557d7e5d68f3bbfe7d GIT binary patch literal 5813 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 literal 0 HcmV?d00001 diff --git a/assets/ui/button_resume_game.png b/assets/ui/button_resume_game.png new file mode 100644 index 0000000000000000000000000000000000000000..b2ea0a02d05e42377eb551a4b51428b511a32f5d GIT binary patch literal 3659 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 literal 0 HcmV?d00001 diff --git a/assets/ui/button_start.png b/assets/ui/button_start.png new file mode 100644 index 0000000000000000000000000000000000000000..6845e2f5c21598ab61f1684d2075aeec0334bf23 GIT binary patch literal 3999 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 literal 0 HcmV?d00001 diff --git a/assets/ui/game_end.png b/assets/ui/game_end.png new file mode 100644 index 0000000000000000000000000000000000000000..876334279c1711b349a62131a33607eecf924eb6 GIT binary patch literal 7937 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 P3=9mOu6{1-oD!M<Ur^e& literal 0 HcmV?d00001 diff --git a/assets/ui/move-blank.png b/assets/ui/move-blank.png new file mode 100644 index 0000000000000000000000000000000000000000..3a504ec4e9ab530b04d5324bcfb4cf187a3c1b86 GIT binary patch literal 170 zcmeAS@N?(olHy`uVBq!ia0y~yU^oE6jLZxS3>iZITo@P_I14-?iy0VLJ3*K+H)HNE z1_lPn64!{5;QX|b^2DN4hVt@qz0ADq;^f4FRK5J7^x5xhq!<_&m;-!5T>t<7zx;^w z2?hoR#*!evU<QY0H_{jw7^FR2977}|-ySq%WME)8r0{!r94~(Y0|Ns?fsW7t2F7;w Qi|;@@Pgg&ebxsLQ0Nd>^`~Uy| literal 0 HcmV?d00001 diff --git a/assets/ui/move-left-foot.png b/assets/ui/move-left-foot.png new file mode 100644 index 0000000000000000000000000000000000000000..1d4a90c1b42d4da8a617c0b10cad5c5dc9329b32 GIT binary patch literal 2830 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 literal 0 HcmV?d00001 diff --git a/assets/ui/move-left-hand.png b/assets/ui/move-left-hand.png new file mode 100644 index 0000000000000000000000000000000000000000..d8e288cc61afef47b8f6342fdf109098d321dde8 GIT binary patch literal 1937 zcmeAS@N?(olHy`uVBq!ia0y~yU^oE69Lx+145>_WOc@v$I14-?iy0VLJ3*K+H)HNE z1_lPn64!{5;QX|b^2DN4hVt@qz0ADq;^f4FRK5J7^x5xhq!<{O90GhoT>t<7&oBx` zLx9{6nCIWq!N9<<za+>nn1PXrnT3^&or9B$hnJ6EKu}0nL{v;lT1Hk*UO`b=MO95* zLsLszPv5}M$k@cx%-q7#+Q!z-!O_Xt#og1(+s8K`Feo@AG%P$KGCC$UE-@)3H9aFM zCpRy@ps=W<w6dzUuD+qMxwXBcv%9Caf5N26Q>IOyIcxTu`3sk=SiNrjhK-vxZ{4<Y z_r61ij~qLG^3>@wXV0C#aOv`uYu9hyx_#%~{YQ_VJbm{3#miT(-@JYI{=>&lpTB(l z_Wj4tU%&tS{rA-5S`q^T3%jR_V@SoVw|CNmgB?ZK9teA71hR-~iv{Sg1d41-a8xbZ zsKg}p#^tCO_tB<~uDy#JWJRT21G^M|W=pwyh+Uhp>E7MPbH3lcYdpDfzGqSK_cS2} zu2!do0U9DO%C+EE>N}I?K2Fi``=4*VJ8}0(i{H}gZr2~5)w*&2x=Z(jU(2*KOf8#! zh4mrxog&s7moI+pf0xAI`hvl2+5W#eUE6bm!oD49+|M5rc|eurZ@5jV$y*EKFV`>n z<TNxqdbiHkIFO0Y^7OVIXAX}t+m|6b8C6o&%}osuC}>-0{m`EwxcBjbuPIxy{xZ*= zxy$-T!-30p+j%>?7?W=7Qvca-_}pWD<*Q6RsjsIMGOje6-paL=p>Q69o`Lq_i;PbD z47Dp?Fzk&>bAOUwvHYy8{2Nw<iLsg!W9<Lsv_DP>GC7dk5U6=z)jRPouT!Qp@MQZ> zJXXlTIPrbVteC40nRAj?`5b58aV*5Lga5*j5Y6N48;nBNJz_2qY+ztfU|{5MU|<r! z&U|3cz;a~vG(9Ob)&hBjPTg?E4F~Q?&wR!wu=W4d^;gwrAKuD-qo_ChMv-jf<#!vj zW2KkJ6f;LW`txj$o@a4Gej#g4QSE_kCM6A#Zys;$ab)YLxS3tTD!E|8c^y{Kgplg_ zH#tKZKJDAgK7*mya`XBQ2FI%>7%lhQl(uPD(ZA=Wr40jj`8Gygg@@MpU+j2z7j#*l za{ug^^T;o&c_V{Ue1ZDs3o*U#p73d1e8b+h<<m9m#}^n*?RY)wPfiGj!qH>4t!_z- zQ|>*}%V*JPaC`Uau!9Lh-6aNdht>Ce|7$x$>l7WZJHWf5-+?cJc?Y9;=?=ziD;uN( zxOOncCrx1HVlaPDsZr#h_JA#fF|VOisptUb0p1MB7MXwrh4n84YDA^hnJx&_N!WYs z@4~o@|I@3^GH!KZygBQ8`)Q@$<}I208y48H{ykIH`1dxuH^UO;yT`vQ*?+))(@G<T z0Npk9y;J@b_(&wQ%N)o_+^(eZH!|V^`y6S>bU#j}HPcvU=q#%*-l4;AM}WyfZ>|0B z`3Lqb3x4$fFRPsxg9ck?q{;(M2~}Z+>wnp1EdAF0!>qnMNwA1<!)<?uwkQ7<?0+6! z&+T!Zqh3!#uJt7Qftvf@S?51;XifS3XX-be0Oji1J<_>Mp+QeBtKTRJP~U0xKHo)$ zZR)0Tx1;ywr#D1-R=o@Tzx%M`R+soMkKR@*u!#7yE&slH{?evdotsLk0{%a~CJ<n@ zQT5vX+y-uC$CpRM%yv|N%rR{EbaD2nSv%g|mzlecS%!1o=H-+2daSjdZ*Iv@psJP{ n`A={2&6o*$?_^+P(3(1il76*Yi*)5KfJ#PBS3j3^P6<r_RYX*1 literal 0 HcmV?d00001 diff --git a/assets/ui/move-right-foot.png b/assets/ui/move-right-foot.png new file mode 100644 index 0000000000000000000000000000000000000000..f9fa2905b064ca539d6785851d89ae3e55428d4d GIT binary patch literal 2846 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 literal 0 HcmV?d00001 diff --git a/assets/ui/move-right-hand.png b/assets/ui/move-right-hand.png new file mode 100644 index 0000000000000000000000000000000000000000..297d74dff520bba8e4ab33b893a1fe4f49fcf197 GIT binary patch literal 1924 zcmeAS@N?(olHy`uVBq!ia0y~yU^oE69Lx+145>_WOc@v$I14-?iy0VLJ3*K+H)HNE z1_lPn64!{5;QX|b^2DN4hVt@qz0ADq;^f4FRK5J7^x5xhq!<{OOagpDT>t<7&oBx` zLx7YJ@JjELWME*}T@vIM%)rRR%)-jX&cVsW&BM#bFCZu+A}S^>B`qT>C$FHWq^zQ< zrmmr-t*38bXk=_+YG!U>X=QC=YiIB1<m}?=;pOcU5Ev938WtWA6&Ih7n3A56nU#}U zP*_x4Rb5kCSKrXm+R@q7-P_kcdFqUrv*s>XykzOJ<ttXMS-WoihK-vxZ`rzS=idDX z4jwvu^!SNWXU|=@c<IX3Yu9hwx_$TlgNKhEKY9A>`HPpYUcY(!?)}G4pTB(l_Wj4t zU%&tS{g=0J&lCm*7A{X0$B>F!Z}0rHo)Rc>;A69!(9|s%mzA^@2J~h)Z(HQLX-bgd zP4C}vUY<)ct}dFSntd|je*HT4i8~*w85i&UKJU5Z{rmTRpSGO;-0-kY97ES44FnRp z_v^}?#~a^eT<5P_$9sKd>VcjA_c7)!anF<Ze*a^6z3=Ykg8IYOXV+BJG|YYWJG=iQ zBbUU112QEIt9cwgyR+{&&a|yZrrK?(3Y(<@i$uM=-mW{=HG$&&A_CV8t0EtD2yR%G z{l+T%=-j)9zaL*Wli|aKH&q4!JHOg`*Yo6m(|Q)qQ2*?~qO9NSX8r5J-ke}hF=Bt) zWy>{fd*9o#Hhzs%f3X#;JF>Uk3eXTQ=(LSodgY(pnLKkf<`02f6Ar$)_O^#F;Dp|G zA=w8qyJH$e8Jh2Z6DYsR&^Vc`;t9Wf?aXy++_-MsF!;6MX0y<P>vrEJsS7C{Sa|Bq zu1qsK2Z`bdjr<eN%Y-~)zLUmx>jUGe8O?QX8W@=b8W>m<7#KMm7;rE@M9av^@4h9d zeZX|<yYCy$FSk)+YKhca{x)fU(3j`GMND@wn_cOx)swYlTwtV8Il*=%pWmxw|99-m zva6Vte4c&g`y-tY9L)dWWfFUyD_g;e4KpI%w{dT%ymKNarr}Yca`m*uOjr8mSIUY$ zP)~nh=F=c|?m{@5OM~0JD~CH6wpRq-w*AO3X=ifx2ZosF9m&@p{R~!Ww%*Rff8fJ3 zYYR4B2Bn?NuVXJV=)|Y>PU&QSaQpb|J9}Ap4?K#uakuVgH+U|c|7C(8F9Tm;y1lkD zbHS>oeEIIo4F$QYF1_wun|*`vp6v<73B9rpt~JOoc-~;-^PkA9kiucfaB2hdo_)&f z2MpgZOy@trD9|p;z`ud{8m|Jo183QRKYmXb9{Mlc#hg|r%5CtHx1l+SaqTW<9{E<L zSEcL@s{#&q|7Fy3l4pJTi}{G1pr{wa8DmbCCEg4NDjOT#8Zpdf`6oBIxP6BCA%^oy z-sBfP*RR%^z#O6VhPTVWlHu9A@^4q(sBliu;0Vykte<gFkxk+Kwbr9f-_}psX20fz zIAh0Tc7acVueoQI$9;LZSzh5gTXbdFd;PD^{211K+h=6_^CatmJZr|83f8~xJy|ua zzPx6-l=Zj#&ul+`-uf5Yd~ogpC%4)5|NPiZy(G5e{?`duy4UQlXQ9*;%~QYTFIqTx zLC`TZ?eFjJvS&U0dSumK`_`tcolhb^9=v>Z=Yj;q_3At8f|XX>{GyyY`R7ahPYkJ_ zPyD^VJpFI_^u|kPy{hdG{_4Hy?f6rsx3%}r`}DYuQZ^Y&)+KlU9MZWTRHu|A_287P shT_u<KR>^)s+t#03y?DEA`QpCOnL8Tb_A8Zu>%!`p00i_>zopr0B&VuIsgCw literal 0 HcmV?d00001 diff --git a/assets/ui/placeholder.png b/assets/ui/placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..814df31be6ddc4275ebe4490c79365578dbef1f0 GIT binary patch literal 170 zcmeAS@N?(olHy`uVBq!ia0y~yU^oE6jLZxS3>iZITo@P_I14-?iy0WCPk}I_+{y{! z3=9mCC9V-A!TD(=<%vb94CUqJdYO6I#mR{Use1WE>9gP2NHH)lFbDXAxc>kDfB6yV z6ATOtj3q&S!3+-1Zlp0VFi3m4IEF|_zCCEj$iTpGNa6SLI9~n)1_lO(0v(|P42<pU Q7vF(+p00i_>zopr0K3yKtN;K2 literal 0 HcmV?d00001 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 0000000..d4afd51 --- /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 0000000..6a9871a --- /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 4d7f1be..0000000 --- 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 0000000..6b0ae12 --- /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 988ca61..0000000 --- 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 0000000..d2d7d49 --- /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 0000000..f090d53 --- /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 c327148..0000000 --- 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 0000000..1af3bf3 --- /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 be39034..74f532f 100644 --- a/lib/config/theme.dart +++ b/lib/config/theme.dart @@ -39,11 +39,9 @@ final ColorScheme lightColorScheme = ColorScheme.light( secondary: primarySwatch.shade500, onSecondary: Colors.white, error: errorColor, - background: textSwatch.shade200, - onBackground: textSwatch.shade500, onSurface: textSwatch.shade500, surface: textSwatch.shade50, - surfaceVariant: Colors.white, + surfaceContainerHighest: Colors.white, shadow: textSwatch.shade900.withOpacity(.1), ); @@ -52,11 +50,9 @@ final ColorScheme darkColorScheme = ColorScheme.dark( secondary: primarySwatch.shade500, onSecondary: Colors.white, error: errorColor, - background: const Color(0xFF171724), - onBackground: textSwatch.shade400, onSurface: textSwatch.shade300, surface: const Color(0xFF262630), - surfaceVariant: const Color(0xFF282832), + surfaceContainerHighest: const Color(0xFF282832), shadow: textSwatch.shade900.withOpacity(.2), ); @@ -192,5 +188,3 @@ final ThemeData darkTheme = lightTheme.copyWith( ), ), ); - -final ThemeData appTheme = darkTheme; diff --git a/lib/cubit/bottom_nav_cubit.dart b/lib/cubit/bottom_nav_cubit.dart deleted file mode 100644 index c633c65..0000000 --- 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 325c872..f0eed2c 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 204801e..00e2116 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 0000000..c998a44 --- /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 200abf9..0000000 --- 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 0000000..e9a16c0 --- /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 0000000..5acd85b --- /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 0000000..1742cfb --- /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 0000000..ebcddd7 --- /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 6048256..0000000 --- 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 78da985..d77b529 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 0000000..3ba1cd2 --- /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 0d47b0f..a026fbc 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 0000000..36331ff --- /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 0000000..8805c92 --- /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 0000000..ea2b71b --- /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 9354124..b98107b 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 0000000..dedfb1b --- /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 0000000..0c6cf47 --- /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 0000000..7ccf9b8 --- /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 0000000..fc4b576 --- /dev/null +++ b/lib/ui/parameters/parameter_image.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +class ParameterImage extends StatelessWidget { + const ParameterImage({ + super.key, + required this.code, + required this.value, + required this.isSelected, + }); + + final String code; + final String value; + final bool isSelected; + + static const Color buttonBackgroundColor = Colors.white; + static const Color buttonBorderColorActive = Colors.blue; + static const Color buttonBorderColorInactive = Colors.white; + static const double buttonBorderWidth = 8.0; + static const double buttonBorderRadius = 8.0; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: buttonBackgroundColor, + borderRadius: BorderRadius.circular(buttonBorderRadius), + border: Border.all( + color: isSelected ? buttonBorderColorActive : buttonBorderColorInactive, + width: buttonBorderWidth, + ), + ), + child: Image( + image: AssetImage('assets/ui/${code}_$value.png'), + fit: BoxFit.fill, + ), + ); + } +} diff --git a/lib/ui/parameters/parameter_painter.dart b/lib/ui/parameters/parameter_painter.dart new file mode 100644 index 0000000..774c21d --- /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 f9adc5a..0000000 --- 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 0000000..65c1e5d --- /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 0000000..6ad44e4 --- /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 0000000..aa5bcc7 --- /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 b0ecbd5..0000000 --- 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 0000000..e294590 --- /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 0000000..15f343d --- /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 ecc81e1..3238f96 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 0000000..4036c28 --- /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 0000000..58cde3e --- /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 0000000..fd246a8 --- /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 0000000..45dc8ff --- /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 b777c21..0000000 --- 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 24e9a83..0000000 --- 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 fadc925..0000000 --- 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 0000000..0fcdef2 --- /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 0000000..ae0c7c4 --- /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 0000000..67eb3e7 --- /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 c40dd3a..48846a9 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 0000000..6b6c07e --- /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 81fac9e..0000000 --- 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 bd935ab..0000000 --- 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 fe15305..fc3ef49 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 4efd07a..16ca4fa 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 27dbe26..6d67b8f 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 0000000..68bc1d0 --- /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 660e16f..a5fe84c 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 0000000..93344c8 --- /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 0000000..2622a57 --- /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 0000000..ac7eefe --- /dev/null +++ b/resources/ui/images/button_delete_saved_game.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect x=".44662" y=".89101" width="92.772" height="91.894" ry="11.689" fill="#ee7d49" stroke="#fff" stroke-width=".238"/><path d="m61.07 35.601-1.7399 27.837c-0.13442 2.1535-1.9205 3.8312-4.0781 3.8312h-16.84c-2.1576 0-3.9437-1.6777-4.0781-3.8312l-1.7399-27.837h-2.6176c-0.84621 0-1.5323-0.68613-1.5323-1.5323 0-0.84655 0.68613-1.5323 1.5323-1.5323h33.711c0.84621 0 1.5323 0.68578 1.5323 1.5323 0 0.84621-0.68613 1.5323-1.5323 1.5323zm-3.2617 0h-21.953l1.4715 26.674c0.05985 1.0829 0.95531 1.9305 2.0403 1.9305h14.929c1.085 0 1.9804-0.84757 2.0403-1.9305zm-10.977 3.0647c0.78977 0 1.4301 0.6403 1.4301 1.4301v19.614c0 0.78977-0.6403 1.4301-1.4301 1.4301s-1.4301-0.6403-1.4301-1.4301v-19.614c0-0.78977 0.6403-1.4301 1.4301-1.4301zm-6.1293 0c0.80004 0 1.4588 0.62935 1.495 1.4286l0.89647 19.719c0.03182 0.70016-0.50998 1.2933-1.2101 1.3255-0.01915 7.02e-4 -0.03831 1e-3 -0.05781 1e-3 -0.74462 0-1.3596-0.58215-1.4003-1.3261l-1.0757-19.719c-0.0407-0.74701 0.53188-1.3852 1.2786-1.4259 0.02462-0.0014 0.04926-2e-3 0.07388-2e-3zm12.259 0c0.74804 0 1.3541 0.60609 1.3541 1.3541 0 0.02462-3.28e-4 0.04926-0.0017 0.07388l-1.0703 19.618c-0.04379 0.80106-0.70597 1.4281-1.5081 1.4281-0.74804 0-1.3541-0.60609-1.3541-1.3541 0-0.02462 3.49e-4 -0.04925 0.0017-0.07388l1.0703-19.618c0.04379-0.80106 0.70597-1.4281 1.5081-1.4281zm-10.216-12.259h8.1728c2.2567 0 4.086 1.8293 4.086 4.086v2.0433h-16.344v-2.0433c0-2.2567 1.8293-4.086 4.086-4.086zm0.20453 3.0647c-0.67725 0-1.2259 0.54863-1.2259 1.2259v1.8388h10.215v-1.8388c0-0.67725-0.54863-1.2259-1.2259-1.2259z" fill="#fff" fill-rule="evenodd" stroke="#bd4812" stroke-width=".75383"/></svg> diff --git a/resources/ui/images/button_resume_game.svg b/resources/ui/images/button_resume_game.svg new file mode 100644 index 0000000..6ad8b64 --- /dev/null +++ b/resources/ui/images/button_resume_game.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect x=".44662" y=".89101" width="92.772" height="91.894" ry="11.689" fill="#49a1ee" stroke="#fff" stroke-width=".238"/><path d="m39.211 31.236c-0.84086-0.84489-2.9911-0.84489-2.9911 0v34.329c0 0.84594 2.1554 0.84594 2.9993 0l28.178-15.637c0.84392-0.84086 0.85812-2.2091 0.01623-3.053z" fill="#fefeff" stroke="#105ca1" stroke-linecap="round" stroke-linejoin="round" stroke-width="6.1726"/><path d="m40.355 33.714c-0.71948-0.72294-2.5594-0.72294-2.5594 0v29.373c0 0.72383 1.8442 0.72383 2.5663 0l24.11-13.38c0.7221-0.71948 0.73426-1.8902 0.01389-2.6124z" fill="#fefeff" stroke="#feffff" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.225"/><path d="m28.369 66.919v-37.591" fill="#105ca2" stroke="#105ca2" stroke-linecap="round" stroke-width="4.0337"/></svg> diff --git a/resources/ui/images/button_start.svg b/resources/ui/images/button_start.svg new file mode 100644 index 0000000..e9d49d2 --- /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 0000000..fe20923 --- /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 0000000..23ace81 --- /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"/> -- GitLab