From ac1c6be8f3968c369f99809bd36f9a9a0120425d Mon Sep 17 00:00:00 2001 From: Bossanyi Tibor Date: Mon, 17 Aug 2020 12:38:47 +0200 Subject: [PATCH] wt1.1a flutter_bloc --- android/app/build.gradle | 20 +- android/app/src/main/AndroidManifest.xml | 18 + android/app/src/main/res/values/strings.xml | 13 + .../gradle/wrapper/gradle-wrapper.properties | 2 +- android/key.properties | 4 + android/settings_aar.gradle | 1 + asset/icon/WT_long_logo_1024x500.png | Bin 0 -> 20664 bytes asset/icon/icon_512.png | Bin 0 -> 78342 bytes asset/image/login_fb.png | Bin 0 -> 6975 bytes i18n/en.json | 14 +- i18n/hu.json | 16 +- lib/bloc/account/account_bloc.dart | 46 ++ lib/bloc/account/account_event.dart | 46 ++ lib/bloc/account/account_state.dart | 36 ++ lib/bloc/custom_exercise_form_bloc.dart | 127 +++++ .../customer_change/customer_change_bloc.dart | 38 ++ .../customer_change_event.dart | 36 ++ .../customer_change_state.dart | 32 ++ lib/bloc/customer_change_form_bloc.dart | 101 ++++ lib/bloc/exercise_form_bloc.dart | 64 +++ lib/bloc/login_form_bloc.dart | 61 +++ lib/bloc/menu/menu_bloc.dart | 51 ++ lib/bloc/menu/menu_event.dart | 41 ++ lib/bloc/menu/menu_state.dart | 34 ++ lib/bloc/registration_form_bloc.dart | 61 +++ lib/bloc/session/session_bloc.dart | 35 ++ lib/bloc/session/session_event.dart | 16 + lib/bloc/session/session_state.dart | 32 ++ lib/bloc/settings/settings_bloc.dart | 55 ++ lib/bloc/settings/settings_event.dart | 13 + lib/bloc/settings/settings_state.dart | 46 ++ lib/library_keys.dart | 9 + lib/localization/app_localization.dart | 26 +- lib/main.dart | 53 +- lib/model/auth.dart | 41 +- lib/model/exercise_tree.dart | 20 + lib/model/exercise_type.dart | 13 +- lib/model/workout_tree.dart | 5 +- lib/repository/customer_repository.dart | 143 ++++++ lib/repository/exercise_repository.dart | 88 ++++ lib/repository/menu_tree_repository.dart | 73 +++ lib/repository/user_repository.dart | 32 ++ lib/service/customer_service.dart | 28 +- lib/service/exercise_tree_service.dart | 19 + lib/service/exercisetype_service.dart | 4 +- lib/util/common.dart | 28 ++ lib/util/loading_screen.dart | 61 --- lib/util/loading_screen_state.dart | 129 ----- lib/util/menu_tests.dart | 93 ---- lib/util/message_state.dart | 32 -- lib/util/session.dart | 30 +- lib/view/account.dart | 244 ++++----- lib/view/custom_exercise_page.dart | 346 +++++++++++++ lib/view/customer_bodytype_page.dart | 294 +++++------ lib/view/customer_fitness_page.dart | 435 ++++++++-------- lib/view/customer_goal_page.dart | 272 +++++----- lib/view/customer_list_page.dart | 93 ---- lib/view/customer_modify_page.dart | 470 +++++++++--------- lib/view/exercise_new_page.dart | 332 ++++++++----- lib/view/exercise_type_list_page.dart | 59 --- lib/view/exercise_type_modify_page.dart | 67 --- lib/view/exercise_type_new_page.dart | 64 --- lib/view/login.dart | 323 ++++++------ lib/view/menu_page.dart | 238 ++++----- lib/view/registration.dart | 322 +++++++----- lib/view/settings.dart | 91 ++-- .../customer_changing_view_model.dart | 37 -- lib/viewmodel/customer_view_model.dart | 88 ---- .../exercise_changing_view_model.dart | 56 --- .../exercise_type_changing_view_model.dart | 36 -- lib/viewmodel/exercise_type_view_model.dart | 36 -- lib/viewmodel/exercise_view_model.dart | 31 -- lib/viewmodel/user_changing_view_model.dart | 22 - lib/viewmodel/user_view_model.dart | 23 - lib/widgets/bottom_nav.dart | 42 +- lib/widgets/customer_list_widget.dart | 67 --- lib/widgets/datetime_picker.dart | 65 --- lib/widgets/exercise_type_list_widget.dart | 75 --- lib/widgets/home.dart | 92 ++-- lib/widgets/loading.dart | 92 ++-- lib/widgets/menu_page_widget.dart | 136 +++++ lib/widgets/nav_drawer.dart | 91 ---- lib/widgets/splash.dart | 32 ++ pubspec.lock | 227 +++++++-- pubspec.yaml | 17 +- test/account_bloc_test.dart | 79 +++ test/widget_test.dart | 30 -- test/widget_test.login.dart | 107 ++++ test_driver/app.dart | 11 + test_driver/app_test.dart | 9 + 90 files changed, 4282 insertions(+), 2855 deletions(-) create mode 100644 android/app/src/main/res/values/strings.xml create mode 100644 android/key.properties create mode 100644 android/settings_aar.gradle create mode 100644 asset/icon/WT_long_logo_1024x500.png create mode 100644 asset/icon/icon_512.png create mode 100644 asset/image/login_fb.png create mode 100644 lib/bloc/account/account_bloc.dart create mode 100644 lib/bloc/account/account_event.dart create mode 100644 lib/bloc/account/account_state.dart create mode 100644 lib/bloc/custom_exercise_form_bloc.dart create mode 100644 lib/bloc/customer_change/customer_change_bloc.dart create mode 100644 lib/bloc/customer_change/customer_change_event.dart create mode 100644 lib/bloc/customer_change/customer_change_state.dart create mode 100644 lib/bloc/customer_change_form_bloc.dart create mode 100644 lib/bloc/exercise_form_bloc.dart create mode 100644 lib/bloc/login_form_bloc.dart create mode 100644 lib/bloc/menu/menu_bloc.dart create mode 100644 lib/bloc/menu/menu_event.dart create mode 100644 lib/bloc/menu/menu_state.dart create mode 100644 lib/bloc/registration_form_bloc.dart create mode 100644 lib/bloc/session/session_bloc.dart create mode 100644 lib/bloc/session/session_event.dart create mode 100644 lib/bloc/session/session_state.dart create mode 100644 lib/bloc/settings/settings_bloc.dart create mode 100644 lib/bloc/settings/settings_event.dart create mode 100644 lib/bloc/settings/settings_state.dart create mode 100644 lib/library_keys.dart create mode 100644 lib/model/exercise_tree.dart create mode 100644 lib/repository/customer_repository.dart create mode 100644 lib/repository/exercise_repository.dart create mode 100644 lib/repository/menu_tree_repository.dart create mode 100644 lib/repository/user_repository.dart create mode 100644 lib/service/exercise_tree_service.dart delete mode 100644 lib/util/loading_screen.dart delete mode 100644 lib/util/loading_screen_state.dart delete mode 100644 lib/util/menu_tests.dart delete mode 100644 lib/util/message_state.dart create mode 100644 lib/view/custom_exercise_page.dart delete mode 100644 lib/view/customer_list_page.dart delete mode 100644 lib/view/exercise_type_list_page.dart delete mode 100644 lib/view/exercise_type_modify_page.dart delete mode 100644 lib/view/exercise_type_new_page.dart delete mode 100644 lib/viewmodel/customer_changing_view_model.dart delete mode 100644 lib/viewmodel/customer_view_model.dart delete mode 100644 lib/viewmodel/exercise_changing_view_model.dart delete mode 100644 lib/viewmodel/exercise_type_changing_view_model.dart delete mode 100644 lib/viewmodel/exercise_type_view_model.dart delete mode 100644 lib/viewmodel/exercise_view_model.dart delete mode 100644 lib/viewmodel/user_changing_view_model.dart delete mode 100644 lib/viewmodel/user_view_model.dart delete mode 100644 lib/widgets/customer_list_widget.dart delete mode 100644 lib/widgets/datetime_picker.dart delete mode 100644 lib/widgets/exercise_type_list_widget.dart create mode 100644 lib/widgets/menu_page_widget.dart delete mode 100644 lib/widgets/nav_drawer.dart create mode 100644 lib/widgets/splash.dart create mode 100644 test/account_bloc_test.dart delete mode 100644 test/widget_test.dart create mode 100644 test/widget_test.login.dart create mode 100644 test_driver/app.dart create mode 100644 test_driver/app_test.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index 7fb27d7..65cff7c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -8,7 +8,7 @@ if (localPropertiesFile.exists()) { def flutterRoot = localProperties.getProperty('flutter.sdk') if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") + throw new Exception("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") } def flutterVersionCode = localProperties.getProperty('flutter.versionCode') @@ -26,6 +26,12 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" apply plugin: 'com.google.gms.google-services' +def keystoreProperties = new Properties() +def keystorePropertiesFile = rootProject.file('key.properties') +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) +} + android { compileSdkVersion 28 @@ -46,11 +52,20 @@ android { versionName flutterVersionName } + signingConfigs { + release { + keyAlias keystoreProperties['keyAlias'] + keyPassword keystoreProperties['keyPassword'] + storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null + storePassword keystoreProperties['storePassword'] + } + } + buildTypes { release { // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug + signingConfig signingConfigs.release } } } @@ -62,4 +77,5 @@ flutter { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'com.google.firebase:firebase-analytics:17.2.2' + implementation 'com.facebook.android:facebook-login:[5,6)' } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 55aec36..7f52f63 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -48,5 +48,23 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..432c9c7 --- /dev/null +++ b/android/app/src/main/res/values/strings.xml @@ -0,0 +1,13 @@ + + + WorkoutTest + + + 584181112271127 + + + fb584181112271127 + \ No newline at end of file diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 296b146..bc24dcf 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip diff --git a/android/key.properties b/android/key.properties new file mode 100644 index 0000000..39725b5 --- /dev/null +++ b/android/key.properties @@ -0,0 +1,4 @@ +storePassword=tbi6012Andi +keyPassword=tbi6012Andi +keyAlias=key +storeFile=c:/Users/bossa/.ssh/key.jks \ No newline at end of file diff --git a/android/settings_aar.gradle b/android/settings_aar.gradle new file mode 100644 index 0000000..e7b4def --- /dev/null +++ b/android/settings_aar.gradle @@ -0,0 +1 @@ +include ':app' diff --git a/asset/icon/WT_long_logo_1024x500.png b/asset/icon/WT_long_logo_1024x500.png new file mode 100644 index 0000000000000000000000000000000000000000..732c026fe18665fde39b975cfc9227d11e72769e GIT binary patch literal 20664 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 AeE1d%{M=`~0P zDJmo=2%(oy0-+~_8xH3k@B4k<`2O5G#{IX)-fKN`KJ!^?J$tMfb8Yo@bytesf@|1OR+=dRpoae1ENsR4@YSZ0+UiElY>zA@tzWe<^JH_yA}J zc>v&%0B(1K05k#w(_jGrItXya0{|poKw1JAK!CvLP<{Y_@BkvS002V+62kNV0H#AQ z{`aOLa+?$iK-5S)^OM`!5ap{^1hT0ZVJ8@3Cm3%3U@)CvSe?+r{X>uYgkJ4G^!}Su zCA`K$oNF_c$*#6}vH8B}@y^h^H7lrZ@!i?pR43lrWMOIIJLcm4>Tzh5 zjLw`4@{LF)aPqk-*eFCD%q}pE=U6BA|NF~rJ!H0Jm4DjJZE4LKvAgd;NqXE#Yq!< zqNt>4ZkF!(>>Kw@iN#7APNcl|Aa6l1;GzAqOmlP8px*{;Kj+_(@z%xn+G6`K+Zo)? z!!Z-ri6+*w;*;k0tAc-EEUuP14ZR);*W-d6!IVV3y)X>$Flh&;(2z1V+|qmu>e=P9OB9Nl-lGs@e8=q*+|x+TUoo~wfu-}e z5l@)i_P%0sYb|A!>JRJ8wX=!ZQ8%fTcxT>`nYZdY0kyxun8SDmxAeUn@+^4r zw^DguGVE5%sRMghO)onf3 zNrcs6%SGkgP@oMRU2xJn)v{YR<Bs?Wvd8cQ-58#>Cgj7v|R8!j|LJX(QtAKlA$?K3!#~w zb2RZD^8yrfDaT}Nr+qG|+R*h92|p*yD(VzmrjwKA7LO9hcfiih3>qrrX8xmso)aR3yMlo*4zej;Gu<7% z_-w*YLNq5Zf5gAXY0t@HV!9-vCUIhNcqt$#sS!|&2LVUrK4M=@Ow9O?7!GL;l>#jl zvkjkADj1hmaa^f#GCE~>z*Chc7Z<9lac-UdqPjar;$})J2gB*7Q4HEhI?H=kF?2}- zYn0r&%%;Hs6;f6pM*gyF4SbM%c{k@X<`;YwyFPJI*74#9K;DKLG|-X zf`Re3nwZzlxs$b51#M;Z2T&qF8h=QKeFanxg42fJ6ouBfyF>=G*G*qPJ8GoV`NH}Q zJ>noRuGby*Kq?b%E-)G2Jfhg%(HG49+0!#`(R(`yKdl?jlHiGC1Vp-E73N4C4zMpf zcuyAY6SY)=^*CJd@%A%I4or+6DNq9b9D3qU(iQ1Gyoq>to$Iw2 zotqJ0gR!o&pl6vOw)|{4B+tb>9drL{+?54{p5>tq8M$Q-h}#3MZvd8k1`g*MXVu$l zsi3d+73R++5ll1$pHy#?GK?G07V&@1;ikh4e1AR|Ew(YtG}a+x3vDd*D!nk>-QKWQ z?a^KHb_#6Uerxz+6?nQU{`niC%qbwrq1^ns>4hke)mcQQf`RcmLB)NgombNDu|DGw zO!c59#I}DRI)=HL@KW4f&TDyxL!q#tJ% zf33cAAp5db>6E0rAiP<^wn`_?MEoN*g9jZ3s?C>b3S4_tyMYoXu$VF)?K7w`?_c0A z4~yT@hSU%SNgF?>-!xtB@#8xd64JpkJ+)EOy}_l?J?30u!+)s@Da&b{PYd%1(yQP3 z$$j3T1R?+O#ug^F9KMyDhCJ_zpF(5>Hiu8HQ&v$*~9)aJ1;PQaR zf+ndz!c^slPyJuVIew#>)dws^X(Z&sF-kzv%#F~shu?fiFrMH;Ymdek4?3YBC0dxR z5+8b&JRS{7Czdw*hY`84OHqrkp)rWw}owzOw^Bz+$K`=L) z8RpT#coTFxu4?q6-H2Ni_Jjs`=wvFkD(9z^;Xf3L2kgLThoYVhbEcw4!8{mXqVY1m zaYnMvwZ@*hJ1m?t>V2!GoKVLlpat!XvAW#2V*=(0wmsEG)l_?v0QM>@jxUDwFDVCcmK7X~Vd#GfGF z#IyLHcY@#K#y7JaeccLvrtO|e36gl-Lf)TOlssZpZO*xhvd}ylAYoOScuO`e$8T%J z$BM9~I?l2^ASQ`77LLDZKvl~T#=R@Dw$k?D?Vfi%4@QB`s`)a_Z#>-ne^q}Vo z9l;(T%teF9@$lRzlG*kfq74M-6vnwy_G-C@5`5$k+b>I4TDL}*z6gJ0skq2@Mv$kZ z(tIz2a$XawLs$NUlE4I1p94}3E$V0&7IIyWve*vdT2G4)OK>?7HH$%Yz}xq0Kh6R( z9nlZZ><8Eb+#hZOL2d?)zhrzLd>!bIx(1~@n%d}LnvT2VGTX>}ywwX8!sGC~0S?2YRBY4}!x61fUSWdh7KEuFw z^Jj{f_7_8T_N~SEMo=`;PM=-e`v2gbfUZI%3YUmwL?icPp=Nur!@xpO7Mxpfc1W^N zr@V#Fi#u|Mc27_OR>soPbr!5USy~%jlN@67)_4y2!nrs}41AzTmN0G`Qf-VkZpy5( zVSAqX>27I;m0zA=GX1{uX*O`}6(AVjin;Hl`@?jjZEjU^HAM)YmPtZ&kHVOQ_C%KH z`>Nk~di}F7_RjVk5!(F?GaH2P_cQtU&JOmImSKq4A3`nNrf}btPkMdA-nCgRa@LFH z$bg_!EbFexDjSr=bBqP+#H{j8I?oj=w-4VQ%Os|#a8cZMNBz3Eyy`l^U#v3lz1ZOU z5UU6>yj68Qt)YEON6|DSOJC9Syn#VILy?|^=8_{ zl*%AN9@=4H%h;`EUHA#-8U2r(b$I?ZcqO1@+TDmx14c`l({kJghhg|A&aB+fX;5%1 z#cMU=?rv5c_BEsE`UKiy=$vXo4INWXcG$XcN#Xv+wo=02d$qsMhC$0~W(G&CLr3AK zsH~=_U!XJg2Lx(unCEoBh6)RXNlW$AEus9D`NwRBX#xjRi8b#M1vwH z4xbkJ-4!t8f#1@Ud8gBU8ug8a&{=^b#&xByWGJ$pDnr45h)&(?0fnusr&mHz_$}p^ zq_y!I=6?3w?st}1Nxjw~!4&q-guvwwdhwrgz?2U#B{Y2sB3Uq-Yw`FTgO+vLZ+12r}z)DeHZ?>i;o zm~9_mB-APSPPj#5p2J;(Z~jJLQ^Uws17+Wn&jvg{KHf(yTXDfgm)GV)EjDl($SM}f zj)n?OfLUN=mViNz>$)|j`M`q18D!7bTmA&F)M(NvIg5V5Z@eAM4EtHG+Oxn8uGM-( zLl~4XElZhz{kxNgPXmI=yt56>4#5fQ$?;p_PNTayKP58uh#LI*cUo479WpM~yZs@z zZFk4sbb8ErDt1`6OcdsHZgwe;z`!HWPAQ+*v=ECn-^cx&@jc!X8^fM!sloj?NvHO` zbiBw_)ls}p(Nf^PLpW3U=E#~As{{R@uM@+s`1dQuB_f=btGviZo9TaPLU7YS<2#S4 ztu^FgC6Td9-KX5H!i_QMf938E@WQ2^ z3re%OD_e0PH%H|Cc3kr?<#&g-EBCuHt{pFex`U3u6H<8qIhG{*psari2xqOEbkjcW zJtNPYDXrh4?gjIgLvvVN`$t%FN_gbtzThKUdr~hqe}tcE_%l0LRpG6hn}*&@bcprO ztM|~U_wogGD~mF9|8D!M)IKdY%8)7D+UT~*B2sbE5CXh>b z$*sW@Y`#t^cW3^Gu}Q5R6#Z6Kyo+t3YS}(Z5`lRlP{H zfOTxEx;YRv=ZaqHzp5L+gsh?}r=`5HAp9;L(NE#T3BB*S!A#z$iieU%^ahve3Q@l1>_PoIV6c!5`J5DwlMhH^EOLRQ8; z4B0e8YuZr!wy|%6SxBlot0FSdH{)h}?WOe3a!NV1vq|b#T*)hSVb{1}Ud1iV{=@Wb zM{2@0&ftTG(GX8@UZg$zyEe0leoSu<%;IW6R1N`l(MHK#Jtj(aMlUh$X!3Dqy5anL zBDv^pHEWBW3aI|`;2(;g6>Yt}Nz+eiT?}oCYHfQr+8e*kyj(TS&3zmJ5|c!?VNBw) zq!eu1+V{ z1x7v^c;#Ss(AepHF0yzKPL5g#>V37Ui~O_`61rpV(iB5Wx>hgnzQr)4>o0dD$>1&N z(BP6mqt9p{l@m8m6ps7@QvSjQ%uxO!h9L~l+D^|t*{1Mjtd!SopaH2wL zP3bpShwJOkt4qzJYkNDQn9ObO>Gt$$!Dp{?wx(;igJOm*s7DKcV`6i4v9BYfWen$E zXlsF;>s4nqzIa}$zY(cFu`{(F;hvb2$OV%55-5==hFGl?sb?8;d8O{AB?ooU~nG5HliQ%O)S4M$OnJeokM`aawV6)Q{ z1hTFV6FJ@}^5DMHkl%QPgG>kE0LV1n=xz{Y`|HuVoWIT9MX#n_ctnLE8+^M_YnFOv zg#>nm%FABNhN>g(eprx303}@?E{C*D4m3h`ei#25d^HxqB7i!C81O8IEc-=u3qaiT zKtXSh5dRV1Ri}m0+Gq1+ka7(b^e!hpa{SA&u+6NcC2^cA5^MQ6Sbzp8m@nw1YP%gL zsF=K0irpx-oy{ifF3U%D-+{O}f+%Q(HlXB$z`64tjNq?;Wh-!oT@vki23Ja=M!Ew% z<$M1mXFB#b20XY#nnrhg5MC#mo@dwSBvo~-S&rkJT>U2hzv0f}0x_{M0^(5ExbA<3 z`$W^|m+N&Qg6aPbR{*$nn9FjY~_8;;yX<=00O+^?&w=mX??u zqK}cFDxmD@FVN(Fm6s}7NMyIbI`s@ly7gr>xbKYyRsK=hN`Y86P@xUR+8< zbv$aa(lSuZs*?&AHWZT*Q4{|M`Z1{azLjVSa$=~);dj2czXxwdqA+?{GBVQY2}gQ2VYi|~Y0C_7BwVFHn0H;Rogearir4~vYX^#wuO zfCYNrceQjNu0B&QXTRbdv0DFofo@jqR9r^KQx-2DzZH1OVDYL@0{s)z+&w9u8YS9p z?;cY7P)f>6CGhEQ2gOI1S-;<=Za2vJynU8fHPInRB z(&1%aTA&W@95CNk6%XmAUqSWNjMEhta#7C}EiEEv#yJc&G{QsJNhh}+(sMcixiX|4(lq{Yt(l0MvQZpEVRYdK}Z+J~NQRevqGTihy_TacaInr&caJf7|??Ngdx2%S^wL2Z45J-O7%*&&j|H6vPB8{1oQ zTC9*;KF^bC%_>i?kfobRk&)ect$xkS3qexA4Ye~vbS>fr)|NlNf3HacvO1UDH~`AFQ@J>E1l=ZI_Pb4$KsF1=wr9Pc{3T4STi zD71GJd@!Pe4U5F?YR9fQSfYSk20}O+x1~!0$k(L0{@ZG}8NyZ!ZS4wrJ4O$p98vEo zWofRX0m#>N?!{3U7y_V-)F&E9&1~ zPx;RB5FG%7+l?OCu7?+2Nqp(}^w_21nE%_QN{~2lsY0m^5?Mh1t { + final CustomerRepository customerRepository; + bool loggedIn = false; + AccountBloc({this.customerRepository}) : super(AccountInitial()) { + if ( Auth().userLoggedIn != null ) { + customerRepository.customer = Auth().userLoggedIn; + loggedIn = true; + } + } + + @override + Stream mapEventToState( + AccountEvent event, + ) async* { + try { + if (event is AccountChangeCustomer) { + // route to Customer Change page + yield AccountReady(); + } else if (event is AccountLogin) { + //route to Login Page + } else if (event is AccountLogInFinished) { + customerRepository.customer = event.customer; + yield AccountLoggedIn(); + } else if (event is AccountLogout) { + await Auth().logout(); + customerRepository.customer = null; + loggedIn = false; + yield AccountLoggedOut(); + } + } on Exception catch(e) { + yield AccountError(message: e.toString()); + } + } +} diff --git a/lib/bloc/account/account_event.dart b/lib/bloc/account/account_event.dart new file mode 100644 index 0000000..a433b05 --- /dev/null +++ b/lib/bloc/account/account_event.dart @@ -0,0 +1,46 @@ +part of 'account_bloc.dart'; + +@immutable +abstract class AccountEvent extends Equatable { + const AccountEvent(); + + @override + List get props => []; +} + +class AccountChangeCustomer extends AccountEvent { + final Customer customer; + + const AccountChangeCustomer({this.customer}); + + @override + List get props => [customer]; +} + +class AccountLogout extends AccountEvent { + final Customer customer; + + const AccountLogout({this.customer}); + + @override + List get props => [customer]; + +} + +class AccountLogin extends AccountEvent { + final Customer customer; + + const AccountLogin({this.customer}); + + @override + List get props => [customer]; +} + +class AccountLogInFinished extends AccountEvent { + final Customer customer; + + const AccountLogInFinished({this.customer}); + + @override + List get props => [customer]; +} \ No newline at end of file diff --git a/lib/bloc/account/account_state.dart b/lib/bloc/account/account_state.dart new file mode 100644 index 0000000..8fa858d --- /dev/null +++ b/lib/bloc/account/account_state.dart @@ -0,0 +1,36 @@ +part of 'account_bloc.dart'; + +@immutable +abstract class AccountState extends Equatable { + const AccountState(); + @override + List get props => []; +} + +class AccountInitial extends AccountState { + const AccountInitial(); +} + +class AccountLoading extends AccountState { + const AccountLoading(); +} + +class AccountReady extends AccountState { + const AccountReady(); +} + +class AccountLoggedOut extends AccountState { + const AccountLoggedOut(); +} + +class AccountLoggedIn extends AccountState { + const AccountLoggedIn(); +} + +class AccountError extends AccountState { + final String message; + const AccountError({this.message}); + + @override + List get props => [message]; +} diff --git a/lib/bloc/custom_exercise_form_bloc.dart b/lib/bloc/custom_exercise_form_bloc.dart new file mode 100644 index 0000000..4f7675a --- /dev/null +++ b/lib/bloc/custom_exercise_form_bloc.dart @@ -0,0 +1,127 @@ +import 'dart:math'; + +import 'package:aitrainer_app/repository/exercise_repository.dart'; +import 'package:flutter_form_bloc/flutter_form_bloc.dart'; + +class CustomExerciseFormBloc extends FormBloc { + final ExerciseRepository exerciseRepository; + final quantityField = TextFieldBloc( + validators: [ + FieldBlocValidators.required, + ], + ); + + final unitQuantityField = TextFieldBloc( + validators: [ + FieldBlocValidators.required, + ], + ); + + //1RM calculated Fields + final rmWendlerField = TextFieldBloc( initialValue: "0",); + final rmMcGothlinField = TextFieldBloc( initialValue: "0"); + final rmLombardiField = TextFieldBloc( initialValue: "0"); + final rmMayhewField = TextFieldBloc( initialValue: "0"); + final rmOconnerField = TextFieldBloc( initialValue: "0"); + final rmWathenField = TextFieldBloc( initialValue: "0"); + final rmAverageField = TextFieldBloc( initialValue: "0"); + final rm90Field = TextFieldBloc( initialValue: "0"); + final rm80Field = TextFieldBloc( initialValue: "0"); + final rm70Field = TextFieldBloc( initialValue: "0"); + final rm60Field = TextFieldBloc( initialValue: "0"); + final rm50Field = TextFieldBloc( initialValue: "0"); + + CustomExerciseFormBloc({this.exerciseRepository}) { + addFieldBlocs(fieldBlocs: [ + quantityField, + unitQuantityField, + rmWendlerField, + rmMcGothlinField, + rmLombardiField, + rmMayhewField, + rmOconnerField, + rmWathenField, + rmAverageField, + rm90Field, + rm80Field, + rm70Field, + rm60Field, + rm50Field + ]); + + quantityField.onValueChanges(onData: (previous, current) async* { + exerciseRepository.setQuantity(current.valueToDouble); + calculate1RM(); + }); + + unitQuantityField.onValueChanges(onData: (previous, current) async* { + exerciseRepository.setUnitQuantity(current.valueToDouble); + calculate1RM(); + }); + } + + @override + void onSubmitting() async { + print("on Submitting Custom form"); + try { + emitLoading(progress: 30); + // Emit either Loaded or Error + + emitSuccess(canSubmitAgain: false); + } on Exception catch (ex) { + emitFailure(failureResponse: ex.toString()); + } + } + + void calculate1RM() { + double weight = exerciseRepository.exercise.unitQuantity; + double repeat = exerciseRepository.exercise.quantity; + if ( weight == 0 || repeat == 0) { + return; + } + print("Calculating 1RM"); + exerciseRepository.rmWendler = weight * repeat * 0.0333 + weight; + rmWendlerField.updateValue(exerciseRepository.rmWendler.toString()); + exerciseRepository.rmMcglothlin = 100 * weight / (101.3 - 2.67123 * repeat); + rmMcGothlinField.updateValue(exerciseRepository.rmMcglothlin.toString()); + exerciseRepository.rmLombardi = pow(weight * repeat, 0.1); + rmLombardiField.updateValue(exerciseRepository.rmLombardi.toString()); + exerciseRepository.rmOconner = weight * (1 + repeat / 40); + rmOconnerField.updateValue(exerciseRepository.rmOconner.toString()); + exerciseRepository.rmMayhew = + 100 * weight / (52.2 + 41.9 * pow(e, -0.055 * repeat)); + rmMayhewField.updateValue(exerciseRepository.rmMayhew.toString()); + exerciseRepository.rmWathen = + 100 * weight / (48.8 + 53.8 * pow(e, -0.075 * repeat)); + rmWathenField.updateValue(exerciseRepository.rmWathen.toString()); + double average = (exerciseRepository.rmWendler + exerciseRepository.rmWathen + + exerciseRepository.rmMayhew + exerciseRepository.rmOconner + + exerciseRepository.rmMcglothlin)/5; + rmAverageField.updateValue(average.toString()); + rm90Field.updateValue((average*0.9).toString()); + rm80Field.updateValue((average*0.8).toString()); + rm70Field.updateValue((average*0.7).toString()); + rm60Field.updateValue((average*0.6).toString()); + rm50Field.updateValue((average*0.5).toString()); + } + + //@override + Future close() { + quantityField.close(); + unitQuantityField.close(); + + rmWendlerField.close(); + rmMcGothlinField.close(); + rmLombardiField.close(); + rmMayhewField.close(); + rmOconnerField.close(); + rmWathenField.close(); + rmAverageField.close(); + rm90Field.close(); + rm80Field.close(); + rm70Field.close(); + rm60Field.close(); + rm50Field.close(); + return super.close(); + } +} diff --git a/lib/bloc/customer_change/customer_change_bloc.dart b/lib/bloc/customer_change/customer_change_bloc.dart new file mode 100644 index 0000000..13516ca --- /dev/null +++ b/lib/bloc/customer_change/customer_change_bloc.dart @@ -0,0 +1,38 @@ +import 'dart:async'; + +import 'package:aitrainer_app/repository/customer_repository.dart'; +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; + +part 'customer_change_event.dart'; +part 'customer_change_state.dart'; + +class CustomerChangeBloc extends Bloc { + final CustomerRepository customerRepository; + CustomerChangeBloc({this.customerRepository}) : super(CustomerChangeInitial()); + + @override + Stream mapEventToState( + CustomerChangeEvent event, + ) async* { + try { + if (event is CustomerGoalChange) { + customerRepository.setGoal(event.goal); + yield CustomerDataChanged(); + } else if (event is CustomerFitnessChange) { + customerRepository.setFitnessLevel(event.fitness); + yield CustomerDataChanged(); + } else if (event is CustomerBodyTypeChange) { + customerRepository.setBodyType(event.bodyType); + yield CustomerDataChanged(); + } else if (event is CustomerSave) { + yield CustomerSaving(); + await customerRepository.saveCustomer(); + yield CustomerSaveSuccess(); + } + } on Exception catch(e) { + yield CustomerSaveError(message: e.toString()); + } + } +} diff --git a/lib/bloc/customer_change/customer_change_event.dart b/lib/bloc/customer_change/customer_change_event.dart new file mode 100644 index 0000000..7ae94b0 --- /dev/null +++ b/lib/bloc/customer_change/customer_change_event.dart @@ -0,0 +1,36 @@ +part of 'customer_change_bloc.dart'; + +@immutable +abstract class CustomerChangeEvent extends Equatable { + const CustomerChangeEvent(); + @override + List get props => []; +} + +class CustomerGoalChange extends CustomerChangeEvent { + final String goal; + const CustomerGoalChange({this.goal}); + + @override + List get props => [goal]; +} + +class CustomerFitnessChange extends CustomerChangeEvent { + final String fitness; + const CustomerFitnessChange({this.fitness}); + + @override + List get props => [fitness]; +} + +class CustomerBodyTypeChange extends CustomerChangeEvent { + final String bodyType; + const CustomerBodyTypeChange({this.bodyType}); + + @override + List get props => [bodyType]; +} + +class CustomerSave extends CustomerChangeEvent { + const CustomerSave(); +} diff --git a/lib/bloc/customer_change/customer_change_state.dart b/lib/bloc/customer_change/customer_change_state.dart new file mode 100644 index 0000000..5506468 --- /dev/null +++ b/lib/bloc/customer_change/customer_change_state.dart @@ -0,0 +1,32 @@ +part of 'customer_change_bloc.dart'; + +@immutable +abstract class CustomerChangeState extends Equatable { + const CustomerChangeState(); + @override + List get props => []; +} + +class CustomerChangeInitial extends CustomerChangeState { + const CustomerChangeInitial(); +} + +class CustomerSaving extends CustomerChangeState { + const CustomerSaving(); +} + +class CustomerDataChanged extends CustomerChangeState { + const CustomerDataChanged(); +} + +class CustomerSaveSuccess extends CustomerChangeState { + const CustomerSaveSuccess(); +} + +class CustomerSaveError extends CustomerChangeState { + final String message; + const CustomerSaveError({this.message}); + + @override + List get props => [message]; +} diff --git a/lib/bloc/customer_change_form_bloc.dart b/lib/bloc/customer_change_form_bloc.dart new file mode 100644 index 0000000..0ccac4b --- /dev/null +++ b/lib/bloc/customer_change_form_bloc.dart @@ -0,0 +1,101 @@ +import 'package:aitrainer_app/repository/customer_repository.dart'; +import 'package:flutter_form_bloc/flutter_form_bloc.dart'; + +class CustomerChangeFormBloc extends FormBloc { + final CustomerRepository customerRepository; + + final emailField = TextFieldBloc( + validators: [ + FieldBlocValidators.required, + ], + ); + + final firstNameField = TextFieldBloc( + validators: [ + FieldBlocValidators.required, + ], + ); + + final nameField = TextFieldBloc(); + final passwordField = TextFieldBloc( + validators: [ + //FieldBlocValidators.confirmPassword(passwordField), + ], + ); + final birthYearField = TextFieldBloc(); + final weightField = TextFieldBloc(); + final genderField = SelectFieldBloc(); + + final goalField = TextFieldBloc(); + + CustomerChangeFormBloc({this.customerRepository}) { + addFieldBlocs(fieldBlocs: [ + emailField, + firstNameField, + nameField, + passwordField, + birthYearField, + weightField, + genderField, + + goalField, + ]); + + emailField.updateInitialValue(customerRepository.customer.email); + firstNameField.updateInitialValue(customerRepository.customer.firstname); + nameField.updateInitialValue(customerRepository.customer.name); + birthYearField.updateInitialValue(customerRepository.customer.birthYear.toString()); + weightField.updateInitialValue(customerRepository.customer.weight.toString()); + genderField.updateInitialValue(customerRepository.getGenderByDBValue(customerRepository.sex)); + + firstNameField.onValueChanges(onData: (previous, current) async* { + customerRepository.setFirstName(current.value); + }); + nameField.onValueChanges(onData: (previous, current) async* { + customerRepository.setName(current.value); + }); + birthYearField.onValueChanges(onData: (previous, current) async* { + customerRepository.setBirthYear(current.valueToInt); + }); + weightField.onValueChanges(onData: (previous, current) async* { + customerRepository.setWeight(current.valueToInt); + }); + + customerRepository.genders.forEach((element) { + genderField.addItem(element.name); + }); + + genderField.onValueChanges(onData: (previous, current) async* { + String dbValue = customerRepository.getGenderByName(current.value); + customerRepository.setSex(dbValue); + }); + + + } + + @override + void onSubmitting() async { + print("on Submitting Custom form"); + try { + emitLoading(progress: 30); + // Emit either Loaded or Error + await customerRepository.saveCustomer(); + emitSuccess(canSubmitAgain: false); + } on Exception catch (ex) { + emitFailure(failureResponse: ex.toString()); + } + } + + @override + Future close() { + emailField.close(); + firstNameField.close(); + nameField.close(); + passwordField.close(); + birthYearField.close(); + weightField.close(); + genderField.close(); + return super.close(); + } + +} \ No newline at end of file diff --git a/lib/bloc/exercise_form_bloc.dart b/lib/bloc/exercise_form_bloc.dart new file mode 100644 index 0000000..ebb6a3b --- /dev/null +++ b/lib/bloc/exercise_form_bloc.dart @@ -0,0 +1,64 @@ +import 'package:aitrainer_app/repository/exercise_repository.dart'; +import 'package:flutter_form_bloc/flutter_form_bloc.dart'; + +class ExerciseFormBloc extends FormBloc { + final ExerciseRepository exerciseRepository; + + final quantityField = TextFieldBloc( + validators: [ + FieldBlocValidators.required, + ], + ); + + final unitField = TextFieldBloc( + validators: [ + FieldBlocValidators.required, + ], + ); + + final unitQuantityField = TextFieldBloc(); + final unitQuantityUnitField = TextFieldBloc(); + + ExerciseFormBloc({this.exerciseRepository}) { + addFieldBlocs(fieldBlocs: [ + quantityField, + unitField, + unitQuantityField, + unitQuantityUnitField + ]); + + quantityField.onValueChanges(onData: (previous, current) async* { + exerciseRepository.setQuantity(current.valueToDouble); + }); + + unitField.onValueChanges(onData: (previous, current) async* { + exerciseRepository.setUnit(current.value); + }); + + unitQuantityField.onValueChanges(onData: (previous, current) async* { + exerciseRepository.setUnitQuantity(current.valueToDouble); + }); + } + + @override + void onSubmitting() async { + try { + emitLoading(progress: 30); + // Emit either Loaded or Error + await exerciseRepository.addExercise(); + emitSuccess(canSubmitAgain: false); + + } on Exception catch (ex) { + emitFailure(failureResponse: ex.toString()); + + } + } + + @override + Future close() { + unitQuantityField.close(); + unitQuantityUnitField.close(); + return super.close(); + } + +} \ No newline at end of file diff --git a/lib/bloc/login_form_bloc.dart b/lib/bloc/login_form_bloc.dart new file mode 100644 index 0000000..7a720ec --- /dev/null +++ b/lib/bloc/login_form_bloc.dart @@ -0,0 +1,61 @@ +import 'package:aitrainer_app/bloc/account/account_bloc.dart'; +import 'package:aitrainer_app/model/auth.dart'; +import 'package:aitrainer_app/repository/user_repository.dart'; +import 'package:aitrainer_app/util/common.dart'; +import 'package:flutter_form_bloc/flutter_form_bloc.dart'; + +class LoginFormBloc extends FormBloc { + final AccountBloc accountBloc; + final UserRepository userRepository; + + final emailField = TextFieldBloc( + validators: [ + FieldBlocValidators.required, + ], + ); + final passwordField = TextFieldBloc( + validators: [ + FieldBlocValidators.required, + ] + ); + + + LoginFormBloc({this.userRepository, this.accountBloc}) { + addFieldBlocs(fieldBlocs: [ + emailField, + passwordField + ]); + + emailField.onValueChanges(onData: (previous, current) async* { + userRepository.setEmail(current.value); + }); + + passwordField.onValueChanges(onData: (previous, current) async* { + userRepository.setPassword(current.value); + }); + + } + + @override + void onSubmitting() async { + try { + emitLoading(progress: 30); + if ( ! Common.validateEmail(userRepository)) { + emailField.addFieldError(Common.EMAIL_ERROR, isPermanent: true); + + emitFailure(failureResponse: Common.EMAIL_ERROR); + } else if ( ! Common.validatePassword(userRepository)) { + passwordField.addFieldError(Common.PASSWORD_ERROR, isPermanent: true); + emitFailure(failureResponse: Common.PASSWORD_ERROR); + } else { + // Emit either Loaded or Error + await userRepository.getUser(); + emitSuccess(canSubmitAgain: false); + accountBloc.add(AccountLogInFinished(customer: Auth().userLoggedIn)); + } + } on Exception catch (ex) { + emitFailure(failureResponse: ex.toString()); + + } + } +} diff --git a/lib/bloc/menu/menu_bloc.dart b/lib/bloc/menu/menu_bloc.dart new file mode 100644 index 0000000..630799a --- /dev/null +++ b/lib/bloc/menu/menu_bloc.dart @@ -0,0 +1,51 @@ +import 'dart:async'; + +import 'package:aitrainer_app/model/workout_tree.dart'; +import 'package:aitrainer_app/repository/menu_tree_repository.dart'; +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; + +part 'menu_event.dart'; +part 'menu_state.dart'; + +class MenuBloc extends Bloc { + final MenuTreeRepository menuTreeRepository; + int parent; + + MenuBloc({this.menuTreeRepository}) : super(MenuInitial()) { + parent = 0; + } + + @override + Stream mapEventToState( + MenuEvent event, + ) async* { + try { + if ( event is MenuCreate ) { + yield MenuLoading(); + await menuTreeRepository.createTree(); + yield MenuReady(); + } else if (event is MenuTreeDown) { + // get child menus or exercises + yield MenuLoading(); + parent = event.parent; + menuTreeRepository.getBranch(event.parent); + yield MenuReady(); + } else if (event is MenuTreeUp) { + yield MenuLoading(); + // get parent menus or exercises + parent = event.parent; + menuTreeRepository.getBranch(parent); + + yield MenuReady(); + } else if (event is MenuClickExercise) { + yield MenuLoading(); + // get exercise page + yield MenuReady(); + } + } on Exception catch(ex) { + yield MenuError(message: ex.toString()); + } + } +} diff --git a/lib/bloc/menu/menu_event.dart b/lib/bloc/menu/menu_event.dart new file mode 100644 index 0000000..ac87f26 --- /dev/null +++ b/lib/bloc/menu/menu_event.dart @@ -0,0 +1,41 @@ +part of 'menu_bloc.dart'; + +@immutable +abstract class MenuEvent extends Equatable { + const MenuEvent(); + + @override + List get props => []; +} + +class MenuCreate extends MenuEvent { + const MenuCreate(); + + @override + List get props => []; +} + +class MenuTreeDown extends MenuEvent { + final int parent; + const MenuTreeDown({this.parent}); + + @override + List get props => [parent]; +} + +class MenuTreeUp extends MenuEvent { + final int parent; + const MenuTreeUp({this.parent}); + + @override + List get props => [parent]; +} + +class MenuClickExercise extends MenuEvent { + final int exerciseTypeId; + const MenuClickExercise({this.exerciseTypeId}); + + @override + List get props => [exerciseTypeId]; +} + diff --git a/lib/bloc/menu/menu_state.dart b/lib/bloc/menu/menu_state.dart new file mode 100644 index 0000000..8408f41 --- /dev/null +++ b/lib/bloc/menu/menu_state.dart @@ -0,0 +1,34 @@ +part of 'menu_bloc.dart'; + +@immutable +abstract class MenuState extends Equatable { + const MenuState(); + + @override + List get props => []; +} + +class MenuInitial extends MenuState { + const MenuInitial(); +} + +class MenuLoading extends MenuState { + +} + +class MenuReady extends MenuState { + final WorkoutTree workoutTree; + + const MenuReady({this.workoutTree}); + + @override + List get props => [workoutTree]; +} + +class MenuError extends MenuState { + final String message; + const MenuError({this.message}); + + @override + List get props => [message]; +} diff --git a/lib/bloc/registration_form_bloc.dart b/lib/bloc/registration_form_bloc.dart new file mode 100644 index 0000000..79c407f --- /dev/null +++ b/lib/bloc/registration_form_bloc.dart @@ -0,0 +1,61 @@ +import 'package:aitrainer_app/model/auth.dart'; +import 'package:aitrainer_app/repository/user_repository.dart'; +import 'package:aitrainer_app/util/common.dart'; +import 'package:flutter_form_bloc/flutter_form_bloc.dart'; +import 'account/account_bloc.dart'; + +class RegistrationFormBloc extends FormBloc { + final AccountBloc accountBloc; + final emailField = TextFieldBloc( + validators: [ + FieldBlocValidators.required, + ], + ); + final passwordField = TextFieldBloc( + validators: [ + FieldBlocValidators.required, + ] + ); + final UserRepository userRepository; + + RegistrationFormBloc({this.userRepository, this.accountBloc}) { + addFieldBlocs(fieldBlocs: [ + emailField, + passwordField + ]); + + emailField.onValueChanges(onData: (previous, current) async* { + userRepository.setEmail(current.value); + }); + + passwordField.onValueChanges(onData: (previous, current) async* { + userRepository.setPassword(current.value); + }); + + } + + @override + void onSubmitting() async { + try { + emitLoading(progress: 30); + if ( ! Common.validateEmail(userRepository)) { + emailField.addFieldError(Common.EMAIL_ERROR, isPermanent: true); + + emitFailure(failureResponse: Common.EMAIL_ERROR); + } else if ( ! Common.validatePassword(userRepository)) { + passwordField.addFieldError(Common.PASSWORD_ERROR, isPermanent: true); + emitFailure(failureResponse: Common.PASSWORD_ERROR); + } else { + // Emit either Loaded or Error + await userRepository.addUser(); + emitSuccess(canSubmitAgain: false); + accountBloc.add(AccountLogInFinished(customer: Auth().userLoggedIn)); + } + } on Exception catch (ex) { + emitFailure(failureResponse: ex.toString()); + + } + } + + +} \ No newline at end of file diff --git a/lib/bloc/session/session_bloc.dart b/lib/bloc/session/session_bloc.dart new file mode 100644 index 0000000..e7136d3 --- /dev/null +++ b/lib/bloc/session/session_bloc.dart @@ -0,0 +1,35 @@ +import 'dart:async'; + +import 'package:aitrainer_app/util/session.dart'; +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; + +part 'session_event.dart'; +part 'session_state.dart'; + +class SessionBloc extends Bloc { + final Session session; + SessionBloc({this.session}) : super(SessionInitial()); + + @override + Stream mapEventToState( + SessionEvent event, + ) async* { + try { + if (event is SessionStart) { + yield SessionLoading(); + await session.fetchSessionAndNavigate(); + yield SessionReady(); + } + } on Exception catch(ex) { + yield SessionFailure(message: ex.toString()); + } + + } + + @override + Future close() async { + await this.close(); super.close(); + } +} diff --git a/lib/bloc/session/session_event.dart b/lib/bloc/session/session_event.dart new file mode 100644 index 0000000..04f34a2 --- /dev/null +++ b/lib/bloc/session/session_event.dart @@ -0,0 +1,16 @@ +part of 'session_bloc.dart'; + +@immutable +abstract class SessionEvent extends Equatable { + const SessionEvent(); + + @override + List get props => []; +} + +class SessionStart extends SessionEvent { + const SessionStart(); + + @override + List get props => []; +} diff --git a/lib/bloc/session/session_state.dart b/lib/bloc/session/session_state.dart new file mode 100644 index 0000000..82822c9 --- /dev/null +++ b/lib/bloc/session/session_state.dart @@ -0,0 +1,32 @@ +part of 'session_bloc.dart'; + +@immutable +abstract class SessionState extends Equatable { + const SessionState(); + + @override + List get props => []; +} + +class SessionInitial extends SessionState { + const SessionInitial(); +} + +class SessionLoading extends SessionState { + const SessionLoading(); +} + +class SessionReady extends SessionState { + const SessionReady(); +} + +class SessionFailure extends SessionState { + final String message; + const SessionFailure({this.message}); + + @override + List get props => [message]; + +} + + diff --git a/lib/bloc/settings/settings_bloc.dart b/lib/bloc/settings/settings_bloc.dart new file mode 100644 index 0000000..f6d64c6 --- /dev/null +++ b/lib/bloc/settings/settings_bloc.dart @@ -0,0 +1,55 @@ +import 'dart:async'; + +import 'package:aitrainer_app/localization/app_language.dart'; +import 'package:aitrainer_app/localization/app_localization.dart'; +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:meta/meta.dart'; + +part 'settings_event.dart'; +part 'settings_state.dart'; + +class SettingsBloc extends Bloc { + String language; + Locale _locale; + BuildContext context; + + SettingsBloc({this.context}) : super(SettingsInitial()); + + @override + Stream mapEventToState( + SettingsEvent event, + ) async* { + if (event is SettingsChangeLanguage) { + yield SettingsLoading(); + await _changeLang( event.language); + yield SettingsReady(_locale); + } + } + + Future _changeLang( String lang ) async{ + + switch ( lang ) { + case "English": + case "Angol": + _locale = Locale('en'); + break; + case "Hungarian": + case "Magyar": + _locale = Locale('hu'); + break; + } + this.language = lang; + await loadLang(); + } + + Future loadLang() async{ + final AppLanguage appLanguage = AppLanguage(); + appLanguage.changeLanguage(_locale); + if ( context != null ) { + AppLocalizations.of(context).setLocale(_locale); + await AppLocalizations.of(context).load(); + } + } +} diff --git a/lib/bloc/settings/settings_event.dart b/lib/bloc/settings/settings_event.dart new file mode 100644 index 0000000..31a8279 --- /dev/null +++ b/lib/bloc/settings/settings_event.dart @@ -0,0 +1,13 @@ +part of 'settings_bloc.dart'; + +@immutable +abstract class SettingsEvent extends Equatable { + const SettingsEvent(); + @override + List get props => []; +} + +class SettingsChangeLanguage extends SettingsEvent { + final String language; + const SettingsChangeLanguage({this.language}); +} diff --git a/lib/bloc/settings/settings_state.dart b/lib/bloc/settings/settings_state.dart new file mode 100644 index 0000000..6935d0f --- /dev/null +++ b/lib/bloc/settings/settings_state.dart @@ -0,0 +1,46 @@ +part of 'settings_bloc.dart'; + +@immutable +abstract class SettingsState extends Equatable { + const SettingsState(); + + @override + List get props => []; +} + +// ignore: must_be_immutable +class SettingsInitial extends SettingsState { + Locale locale; + SettingsInitial(); + + setLocale(locale) { + this.locale = locale; + } + + @override + List get props => [locale]; +} + +class SettingsLoading extends SettingsState { + final Locale locale; + const SettingsLoading({this.locale}); + + @override + List get props => [locale]; +} + +class SettingsReady extends SettingsState { + final Locale locale; + const SettingsReady(this.locale); + + @override + List get props => [locale]; +} + +class SettingsError extends SettingsState { + final String message; + const SettingsError(this.message); + + @override + List get props => [message]; +} \ No newline at end of file diff --git a/lib/library_keys.dart b/lib/library_keys.dart new file mode 100644 index 0000000..528b334 --- /dev/null +++ b/lib/library_keys.dart @@ -0,0 +1,9 @@ +import 'package:flutter/widgets.dart'; + +class LibraryKeys { + + // Login + static final loginEmailField = const Key('__loginEmailField__'); + static final loginPasswordField = const Key('__loginPasswordField__'); + static final loginOKButton = const Key('__loginOKButton__'); +} \ No newline at end of file diff --git a/lib/localization/app_localization.dart b/lib/localization/app_localization.dart index afc6a58..181ba08 100644 --- a/lib/localization/app_localization.dart +++ b/lib/localization/app_localization.dart @@ -6,8 +6,9 @@ import 'package:flutter/services.dart'; class AppLocalizations { Locale locale; + bool isTest; - AppLocalizations(this.locale); + AppLocalizations(this.locale, {this.isTest = false}); // Helper method to keep the code in the widgets concise // Localizations are accessed using an InheritedWidget "of" syntax @@ -17,7 +18,7 @@ class AppLocalizations { // Static member to have a simple access to the delegate from the MaterialApp static const LocalizationsDelegate delegate = - _AppLocalizationsDelegate(); + AppLocalizationsDelegate(); Map _localizedStrings; @@ -38,18 +39,24 @@ class AppLocalizations { return true; } + Future loadTest(Locale locale) async { + return AppLocalizations(locale); + } + // This method will be called from every widget which needs a localized text String translate(String key) { + if (isTest) return key; return _localizedStrings[key]; } } -class _AppLocalizationsDelegate +class AppLocalizationsDelegate extends LocalizationsDelegate { + final bool isTest; // This delegate instance will never change (it doesn't even have fields!) // It can provide a constant constructor. - const _AppLocalizationsDelegate(); + const AppLocalizationsDelegate({this.isTest = false}); @override bool isSupported(Locale locale) { @@ -60,11 +67,16 @@ class _AppLocalizationsDelegate @override Future load(Locale locale) async { // AppLocalizations class is where the JSON loading actually runs - AppLocalizations localizations = new AppLocalizations(locale); - await localizations.load(); + AppLocalizations localizations = new AppLocalizations(locale, isTest: this.isTest); + if (isTest) { + await localizations.loadTest(locale); + } else { + await localizations.load(); + } return localizations; } + @override - bool shouldReload(_AppLocalizationsDelegate old) => false; + bool shouldReload(AppLocalizationsDelegate old) => false; } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index f21b3ae..bb8f67a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,33 +1,33 @@ import 'dart:async'; +import 'package:aitrainer_app/repository/customer_repository.dart'; +import 'package:aitrainer_app/repository/menu_tree_repository.dart'; +import 'package:aitrainer_app/util/session.dart'; import 'package:aitrainer_app/view/account.dart'; +import 'package:aitrainer_app/view/custom_exercise_page.dart'; import 'package:aitrainer_app/view/customer_bodytype_page.dart'; import 'package:aitrainer_app/view/customer_fitness_page.dart'; import 'package:aitrainer_app/view/customer_goal_page.dart'; import 'package:aitrainer_app/view/customer_modify_page.dart'; -import 'package:aitrainer_app/view/customer_new_page.dart'; import 'package:aitrainer_app/view/customer_welcome_page.dart'; import 'package:aitrainer_app/view/gdpr.dart'; import 'package:aitrainer_app/view/login.dart'; import 'package:aitrainer_app/view/exercise_new_page.dart'; -import 'package:aitrainer_app/view/exercise_type_modify_page.dart'; -import 'package:aitrainer_app/view/exercise_type_new_page.dart'; import 'package:aitrainer_app/view/menu_page.dart'; import 'package:aitrainer_app/view/registration.dart'; import 'package:aitrainer_app/view/settings.dart'; -import 'package:aitrainer_app/viewmodel/customer_changing_view_model.dart'; -import 'package:aitrainer_app/viewmodel/exercise_changing_view_model.dart'; import 'package:aitrainer_app/widgets/home.dart'; -import 'package:aitrainer_app/widgets/loading.dart'; import 'package:flutter/material.dart'; -import 'package:aitrainer_app/view/customer_list_page.dart'; -import 'package:aitrainer_app/view/exercise_type_list_page.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; -import 'package:provider/provider.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:aitrainer_app/localization/app_localization.dart'; import 'package:sentry/sentry.dart'; +import 'bloc/account/account_bloc.dart'; +import 'bloc/menu/menu_bloc.dart'; +import 'bloc/session/session_bloc.dart'; +import 'bloc/settings/settings_bloc.dart'; final SentryClient _sentry = new SentryClient(dsn: 'https://5fac40cbfcfb4c15aa80c7a8638d7310@o418565.ingest.sentry.io/5322520'); @@ -95,17 +95,25 @@ Future main() async { // - https://www.dartlang.org/articles/libraries/zones runZonedGuarded>(() async { runApp( - MultiProvider( - // Initialize the model in the builder. That way, Provider - // can own Models's lifecycle, making sure to call `dispose` - // when not needed anymore. + MultiBlocProvider( providers: [ - ChangeNotifierProvider(create: (context) => ExerciseChangingViewModel(null)), - ChangeNotifierProvider(create: (context) => CustomerChangingViewModel(null)), + BlocProvider( + create: (BuildContext context) => SessionBloc(session: Session()), + ), + BlocProvider( + create: (BuildContext context) => MenuBloc( menuTreeRepository: MenuTreeRepository()), + ), + BlocProvider( + create: (BuildContext context) => SettingsBloc(), + ), + BlocProvider( + create: (BuildContext context) => AccountBloc(customerRepository: CustomerRepository()), + ), ], - child: AitrainerApp(), - )); + child: AitrainerApp(), + ) + ); }, (error, stackTrace) async { await _reportError(error, stackTrace); }); @@ -147,18 +155,13 @@ class AitrainerApp extends StatelessWidget { }, routes: { 'home': (context) => AitrainerHome(), - 'loading': (context) => LoadingScreenMain(), - 'customersPage': (context) => CustomerListPage(), - 'customerNewPage': (context) => CustomerNewPage(), 'customerModifyPage': (context) => CustomerModifyPage(), 'customerGoalPage': (context) => CustomerGoalPage(), 'customerFitnessPage': (context) => CustomerFitnessPage(), 'customerBodyTypePage': (context) => CustomerBodyTypePage(), 'customerWelcomePage': (context) => CustomerWelcomePage(), - 'exerciseTypeListPage': (context) => ExerciseTypeListPage(), - 'exerciseTypeNewPage': (context) => ExerciseTypeNewPage(), - 'exerciseTypeModifyPage': (context) => ExerciseTypeModifyPage(), 'exerciseNewPage': (context) => ExerciseNewPage(), + 'exerciseCustomPage': (context) => CustomExercisePage(), 'login': (context) => LoginPage(), 'registration': (context) => RegistrationPage(), 'gdpr': (context) => Gdpr(), @@ -166,7 +169,7 @@ class AitrainerApp extends StatelessWidget { 'account': (context) => AccountPage(), 'settings': (context) => SettingsPage(), }, - initialRoute: 'loading', + initialRoute: 'home', title: 'Aitrainer', theme: ThemeData( brightness: Brightness.light, @@ -176,7 +179,7 @@ class AitrainerApp extends StatelessWidget { bodyText1: TextStyle(fontSize: 14.0), ) ), - home: LoadingScreenMain(), + home: AitrainerHome(), ); } diff --git a/lib/model/auth.dart b/lib/model/auth.dart index fb920f1..14862c3 100644 --- a/lib/model/auth.dart +++ b/lib/model/auth.dart @@ -1,4 +1,6 @@ import 'package:aitrainer_app/model/customer.dart'; +import 'package:aitrainer_app/model/exercise_tree.dart'; +import 'package:aitrainer_app/service/exercise_tree_service.dart'; import 'package:aitrainer_app/service/exercisetype_service.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:aitrainer_app/model/exercise_type.dart'; @@ -35,7 +37,8 @@ class Auth { static final String isRegisteredKey = 'is_registered'; static final String isLoggedInKey = 'is_logged_in'; - static final String _baseUrl = 'http://andio.eu:8888/api/'; + static final String baseUrl = 'http://aitrainer.info:8888/api/'; + static final String mediaUrl = 'https://aitrainer.info:4343/media/'; static final String username = 'bosi'; static final String password = 'andio2009'; @@ -43,7 +46,9 @@ class Auth { Customer userLoggedIn; bool firstLoad = true; List _exerciseTypes; + List _exerciseTree; List deviceLanguages; + String startPage; factory Auth() { return _singleton; @@ -60,7 +65,11 @@ class Auth { } static String getBaseUrl() { - return _baseUrl; + return baseUrl; + } + + static String getMediaUrl() { + return mediaUrl; } afterRegistration(Customer customer) { @@ -70,20 +79,19 @@ class Auth { setPreferences(prefs, SharePrefsChange.registration, customer.customerId); } - afterLogin(Customer customer) { + afterLogin(Customer customer) async { Future prefs = SharedPreferences.getInstance(); userLoggedIn = customer; - setPreferences(prefs, SharePrefsChange.login, customer.customerId); + await setPreferences(prefs, SharePrefsChange.login, customer.customerId); } - logout(){ + logout() async { userLoggedIn = null; authToken = ""; //firstLoad = true; Future prefs = SharedPreferences.getInstance(); - setPreferences(prefs, SharePrefsChange.logout, 0); - + await setPreferences(prefs, SharePrefsChange.logout, 0); } setPreferences(Future prefs, @@ -95,18 +103,21 @@ class Auth { DateTime now = DateTime.now(); sharedPreferences.setString(Auth.lastStoreDateKey, now.toString()); if ( type == SharePrefsChange.registration ) { + Auth().startPage = "home"; sharedPreferences.setInt(Auth.customerIdKey, customerId); sharedPreferences.setBool(Auth.isRegisteredKey, true); sharedPreferences.setBool(Auth.isLoggedInKey, true); - await ExerciseTypeApi().getExerciseTypes(""); + await ExerciseTypeApi().getExerciseTypes(); + await ExerciseTreeApi().getExerciseTree(); } else if ( type == SharePrefsChange.login ) { - + Auth().startPage = "home"; sharedPreferences.setInt(Auth.customerIdKey, customerId); sharedPreferences.setBool(Auth.isLoggedInKey, true); - await ExerciseTypeApi().getExerciseTypes(""); + await ExerciseTypeApi().getExerciseTypes(); + await ExerciseTreeApi().getExerciseTree(); } else if ( type == SharePrefsChange.logout ) { sharedPreferences.setBool(Auth.isLoggedInKey, false); - //sharedPreferences.setInt(Auth.customerIdKey, 0); + sharedPreferences.setInt(Auth.customerIdKey, 0); sharedPreferences.setString(authTokenKey, ""); } } @@ -115,7 +126,15 @@ class Auth { this._exerciseTypes = exerciseTypes; } + void setExerciseTree( List exerciseTree) { + this._exerciseTree = exerciseTree; + } + List getExerciseTypes() { return this._exerciseTypes; } + + List getExerciseTree() { + return this._exerciseTree; + } } \ No newline at end of file diff --git a/lib/model/exercise_tree.dart b/lib/model/exercise_tree.dart new file mode 100644 index 0000000..417190e --- /dev/null +++ b/lib/model/exercise_tree.dart @@ -0,0 +1,20 @@ +class ExerciseTree { + int treeId; + int parentId; + String name; + String imageUrl; + bool active; + String nameTranslation; + + ExerciseTree.fromJson(Map json) { + this.treeId = json['treeId']; + this.name = json['name']; + this.parentId = json['parentId']; + this.imageUrl = json['imageUrl']; + this.active = json['active']; + this.nameTranslation = + json['translations'] != null && (json['translations']).length > 0 + ? json['translations'][0]['name'] + : this.name; + } +} diff --git a/lib/model/exercise_type.dart b/lib/model/exercise_type.dart index 3f11e0b..4301a58 100644 --- a/lib/model/exercise_type.dart +++ b/lib/model/exercise_type.dart @@ -2,22 +2,32 @@ import 'package:flutter/services.dart'; class ExerciseType { int exerciseTypeId; + int treeId; String name; String description; BinaryCodec video; String unit; String unitQuantity; String unitQuantityUnit; + bool active; + String imageUrl; + String nameTranslation; + String descriptionTranslation; ExerciseType({this.name, this.description}); ExerciseType.fromJson(Map json) { this.exerciseTypeId = json['exerciseTypeId']; + this.treeId = json['treeId']; this.name = json['name']; this.description = json['description']; this.unit = json['unit']; this.unitQuantity = json['unitQuantity']; this.unitQuantityUnit = json['unitQuantityUnit']; + this.active = json['active']; + this.imageUrl = json['images'][0]['url']; + this.nameTranslation = json['translations'][0]['name']; + this.descriptionTranslation = json['translations'][0]['description']; } Map toJson() => @@ -26,6 +36,7 @@ class ExerciseType { "description": description, "unit": unit, "unitQuantity": unitQuantity, - "unitQuantityUnit": unitQuantityUnit + "unitQuantityUnit": unitQuantityUnit, + "active": active }; } \ No newline at end of file diff --git a/lib/model/workout_tree.dart b/lib/model/workout_tree.dart index 48f8253..17b17df 100644 --- a/lib/model/workout_tree.dart +++ b/lib/model/workout_tree.dart @@ -1,5 +1,7 @@ import 'dart:ui'; +import 'exercise_type.dart'; + class WorkoutTree { int id; int parent; @@ -9,7 +11,8 @@ class WorkoutTree { double fontSize; bool child; int exerciseTypeId; + ExerciseType exerciseType; - WorkoutTree(this.id, this.parent, this.name, this.imageName, this.color, this.fontSize, this.child, this.exerciseTypeId); + WorkoutTree(this.id, this.parent, this.name, this.imageName, this.color, this.fontSize, this.child, this.exerciseTypeId, this.exerciseType); } diff --git a/lib/repository/customer_repository.dart b/lib/repository/customer_repository.dart new file mode 100644 index 0000000..4226a2c --- /dev/null +++ b/lib/repository/customer_repository.dart @@ -0,0 +1,143 @@ +import 'package:aitrainer_app/model/customer.dart'; +import 'package:aitrainer_app/service/customer_service.dart'; + +class GenderItem { + GenderItem(this.dbValue,this.name); + final String dbValue; + String name; +} + +class CustomerRepository { + Customer customer; + //List customerList = List(); + bool visibleDetails = false; + List genders; + + CustomerRepository({this.customer}) { + customer = Customer(); + genders = [ + GenderItem("m", "Man"), + GenderItem("w", "Woman"), + ]; + } + + String getGenderByName(String name) { + String dbValue; + genders.forEach((element) { + if (element.name == name) { + dbValue = element.dbValue; + } + }); + return dbValue; + } + + String getGenderByDBValue(String dbValue) { + String name; + genders.forEach((element) { + if (element.dbValue == dbValue) { + name = element.name; + } + }); + return name; + } + + String get name { + return this.customer.name != null ? this.customer.name : ""; + } + + String get firstName { + return this.customer.firstname != null ? this.customer.firstname : ""; + } + + String get sex { + return this.customer.sex == "m" ? "Man" : "Woman"; + } + + int get birthYear { + return this.customer.birthYear; + } + + String get goal { + return this.customer.goal; + } + + String get fitnessLevel { + return this.customer.fitnessLevel; + } + + String get bodyType { + return this.customer.bodyType; + } + + setName(String name) { + this.customer.name = name; + } + setFirstName(String firstName) { + this.customer.firstname = firstName; + } + + setPassword( String password ) { + this.customer.password = password; + } + + setEmail(String email) { + this.customer.email = email; + } + + + setSex(String sex) { + this.customer.sex = sex; + } + + setWeight( int weight) { + this.customer.weight = weight; + } + + setBirthYear( int birthYear ) { + this.customer.birthYear = birthYear; + } + + setFitnessLevel( String level ) { + this.customer.fitnessLevel = level; + } + + setGoal( String goal ) { + this.customer.goal = goal; + } + + setBodyType(String bodyType) { + this.customer.bodyType = bodyType; + } + + createNew() { + this.customer = Customer(); + } + + Customer getCustomer() { + return this.customer; + } + + void setCustomer ( Customer customer ) { + this.customer = customer; + } + + Future addCustomer() async { + final Customer modelCustomer = customer; + await CustomerApi().addCustomer(modelCustomer); + } + + Future saveCustomer() async { + final Customer modelCustomer = customer; + await CustomerApi().saveCustomer(modelCustomer); + } + + /* Future> getCustomers() async { + final results = await CustomerApi().getRealCustomers(""); + this.customerList = results.map((item) => CustomerRepository(customer: item)).toList(); + return this.customerList; + } + + addNewCustomerToList(CustomerRepository customerViewModel) { + customerList.add(customerViewModel); + }*/ +} diff --git a/lib/repository/exercise_repository.dart b/lib/repository/exercise_repository.dart new file mode 100644 index 0000000..4bc949c --- /dev/null +++ b/lib/repository/exercise_repository.dart @@ -0,0 +1,88 @@ +import 'package:aitrainer_app/model/customer.dart'; +import 'package:aitrainer_app/model/exercise.dart'; +import 'package:aitrainer_app/model/exercise_type.dart'; +import 'package:aitrainer_app/service/exercise_service.dart'; + +class ExerciseRepository { + Exercise exercise; + Customer customer; + ExerciseType exerciseType; + + double rmWendler = 0; + double rmMcglothlin = 0; + double rmLombardi = 0; + double rmMayhew = 0; + double rmOconner = 0; + double rmWathen = 0; + + createNew() { + this.exercise = Exercise(); + exercise.dateAdd = DateTime.now(); + } + + setQuantity(double quantity) { + if ( this.exercise == null ) { + this.createNew(); + } + this.exercise.quantity = quantity; + } + + setUnitQuantity(double unitQuantity) { + if ( this.exercise == null ) { + this.createNew(); + } + + this.exercise.unitQuantity = unitQuantity; + } + + setUnit( String unit) { + if ( this.exercise == null ) { + this.createNew(); + } + + this.exercise.unit = unit; + } + + setDatetimeExercise(DateTime datetimeExercise) { + if ( this.exercise == null ) { + this.createNew(); + } + + this.exercise.dateAdd = datetimeExercise; + } + + double get unitQuantity { + return this.exercise.unitQuantity; + } + + double get quantity { + return this.exercise.quantity; + } + + Exercise getExercise() { + return this.exercise; + } + + Future addExercise() async { + final Exercise modelExercise = this.exercise; + modelExercise.customerId = this.customer.customerId; + modelExercise.exerciseTypeId = this.exerciseType.exerciseTypeId; + await ExerciseApi().addExercise(modelExercise); + } + + + setCustomer(Customer customer) { + this.customer = customer; + } + + setExerciseType( ExerciseType exerciseType) { + this.exerciseType = exerciseType; + } + +/* + Future> getExercisesByCustomer( int customerId ) async { + final results = await ExerciseApi().getExercisesByCustomer(customerId); + this.exerciseList = results.map((item) => ExerciseRepository(exercise: item)).toList(); + return this.exerciseList; + } */ +} \ No newline at end of file diff --git a/lib/repository/menu_tree_repository.dart b/lib/repository/menu_tree_repository.dart new file mode 100644 index 0000000..860cac8 --- /dev/null +++ b/lib/repository/menu_tree_repository.dart @@ -0,0 +1,73 @@ +import 'dart:collection'; +import 'package:aitrainer_app/localization/app_language.dart'; +import 'package:aitrainer_app/model/auth.dart'; +import 'package:aitrainer_app/model/exercise_tree.dart'; +import 'package:aitrainer_app/model/exercise_type.dart'; +import 'package:aitrainer_app/model/workout_tree.dart'; +import 'package:aitrainer_app/service/exercise_tree_service.dart'; +import 'package:aitrainer_app/service/exercisetype_service.dart'; +import 'package:flutter/material.dart'; + +class MenuTreeRepository { + final LinkedHashMap tree = LinkedHashMap(); + + Future createTree() async { + + final AppLanguage appLanguage = AppLanguage(); + bool isEnglish = appLanguage.appLocal == Locale('en'); + + List exerciseTree = Auth().getExerciseTree(); + if ( exerciseTree == null || exerciseTree.length == 0) { + await ExerciseTreeApi().getExerciseTree(); + } + + exerciseTree.forEach( (treeItem) async { + String treeName = isEnglish ? treeItem.name : treeItem.nameTranslation; + String assetImage = 'asset/menu/' + treeItem.imageUrl.substring(7); + this.tree[treeItem.name] = WorkoutTree( + treeItem.treeId, + treeItem.parentId, + treeName, + assetImage, Colors.white, + 32, + false, + 0, + null + ); + }); + + List exerciseTypes = Auth().getExerciseTypes(); + if ( exerciseTypes == null || exerciseTypes.length == 0) { + await ExerciseTypeApi().getExerciseTypes(); + } + + exerciseTypes.forEach( (exerciseType) { + String exerciseTypeName = isEnglish ? + exerciseType.name : exerciseType.nameTranslation; + String assetImage = 'asset/menu/' + exerciseType.imageUrl.substring(7); + this.tree[exerciseType.name] = WorkoutTree( + exerciseType.exerciseTypeId, + exerciseType.treeId, + exerciseTypeName, + assetImage, + Colors.white, + 16, + true, + exerciseType.exerciseTypeId, + exerciseType + ); + }); + } + + + LinkedHashMap getBranch(int parent) { + LinkedHashMap branch = LinkedHashMap(); + tree.forEach((key, value) { + WorkoutTree workoutTree = value as WorkoutTree; + if ( parent == workoutTree.parent) { + branch[key] = value; + } + }); + return branch; + } +} \ No newline at end of file diff --git a/lib/repository/user_repository.dart b/lib/repository/user_repository.dart new file mode 100644 index 0000000..4a7a94a --- /dev/null +++ b/lib/repository/user_repository.dart @@ -0,0 +1,32 @@ +import 'package:aitrainer_app/model/user.dart'; +import 'package:aitrainer_app/service/customer_service.dart'; + +class UserRepository { + User user; + + UserRepository() { + this.createNewUser(); + } + + setEmail(String email) { + this.user.email = email; + } + + setPassword(String password) { + this.user.password = password; + } + + createNewUser() { + this.user = User(); + } + + Future addUser() async { + final User modelUser = this.user; + await CustomerApi().addUser(modelUser); + } + + Future getUser() async { + final User modelUser = this.user; + await CustomerApi().getUser(modelUser); + } +} diff --git a/lib/service/customer_service.dart b/lib/service/customer_service.dart index f0b154c..b8da324 100644 --- a/lib/service/customer_service.dart +++ b/lib/service/customer_service.dart @@ -39,8 +39,13 @@ class CustomerApi { body); Customer customer; try { - customer = Customer.fromJson(jsonDecode(responseBody)); - Auth().afterRegistration(customer); + int status = jsonDecode(responseBody)['status']; + if ( status != null ) { + throw new Exception(jsonDecode(responseBody)['error']); + } else { + customer = Customer.fromJson(jsonDecode(responseBody)); + Auth().afterRegistration(customer); + } } on FormatException catch(exception) { throw new Exception(responseBody); } @@ -56,7 +61,7 @@ class CustomerApi { Customer customer; try { customer = Customer.fromJson(jsonDecode(responseBody)); - Auth().afterRegistration(customer); + await Auth().afterLogin(customer); } on FormatException catch(exception) { throw new Exception(responseBody); } @@ -66,10 +71,17 @@ class CustomerApi { Future getCustomer(int customerId) async { String body = ""; print(" ===== get the customer by id: " + customerId.toString() ); - final String responseBody = await _client.get( - "customers/"+customerId.toString(), - body); - Customer customer = Customer.fromJson(jsonDecode(responseBody)); - Auth().afterRegistration(customer); + try { + final String responseBody = await _client.get( + "customers/"+customerId.toString(), + body); + Customer customer = Customer.fromJson(jsonDecode(responseBody)); + Auth().afterRegistration(customer); + } catch (exception) { + print ("Exception: " + exception.toString()); + print (" === go to registration "); + Auth().logout(); + Auth().startPage = "registration"; + } } } \ No newline at end of file diff --git a/lib/service/exercise_tree_service.dart b/lib/service/exercise_tree_service.dart new file mode 100644 index 0000000..943c1b1 --- /dev/null +++ b/lib/service/exercise_tree_service.dart @@ -0,0 +1,19 @@ +import 'dart:convert'; + +import 'package:aitrainer_app/model/auth.dart'; +import 'package:aitrainer_app/model/exercise_tree.dart'; +import 'api.dart'; + +class ExerciseTreeApi { + final APIClient _client = new APIClient(); + + Future> getExerciseTree() async { + final body = await _client.get("exercise_tree", ""); + final Iterable json = jsonDecode(body); + final List exerciseTree = json.map((exerciseTree) => + ExerciseTree.fromJson(exerciseTree)).toList(); + Auth().setExerciseTree(exerciseTree); + return exerciseTree; + } + +} diff --git a/lib/service/exercisetype_service.dart b/lib/service/exercisetype_service.dart index d7178e4..997839d 100644 --- a/lib/service/exercisetype_service.dart +++ b/lib/service/exercisetype_service.dart @@ -7,8 +7,8 @@ import 'package:aitrainer_app/service/api.dart'; class ExerciseTypeApi { final APIClient _client=new APIClient(); - Future> getExerciseTypes(String param) async { - final body = await _client.get("exercise_type", param); + Future> getExerciseTypes() async { + final body = await _client.get("exercise_type/active", ""); final Iterable json = jsonDecode(body); final List exerciseTypes = json.map( (exerciseType) => ExerciseType.fromJson(exerciseType) ).toList(); Auth().setExerciseTypes(exerciseTypes); diff --git a/lib/util/common.dart b/lib/util/common.dart index e1ec8fd..12c664c 100644 --- a/lib/util/common.dart +++ b/lib/util/common.dart @@ -3,10 +3,16 @@ import 'dart:convert'; import 'package:aitrainer_app/localization/app_language.dart'; import 'package:aitrainer_app/model/auth.dart'; import 'package:aitrainer_app/model/exercise_type.dart'; +import 'package:aitrainer_app/repository/user_repository.dart'; +import 'package:flutter/cupertino.dart'; import 'package:intl/intl.dart'; class Common { + static const EMAIL_ERROR = "Please type a right email address here."; + static const PASSWORD_ERROR = "The password must have at least 8 characters."; + + static String toJson( Map map ) { String rc = "{"; @@ -47,4 +53,26 @@ class Common { List bytes = text.toString().codeUnits; return utf8.decode(bytes); } + + static double mediaSizeWidth( BuildContext context ) { + return MediaQuery.of(context).size.width; + } + + static bool validateEmail(UserRepository userRepository) { + final String email = userRepository.user.email; + final RegExp _emailRegExp = RegExp( + r'^[a-zA-Z0-9._-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$', + ); + return _emailRegExp.hasMatch(email); + } + + static bool validatePassword(UserRepository userRepository) { + final password = userRepository.user.password; + final RegExp _passwordRegExp = + RegExp(r'^(?=.*[A-Za-z0-9])(?=.*\d)[A-Za-z\d]{7,}$'); + + return _passwordRegExp.hasMatch(password); + } + + } \ No newline at end of file diff --git a/lib/util/loading_screen.dart b/lib/util/loading_screen.dart deleted file mode 100644 index 619d04f..0000000 --- a/lib/util/loading_screen.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:aitrainer_app/util/loading_screen_state.dart'; -import 'package:flutter/material.dart'; - - -/// Loading Screen Widget that updates the screen once all inistializer methods -/// are called -// ignore: must_be_immutable -class LoadingScreen extends StatefulWidget { - /// List of methods that are called once the Loading Screen is rendered - /// for the first time. These are the methods that can update the messages - /// that are shown under the loading symbol - final List initializers; - - /// The name of the application that is shown at the top of the loading screen - RichText title = RichText(text: TextSpan(text: 'AI Trainer')); - //final Text title; - - /// The background colour which is used as a filler when the image doesn't - /// occupy the full screen - final Color backgroundColor; - - /// The styling that is used with the text (messages) that are displayed under - /// the loader symbol - final TextStyle styleTextUnderTheLoader; - - /// The Layout/Scaffold Widget that is loaded once all the initializer methods - /// have been executed - final dynamic navigateToWidget; - - /// The colour that is used for the loader symbol - final Color loaderColor; - - /// The image widget that is used as a background cover to the loading screen - final Image image; - - /// The message that is displayed on the first load of the widget - final String initialMessage; - - /// Constructor for the LoadingScreen widget with all the required - /// initializers - LoadingScreen( - {this.initializers, - this.navigateToWidget, - this.loaderColor, - this.image, - //this.title = Text("Welcome"), - this.backgroundColor = Colors.white, - this.styleTextUnderTheLoader = const TextStyle( - fontSize: 18.0, fontWeight: FontWeight.bold, color: Colors.black), - this.initialMessage}) - // The Widget depends on the initializers and navigateToWidget to have a - // valid value. Thus we assert that the values passed are valid and - // not null - : assert(initializers != null && initializers.length > 0), - assert(navigateToWidget != null); - - /// Bind the Widget to the custom State object - @override - LoadingScreenState createState() => LoadingScreenState(); -} - diff --git a/lib/util/loading_screen_state.dart b/lib/util/loading_screen_state.dart deleted file mode 100644 index 3ce3534..0000000 --- a/lib/util/loading_screen_state.dart +++ /dev/null @@ -1,129 +0,0 @@ -import 'dart:core'; -import 'dart:async'; -import 'package:aitrainer_app/util/loading_screen.dart'; -import 'package:aitrainer_app/widgets/home.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:aitrainer_app/util/message_state.dart'; - -/// The custom state that is used by the Loading Screen widget to handle the -/// messages that are provided by the initializer methods. -/// -/// Note: Although the class is not exported from the package as not required by -/// the implementers using the package, the protected metatag is added to make -/// the code clearer. -@protected -class LoadingScreenState extends MessageState { - - /// Initialise the state - @override - void initState() { - super.initState(); - - /// If the LoadingScreen widget has an initial message set, then the default - /// message in the MessageState class needs to be updated - if (widget.initialMessage != null) { - initialMessage = widget.initialMessage; - } - - /// We require the initializers to run after the loading screen is rendered - SchedulerBinding.instance.addPostFrameCallback((_) { - runInitTasks(); - }); - } - - /// This method calls the initializers and once they complete redirects to - /// the widget provided in navigateAfterInit - @protected - Future runInitTasks() async { - print(" ----- runInitTasks"); - /// Run each initializer method sequentially - Future.forEach(widget.initializers, (init) => init(this, callbackFunction)).whenComplete(() { - // When all the initializers has been called and terminated their - // execution. The screen is navigated to the next scaffolding widget - if (widget.navigateToWidget is String) { - // It's fairly safe to assume this is using the in-built material - // named route component - print(" ----- navigate to " + widget.navigateToWidget); - Navigator.of(context).pushReplacementNamed(widget.navigateToWidget); - } else if (widget.navigateToWidget is Widget) { - Navigator.of(context).pushReplacement(new MaterialPageRoute( - builder: (BuildContext context) => widget.navigateToWidget)); - print(" ----- navigate to main "); - - - } else { - throw new ArgumentError( - 'widget.navigateAfterSeconds must either be a String or Widget'); - } - }); - } - - void callbackFunction() { - print("Call Home callback if widget"); - if (widget.navigateToWidget is Widget) { - AitrainerHome home = widget.navigateToWidget as AitrainerHome; - home.callback(); - } - } - - /// Render the LoadingScreen widget - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: widget.backgroundColor, - body: new InkWell( - child: new Stack( - fit: StackFit.expand, - children: [ - /// Paint the area where the inner widgets are loaded with the - /// background to keep consistency with the screen background - new Container( - decoration: BoxDecoration(color: widget.backgroundColor), - ), - /// Render the background image - new Container( - child: widget.image, - ), - /// Render the Title widget, loader and messages below each other - new Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - new Expanded( - flex: 3, - child: new Container( - child: new Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - new Padding( - padding: const EdgeInsets.only(top: 30.0), - ), - widget.title, - ], - )), - ), - Expanded( - flex: 1, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - /// Loader Animation Widget - CircularProgressIndicator( - valueColor: new AlwaysStoppedAnimation( - widget.loaderColor), - ), - Padding( - padding: const EdgeInsets.only(top: 20.0), - ), - Text(getMessage, style: widget.styleTextUnderTheLoader), - ], - ), - ), - ], - ), - ], - ), - ), - ); - } -} diff --git a/lib/util/menu_tests.dart b/lib/util/menu_tests.dart deleted file mode 100644 index c53e126..0000000 --- a/lib/util/menu_tests.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'dart:collection'; -import 'package:aitrainer_app/localization/app_localization.dart'; -import 'package:aitrainer_app/model/workout_tree.dart'; -import 'package:flutter/material.dart'; - -class MenuTests { - LinkedHashMap tree = LinkedHashMap(); - - MenuTests(BuildContext context) { - this.tree['Cardio']= WorkoutTree(1, 0, AppLocalizations.of(context).translate("Cardio"), - 'asset/menu/1.cardio.png', - Colors.white, 32, false,0); - this.tree['Aerobic']= WorkoutTree(2, 1, AppLocalizations.of(context).translate("Aerobic"), - 'asset/menu/1.1.aerob.png', - Colors.white, 32, false,0); - this.tree['Cooper']= WorkoutTree(21, 2, AppLocalizations.of(context).translate("Cooper"), - 'asset/menu/1.1.1.cooper.png', - Colors.white, 32, true,30); - this.tree['Anaerobic']= WorkoutTree(3, 1, AppLocalizations.of(context).translate("Anaerobic"), - 'asset/menu/1.2.anaerob.png', - Colors.white, 32, false,0); - this.tree['300m']= WorkoutTree(22, 3, "300m", - 'asset/menu/1.2.1.300m.png', - Colors.white, 32, true,31); - this.tree['400m']= WorkoutTree(24, 3, "400m", - 'asset/menu/1.2.2.400m.png', - Colors.white, 32, true,32); - - this.tree['Strength']= WorkoutTree(4, 0, AppLocalizations.of(context).translate("Strength"), - 'asset/menu/2.strength.png', - Colors.white, 32, false,0); - this.tree['Endurance']= WorkoutTree(5, 4, AppLocalizations.of(context).translate("Endurance"), - 'asset/menu/2.1.endurance.png', - Colors.white, 36, false,0); - this.tree['Pullups']= WorkoutTree(6, 5, AppLocalizations.of(context).translate("Pull Ups"), - 'asset/menu/2.1.1.pull-ups.png', - Colors.white, 32, true,38); - this.tree['Pushups']= WorkoutTree(7, 5, AppLocalizations.of(context).translate("Pushups"), - 'asset/menu/2.1.2.pushup.png', - Colors.white, 32, true,33); - this.tree['Situps']= WorkoutTree(10, 5, AppLocalizations.of(context).translate("Sit-ups"), - 'asset/menu/2.1.3.sit-ups.png', - Colors.white, 32, true,36); - this.tree['Squats']= WorkoutTree(11, 5, AppLocalizations.of(context).translate("Squats"), - 'asset/menu/2.1.4.squats.png', - Colors.white, 32, true,35); - this.tree['TimedPushups']= WorkoutTree(12, 5, AppLocalizations.of(context).translate("Timed Pushups"), - 'asset/menu/2.1.5.timedpushup.png', - Colors.white, 32, true,34); - this.tree['Core']= WorkoutTree(43, 5, AppLocalizations.of(context).translate("Core"), - 'asset/menu/2.1.6.core.png', - Colors.white, 32, true,45); - - this.tree['1RM']= WorkoutTree(8, 4, AppLocalizations.of(context).translate("1RM"), - 'asset/menu/2.2.1.1RM.png', - Colors.white, 32, false,0); - this.tree['Chestpress']= WorkoutTree(13, 8, AppLocalizations.of(context).translate("Chest Press"), - 'asset/menu/2.2.1.1.chestpress.png', - Colors.white, 32, true,37); - this.tree['PullUps1rm']= WorkoutTree(14, 8, AppLocalizations.of(context).translate("Pull Ups"), - 'asset/menu/2.2.1.2.pullups.png', - Colors.white, 32, true, 38); - this.tree['Biceps']= WorkoutTree(15, 8, AppLocalizations.of(context).translate("Biceps"), - 'asset/menu/2.2.1.3.biceps.png', - Colors.white, 32, true, 39); - this.tree['Triceps']= WorkoutTree(16, 8, AppLocalizations.of(context).translate("Triceps"), - 'asset/menu/2.2.1.4.triceps.png', - Colors.white, 32, true, 40); - this.tree['Shoulders']= WorkoutTree(17, 8, AppLocalizations.of(context).translate("Shoulders"), - 'asset/menu/2.2.1.5.shoulders.png', - Colors.white, 32, true, 41); - - this.tree['BodyCompositions']= WorkoutTree(9, 0, AppLocalizations.of(context).translate("Body Compositions"), - 'asset/menu/3.bcs1.png', - Colors.white, 32, false,0); - this.tree['BMI']= WorkoutTree(18, 9, AppLocalizations.of(context).translate("BMI"), - 'asset/menu/3.1.BMI.png', - Colors.white, 32, true,42); - this.tree['BMR']= WorkoutTree(19, 9, AppLocalizations.of(context).translate("BMR"), - 'asset/menu/3.2.BMR.png', - Colors.white, 32, true, 43); - this.tree['Sizes']= WorkoutTree(20, 9, AppLocalizations.of(context).translate("Sizes"), - 'asset/menu/3.3.sizes.png', - Colors.white, 32, true, 44); - - } - - LinkedHashMap getMenuItems() { - return this.tree; - } - - -} \ No newline at end of file diff --git a/lib/util/message_state.dart b/lib/util/message_state.dart deleted file mode 100644 index 2f83923..0000000 --- a/lib/util/message_state.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:flutter/widgets.dart'; - -/// An extension class to the Flutter standard State. The class provides getter -/// and setters for updating the message section of the loading screen -/// -/// Note: The class is marked as abstract to avoid IDE issues that expects -/// protected methods to be overloaded -abstract class MessageState extends State { - /// The state variable that will hold the latest message that needs to be - /// displayed. - /// - /// Note: Although Flutter standard allow member variables to be used from - /// instance object reference, this is not a best practice with OOP. OOP - /// design proposes that member variables should be accessed through getter - /// and setter methods. - @protected - String _message = 'Loading . . .'; - - /// The member variable is set as protected this it is not exposed to the - /// widget state class. As a workaround a protected setter is set so it is - /// not used outside the package - @protected - set initialMessage(String message) => _message = message; - - /// Setter for the message variable - set setMessage(String message) => setState(() { - _message = message; - }); - - /// Getter for the message variable - String get getMessage => _message; -} diff --git a/lib/util/session.dart b/lib/util/session.dart index 880a5b8..f62a256 100644 --- a/lib/util/session.dart +++ b/lib/util/session.dart @@ -1,6 +1,8 @@ import 'package:aitrainer_app/localization/app_language.dart'; +import 'package:aitrainer_app/localization/app_localization.dart'; import 'package:aitrainer_app/service/api.dart'; import 'package:aitrainer_app/service/customer_service.dart'; +import 'package:aitrainer_app/service/exercise_tree_service.dart'; import 'package:aitrainer_app/service/exercisetype_service.dart'; import 'package:devicelocale/devicelocale.dart'; import 'package:flutter/services.dart'; @@ -12,18 +14,24 @@ import '../push_notifications.dart'; class Session { Future _prefs = SharedPreferences.getInstance(); - Auth _auth = Auth(); + SharedPreferences _sharedPreferences; final AppLanguage appLanguage = AppLanguage(); - fetchSessionAndNavigate(Function callback ) async { + fetchSessionAndNavigate( ) async { + print (" -- Session: await prefs.."); _sharedPreferences = await _prefs; - if ( _auth.firstLoad ) { - _fetchToken(_sharedPreferences, callback); + if ( Auth().firstLoad ) { + + print (" -- Session: fetch locale.."); + await appLanguage.fetchLocale(); + await AppLocalizations.delegate.load(appLanguage.appLocal); + print (" -- Session: fetch token.."); + await _fetchToken(_sharedPreferences); initDeviceLocale(); - appLanguage.fetchLocale(); + PushNotificationsManager().init(); } @@ -53,7 +61,7 @@ class Session { /* Auth flow of the user, see auth.dart */ - _fetchToken(SharedPreferences prefs, Function callback) async { + _fetchToken(SharedPreferences prefs) async { var responseJson = await APIClient.authenticateUser( Auth.username, @@ -71,6 +79,7 @@ class Session { // registration //Navigator.of(context).pushNamed('registration'); prefs.setBool(Auth.isRegisteredKey, true); + Auth().startPage = "registration"; } else { DateTime now = DateTime.now(); DateTime lastStoreDate = DateTime.parse( @@ -83,16 +92,17 @@ class Session { prefs.get(Auth.isLoggedInKey) == false) { print("************* Login"); //Navigator.of(context).pushNamed('login'); - + Auth().startPage = "login"; } else { print("************** Store SharedPreferences"); // get API customer await CustomerApi().getCustomer(prefs.getInt(Auth.customerIdKey)); + Auth().startPage = "home"; } - await ExerciseTypeApi().getExerciseTypes(""); - print("--- Session finished, call callback "); - callback(); + await ExerciseTypeApi().getExerciseTypes(); + await ExerciseTreeApi().getExerciseTree(); + print("--- Session finished"); } } diff --git a/lib/view/account.dart b/lib/view/account.dart index 46039d9..e695569 100644 --- a/lib/view/account.dart +++ b/lib/view/account.dart @@ -1,142 +1,110 @@ -import 'package:aitrainer_app/localization/app_language.dart'; +import 'package:aitrainer_app/bloc/account/account_bloc.dart'; import 'package:aitrainer_app/localization/app_localization.dart'; -import 'package:aitrainer_app/model/auth.dart'; -import 'package:aitrainer_app/util/common.dart'; -import 'package:aitrainer_app/viewmodel/customer_changing_view_model.dart'; -import 'package:aitrainer_app/viewmodel/customer_view_model.dart'; -import 'package:aitrainer_app/viewmodel/exercise_changing_view_model.dart'; -import 'package:aitrainer_app/viewmodel/exercise_view_model.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:aitrainer_app/viewmodel/user_view_model.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:aitrainer_app/widgets/bottom_nav.dart'; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; -import 'package:provider/provider.dart'; - -class AccountPage extends StatefulWidget{ - _AccountPagePageState _state; - - _AccountPagePageState createState() { - _state = new _AccountPagePageState(); - return _state; - } - - State getState() { - return _state; - } -} - -class _AccountPagePageState extends State { - final UserViewModel user = UserViewModel(); - final AppLanguage appLanguage = AppLanguage(); - final Future _prefs = SharedPreferences.getInstance(); - final BottomNavigator bottomNav = BottomNavigator(); - Future> _exercises; - ExerciseChangingViewModel exerciseChangingViewModel; - - - - @override - void initState() { - exerciseChangingViewModel = Provider.of(context, listen: false); - super.initState(); - } +// ignore: must_be_immutable +class AccountPage extends StatelessWidget { + // ignore: close_sinks + AccountBloc accountBloc; @override Widget build(BuildContext context) { - return Consumer( - builder: (context, model, child ) { - if ( model.customer == null ) { - CustomerViewModel customerViewModel = CustomerViewModel(); - model.customer = customerViewModel; - if ( model.customer.getCustomer() == null ) { - model.customer.setCustomer(Auth().userLoggedIn); + accountBloc = BlocProvider.of(context); + return Scaffold( + appBar: AppBar( + title: Text(AppLocalizations.of(context).translate('Account')), + backgroundColor: Colors.transparent, + ), + body: Container( + foregroundDecoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('asset/image/WT_long_logo.png'), + alignment: Alignment.topRight, + ), + ), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('asset/image/WT_light_background.png'), + fit: BoxFit.cover, + alignment: Alignment.center, + ), + ), + child: BlocConsumer( + listener: (context, state) { + if (state is AccountError) { + Scaffold.of(context).showSnackBar(SnackBar( + backgroundColor: Colors.orange, + content: + Text(state.message, style: TextStyle(color: Colors.white)))); + } else if (state is AccountLoading) { + + } + }, + builder: (context, state) { + if ( state is AccountInitial ) { + String customerName = accountBloc.customerRepository.firstName + + " " + accountBloc.customerRepository.name; + return accountWidget(context, customerName, accountBloc); + } else if ( state is AccountLoggedIn ) { + String customerName = accountBloc.customerRepository.firstName + + " " + accountBloc.customerRepository.name; + return accountWidget(context, customerName, accountBloc); + } else if ( state is AccountLoggedOut ) { + String customerName = ""; + return accountWidget(context, customerName, accountBloc); + } else if ( state is AccountReady ) { + String customerName = accountBloc.customerRepository.firstName + + " " + accountBloc.customerRepository.name; + return accountWidget(context, customerName, accountBloc); + } else { + return accountWidget(context, "", accountBloc); + } + } - } - - if ( Auth().userLoggedIn != null ) { - _exercises = - exerciseChangingViewModel.getExercisesByCustomer( - Auth().userLoggedIn.customerId); - } - - - return Scaffold( - appBar: AppBar( - title: Text(AppLocalizations.of(context).translate('Account')), - backgroundColor: Colors.transparent, ), - body: Container( - foregroundDecoration: BoxDecoration( - image: DecorationImage( - image: AssetImage('asset/image/WT_long_logo.png'), - alignment: Alignment.topRight, - ), - ), - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage('asset/image/WT_light_background.png'), - fit: BoxFit.cover, - alignment: Alignment.center, - ), - ), - child: - ListView( - padding: EdgeInsets.only(top: 135), - children: [ - ListTile( - leading: Icon(Icons.perm_identity), - subtitle: Text( - AppLocalizations.of(context).translate("Profile")), - title: FlatButton( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(Auth().userLoggedIn != null ? - Auth().userLoggedIn.name + " " + - Auth().userLoggedIn.firstname : "", - style: TextStyle(color: Colors.blue)), - Icon(Icons.arrow_forward_ios), - ]), - textColor: Colors.grey, - color: Colors.white, - onPressed: () => { - if (model.customer.getCustomer() != null) { - Navigator.of(context).pushNamed( - 'customerModifyPage'), - print("Profile"), - } - }, - ), - - ), - ListTile( - leading: Icon(Icons.language), - title: Text(appLanguage.appLocal == Locale('en') ? - AppLocalizations.of(context).translate("English") : - AppLocalizations.of(context).translate("Hungarian")), - subtitle: Text(AppLocalizations.of(context).translate( - "Selected Language")), - ), - loginOut( model ), - exercises(exerciseChangingViewModel), - ] - ) - ), - bottomNavigationBar: bottomNav.buildBottomNavigator( - context, widget._state) - ); - }); + ), + bottomNavigationBar: BottomNavigator(bottomNavIndex: 2)); } - ListTile loginOut( CustomerChangingViewModel model ) { + ListView accountWidget(BuildContext context, String customerName, AccountBloc accountBloc) { + return ListView(padding: EdgeInsets.only(top: 135), children: [ + ListTile( + leading: Icon(Icons.perm_identity), + subtitle: + Text(AppLocalizations.of(context).translate("Profile")), + title: FlatButton( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(customerName, + style: TextStyle(color: Colors.blue)), + Icon(Icons.arrow_forward_ios), + ]), + textColor: Colors.grey, + color: Colors.white, + onPressed: () => { + if (accountBloc.customerRepository.customer != null) { + Navigator.of(context).pushNamed('customerModifyPage'), + print("Profile"), + } + }, + ), + ), + loginOut( context, accountBloc ), + //exercises(exerciseChangingViewModel), + ]); + } + + ListTile loginOut( BuildContext context, AccountBloc accountBloc ) { ListTile element = ListTile(); String text = "Logout"; Color buttonColor = Colors.orange; - if ( model.customer.getCustomer() == null ) { + if ( accountBloc.customerRepository.customer == null || accountBloc.customerRepository.customer.email == null) { text = "Login"; buttonColor = Colors.blue; } @@ -149,25 +117,20 @@ class _AccountPagePageState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(AppLocalizations.of(context).translate(text), - style: TextStyle( - color: buttonColor - )), + style: TextStyle( + color: buttonColor + )), Icon(Icons.arrow_forward_ios), ]), textColor: buttonColor, color: Colors.white, onPressed: () => { - setState(() { - if ( model.customer.getCustomer() == null ) { - print("Login"); - Navigator.of(context).pushNamed("login", arguments: widget._state); - } else { - print("Logout"); - Auth().logout(); - model.customer.setCustomer(null); - } - - }) + if ( accountBloc.loggedIn ) { + accountBloc.add(AccountLogout()) + } else { + accountBloc.add(AccountLogin()), + Navigator.of(context).pushNamed('login'), + } }, ), ); @@ -175,7 +138,7 @@ class _AccountPagePageState extends State { return element; } - ListTile exercises( ExerciseChangingViewModel model ) { + /* ListTile exercises( ExerciseChangingViewModel model ) { ListTile element = ListTile(); if ( Auth().userLoggedIn == null ) { return element; @@ -202,7 +165,8 @@ class _AccountPagePageState extends State { return element; } - +*/ + /* Widget getExercises( ExerciseChangingViewModel model ) { List exercises = model.exerciseList; @@ -262,5 +226,5 @@ class _AccountPagePageState extends State { return element; - } -} \ No newline at end of file + } */ +} diff --git a/lib/view/custom_exercise_page.dart b/lib/view/custom_exercise_page.dart new file mode 100644 index 0000000..820f8eb --- /dev/null +++ b/lib/view/custom_exercise_page.dart @@ -0,0 +1,346 @@ +import 'package:aitrainer_app/bloc/custom_exercise_form_bloc.dart'; +import 'package:aitrainer_app/localization/app_localization.dart'; +import 'package:aitrainer_app/model/exercise_type.dart'; +import 'package:aitrainer_app/repository/exercise_repository.dart'; +import 'package:aitrainer_app/widgets/splash.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_form_bloc/flutter_form_bloc.dart'; + +class CustomExercisePage extends StatefulWidget { + _CustomExerciseNewPageState createState() => _CustomExerciseNewPageState(); +} + +class _CustomExerciseNewPageState extends State { + final GlobalKey _scaffoldKey = new GlobalKey(); + + @override + Widget build(BuildContext context) { + final ExerciseType exerciseType = ModalRoute.of(context).settings.arguments; + + return BlocProvider( + create: (context) => + CustomExerciseFormBloc(exerciseRepository: ExerciseRepository()), + child: Builder(builder: (context) { + // ignore: close_sinks + final exerciseBloc = BlocProvider.of(context); + exerciseBloc.exerciseRepository.setExerciseType(exerciseType); + + return Scaffold( + key: _scaffoldKey, + resizeToAvoidBottomInset: true, + appBar: AppBar( + backgroundColor: Colors.transparent, + title: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Image.asset( + 'asset/image/WT_long_logo.png', + fit: BoxFit.cover, + height: 65.0, + ), + ], + ), + leading: IconButton( + icon: Icon(Icons.arrow_back, color: Colors.white), + onPressed: () => Navigator.of(context).pop(), + ), + ), + body: FormBlocListener( + onSubmitting: (context, state) { + LoadingDialog.show(context); + }, + onSuccess: (context, state) { + LoadingDialog.hide(context); + }, + onFailure: (context, state) { + LoadingDialog.hide(context); + Scaffold.of(context).showSnackBar(SnackBar( + backgroundColor: Colors.orange, + content: Text(state.failureResponse, + style: TextStyle(color: Colors.white)))); + }, + child: Container( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + decoration: BoxDecoration( + image: DecorationImage( + image: + AssetImage('asset/image/WT_light_background.png'), + fit: BoxFit.fill, + alignment: Alignment.center, + ), + ), + child: CustomScrollView( + scrollDirection: Axis.vertical, + slivers: [ + SliverList( + delegate: SliverChildListDelegate( + [ + Container( + padding: EdgeInsets.only(top:20,left:25, right:25), + alignment: Alignment.center, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text("Custom Exercise", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: Colors.deepOrange)), + columnQuantityUnit(exerciseBloc), + columnQuantity(exerciseBloc), + ] + ) + ), + ] + ), + + ), + gridCalculation(exerciseBloc) + ] + ) + ) + ) + ); + })); + } + + Column columnQuantityUnit(CustomExerciseFormBloc bloc) { + Column column = Column(); + if (bloc.exerciseRepository.exerciseType != null && + bloc.exerciseRepository.exerciseType.unitQuantity == "1") { + column = Column(children: [ + TextFieldBlocBuilder( + textFieldBloc: bloc.unitQuantityField, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + color: Colors.lightBlue, + fontWeight: FontWeight.bold), + inputFormatters: [ + FilteringTextInputFormatter(RegExp(r"[\d.]"), allow: true) + ], + onChanged: (input) => { + print("UnitQuantity value $input"), + bloc.exerciseRepository.setUnitQuantity(double.parse(input)) + }, + decoration: InputDecoration( + fillColor: Colors.white, + filled: false, + hintStyle: TextStyle( + fontSize: 12, + color: Colors.black54, + fontWeight: FontWeight.w100), + hintText: AppLocalizations.of(context) + .translate("The number of the exercise done with"), + labelStyle: TextStyle(fontSize: 12, color: Colors.lightBlue), + labelText: AppLocalizations.of(context).translate( + bloc.exerciseRepository.exerciseType.unitQuantityUnit), + ), + ), + new InkWell( + child: new Text( + AppLocalizations.of(context).translate( + bloc.exerciseRepository.exerciseType.unitQuantityUnit), + style: TextStyle(fontSize: 12)), + ), + ]); + } + ; + return column; + } + + Column columnQuantity(CustomExerciseFormBloc bloc) { + Column column = Column(children: [ + TextFieldBlocBuilder( + textFieldBloc: bloc.quantityField, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 20, + color: Colors.deepOrange, + fontWeight: FontWeight.bold), + inputFormatters: [ + FilteringTextInputFormatter(RegExp(r"[\d.]"), allow: true) + ], + onChanged: (input) => { + print("Quantity value $input"), + bloc.exerciseRepository.setQuantity(double.parse(input)), + bloc.exerciseRepository + .setUnit(bloc.exerciseRepository.exerciseType.unit) + }, + decoration: InputDecoration( + fillColor: Colors.white, + filled: false, + hintStyle: TextStyle( + fontSize: 12, color: Colors.black54, fontWeight: FontWeight.w100), + hintText: AppLocalizations.of(context) + .translate("The number of the exercise"), + labelStyle: TextStyle(fontSize: 12, color: Colors.deepOrange), + labelText: AppLocalizations.of(context) + .translate(bloc.exerciseRepository.exerciseType.unit), + ), + ), + ]); + + return column; + } + + SliverGrid gridCalculation(CustomExerciseFormBloc bloc) { + return SliverGrid( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 10.0, + crossAxisSpacing: 10.0, + childAspectRatio: 4.0, + ), + delegate: SliverChildListDelegate( + [ + TextFieldBlocBuilder( + isEnabled: false, + textFieldBloc: bloc.rmWendlerField, + padding: EdgeInsets.only(left:30), + style: TextStyle(color: Colors.deepOrange, fontSize: 12), + decoration: InputDecoration( + border: InputBorder.none, + fillColor: Colors.white, + filled: false, + labelText: "1RM by Wendler: ", + )), + TextFieldBlocBuilder( + isEnabled: false, + padding: EdgeInsets.only(left:30), + textFieldBloc: bloc.rmMcGothlinField, + style: TextStyle(color: Colors.deepOrange, fontSize: 12), + decoration: InputDecoration( + border: InputBorder.none, + fillColor: Colors.white, + filled: false, + labelText: "1RM by McGlothin: ", + )), + TextFieldBlocBuilder( + isEnabled: false, + padding: EdgeInsets.only(left:30), + maxLines: 1, + textFieldBloc: bloc.rmLombardiField, + style: TextStyle(color: Colors.deepOrange, fontSize: 12), + decoration: InputDecoration( + border: InputBorder.none, + fillColor: Colors.white, + filled: false, + labelText: "1RM by Lambordini: ", + )), + TextFieldBlocBuilder( + isEnabled: false, + padding: EdgeInsets.only(left:30), + maxLines: 1, + textFieldBloc: bloc.rmWathenField, + style: TextStyle(color: Colors.deepOrange, fontSize: 12), + decoration: InputDecoration( + border: InputBorder.none, + fillColor: Colors.white, + filled: false, + labelText: "1RM by Wahten: ", + )), + TextFieldBlocBuilder( + isEnabled: false, + padding: EdgeInsets.only(left:30), + maxLines: 1, + textFieldBloc: bloc.rmOconnerField, + style: TextStyle(color: Colors.deepOrange, fontSize: 12), + decoration: InputDecoration( + border: InputBorder.none, + fillColor: Colors.white, + filled: false, + labelText: "1RM by O'Conner: ", + )), + TextFieldBlocBuilder( + isEnabled: false, + padding: EdgeInsets.only(left:30), + maxLines: 1, + textFieldBloc: bloc.rmMayhewField, + style: TextStyle(color: Colors.deepOrange, fontSize: 12), + decoration: InputDecoration( + border: InputBorder.none, + fillColor: Colors.white, + filled: false, + labelText: "1RM by Mayhew: ", + )), + TextFieldBlocBuilder( + isEnabled: false, + padding: EdgeInsets.only(left:30), + maxLines: 1, + textFieldBloc: bloc.rmAverageField, + style: TextStyle(color: Colors.blueAccent, fontSize: 12), + decoration: InputDecoration( + border: InputBorder.none, + fillColor: Colors.white, + filled: false, + labelText: "1RM Average: ", + )), + TextFieldBlocBuilder( + isEnabled: false, + padding: EdgeInsets.only(left:30), + maxLines: 1, + textFieldBloc: bloc.rm90Field, + style: TextStyle(color: Colors.deepOrange, fontSize: 12), + decoration: InputDecoration( + border: InputBorder.none, + fillColor: Colors.white, + filled: false, + labelText: "1RM 90%: ", + )), + TextFieldBlocBuilder( + isEnabled: false, + padding: EdgeInsets.only(left:30), + maxLines: 1, + textFieldBloc: bloc.rm80Field, + style: TextStyle(color: Colors.deepOrange, fontSize: 12), + decoration: InputDecoration( + border: InputBorder.none, + fillColor: Colors.white, + filled: false, + labelText: "1RM 80%: ", + )), + TextFieldBlocBuilder( + isEnabled: false, + padding: EdgeInsets.only(left:30), + maxLines: 1, + textFieldBloc: bloc.rm70Field, + style: TextStyle(color: Colors.deepOrange, fontSize: 12), + decoration: InputDecoration( + border: InputBorder.none, + fillColor: Colors.white, + filled: false, + labelText: "1RM 70%: ", + )), + TextFieldBlocBuilder( + isEnabled: false, + padding: EdgeInsets.only(left:30), + maxLines: 1, + textFieldBloc: bloc.rm60Field, + style: TextStyle(color: Colors.deepOrange, fontSize: 12), + decoration: InputDecoration( + border: InputBorder.none, + fillColor: Colors.white, + filled: false, + labelText: "1RM 60%: ", + )), + TextFieldBlocBuilder( + isEnabled: false, + padding: EdgeInsets.only(left:30), + maxLines: 1, + textFieldBloc: bloc.rm50Field, + style: TextStyle(color: Colors.deepOrange, fontSize: 12), + decoration: InputDecoration( + border: InputBorder.none, + fillColor: Colors.white, + filled: false, + labelText: "1RM 50%: ", + )) + ]) + ); + } +} diff --git a/lib/view/customer_bodytype_page.dart b/lib/view/customer_bodytype_page.dart index a3dcba8..70c8f0f 100644 --- a/lib/view/customer_bodytype_page.dart +++ b/lib/view/customer_bodytype_page.dart @@ -1,9 +1,11 @@ +import 'package:aitrainer_app/bloc/customer_change/customer_change_bloc.dart'; import 'package:aitrainer_app/localization/app_localization.dart'; -import 'package:aitrainer_app/viewmodel/customer_changing_view_model.dart'; +import 'package:aitrainer_app/repository/customer_repository.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; // ignore: must_be_immutable -class CustomerBodyTypePage extends StatefulWidget{ +class CustomerBodyTypePage extends StatefulWidget { _CustomerBodyTypePageState _state; _CustomerBodyTypePageState createState() { @@ -13,7 +15,6 @@ class CustomerBodyTypePage extends StatefulWidget{ } class BodyTypeItem { - static String endomorph = "endomorph"; static String ectomorph = "ectomorph"; static String mesomorph = "mesomorph"; @@ -23,15 +24,15 @@ class _CustomerBodyTypePageState extends State { String selected; @override Widget build(BuildContext context) { - final CustomerChangingViewModel changingViewModel = ModalRoute.of(context).settings.arguments; - final double cWidth = MediaQuery.of(context).size.width*0.75; + final CustomerRepository customerRepository = + ModalRoute.of(context).settings.arguments; + final double cWidth = MediaQuery.of(context).size.width * 0.75; return Scaffold( appBar: AppBar( title: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - Image.asset( 'asset/image/WT_long_logo.png', fit: BoxFit.cover, @@ -40,151 +41,160 @@ class _CustomerBodyTypePageState extends State { ], ), backgroundColor: Colors.transparent, - ), - body: Container( - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage('asset/image/WT_light_background.png'), - fit: BoxFit.cover, - alignment: Alignment.center, - ), + ), + body: Container( + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('asset/image/WT_light_background.png'), + fit: BoxFit.cover, + alignment: Alignment.center, ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Divider(), - Wrap( - //runAlignment: WrapAlignment.center, - alignment: WrapAlignment.center, + ), + child: BlocProvider( + create: (context) => + CustomerChangeBloc(customerRepository: customerRepository), + child: Builder(builder: (context) { + // ignore: close_sinks + CustomerChangeBloc changeBloc = + BlocProvider.of(context); + + return Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - AppLocalizations.of(context).translate("Your Body Type"), - textAlign: TextAlign.center, - style: TextStyle(color: Colors.orange, - fontSize: 42, fontFamily: 'Arial', - fontWeight: FontWeight.w900 ),) - ] - ), - - - Divider(), - FlatButton( - child: Container( - width: cWidth, - child: Column( + Divider(), + Wrap( + //runAlignment: WrapAlignment.center, + alignment: WrapAlignment.center, children: [ - Text(AppLocalizations.of(context).translate("Endomorph"), - textWidthBasis: TextWidthBasis.longestLine, - style: TextStyle(color: Colors.blue, - fontSize: 32, fontFamily: 'Arial', - fontWeight: FontWeight.w900 )), - - ], - ) - ), - padding: EdgeInsets.all(10.0), - shape: getShape(changingViewModel, BodyTypeItem.endomorph ), - onPressed:() => - { - setState((){ - selected = BodyTypeItem.endomorph; - changingViewModel.customer.setBodyType(selected); - print(selected); - }), - - } - - ), - Divider(), - FlatButton( - child: Container( - width: cWidth, - child: Column( - children: [ - InkWell( - child: Text(AppLocalizations.of(context).translate("Ectomorph"), - style: TextStyle(color: Colors.blue, - fontSize: 32, fontFamily: 'Arial', - fontWeight: FontWeight.w900 ),), - highlightColor: Colors.white, + Text( + AppLocalizations.of(context) + .translate("Your Body Type"), + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.orange, + fontSize: 42, + fontFamily: 'Arial', + fontWeight: FontWeight.w900), + ) + ]), + Divider(), + FlatButton( + child: Container( + width: cWidth, + child: Column( + children: [ + Text( + AppLocalizations.of(context) + .translate("Endomorph"), + textWidthBasis: TextWidthBasis.longestLine, + style: TextStyle( + color: Colors.blue, + fontSize: 32, + fontFamily: 'Arial', + fontWeight: FontWeight.w900)), + ], + )), + padding: EdgeInsets.all(10.0), + shape: getShape( + customerRepository, BodyTypeItem.endomorph), + onPressed: () => { + setState(() { + selected = BodyTypeItem.endomorph; + changeBloc.add(CustomerBodyTypeChange(bodyType: selected)); + print(selected); + }), + }), + Divider(), + FlatButton( + child: Container( + width: cWidth, + child: Column( + children: [ + InkWell( + child: Text( + AppLocalizations.of(context) + .translate("Ectomorph"), + style: TextStyle( + color: Colors.blue, + fontSize: 32, + fontFamily: 'Arial', + fontWeight: FontWeight.w900), + ), + highlightColor: Colors.white, + ), + ], + ), ), + padding: EdgeInsets.all(10.0), + shape: getShape(customerRepository, BodyTypeItem.ectomorph ), - ], - ), - ), - padding: EdgeInsets.all(10.0), - shape: getShape(changingViewModel, BodyTypeItem.ectomorph ), - - onPressed:() => - { - setState((){ - selected = BodyTypeItem.ectomorph; - changingViewModel.customer.setBodyType(selected); - print(selected); - }), - - } - ), - Divider(), - FlatButton( - child: Container( - width: cWidth, - child: Column( - children: [ - InkWell( - child: Text(AppLocalizations.of(context).translate("Mesomorph"), - style: TextStyle(color: Colors.blue, - fontSize: 32, fontFamily: 'Arial', - fontWeight: FontWeight.w900 ),), - highlightColor: Colors.white, + onPressed: () => { + setState(() { + selected = BodyTypeItem.ectomorph; + changeBloc.add(CustomerBodyTypeChange(bodyType: selected)); + print(selected); + }), + }), + Divider(), + FlatButton( + child: Container( + width: cWidth, + child: Column( + children: [ + InkWell( + child: Text( + AppLocalizations.of(context) + .translate("Mesomorph"), + style: TextStyle( + color: Colors.blue, + fontSize: 32, + fontFamily: 'Arial', + fontWeight: FontWeight.w900), + ), + highlightColor: Colors.white, + ), + ], + ), ), + padding: EdgeInsets.all(10.0), + shape: getShape(customerRepository, BodyTypeItem.mesomorph ), + onPressed: () => { + setState(() { + selected = BodyTypeItem.mesomorph; + changeBloc.add(CustomerBodyTypeChange(bodyType: selected)); - ], - ), - ), - padding: EdgeInsets.all(10.0), - shape: getShape(changingViewModel, BodyTypeItem.mesomorph ), - onPressed:() => - { - setState((){ - selected = BodyTypeItem.mesomorph; - changingViewModel.customer.setBodyType(selected); - print(selected); - }), - - } - ), - - Divider(), - RaisedButton( - - color: Colors.orange, - textColor: Colors.white, - child: InkWell( - child: Text(AppLocalizations.of(context).translate("Next"))), - onPressed: () => { - changingViewModel.saveCustomer(), - Navigator.of(context).pop(), - Navigator.of(context).pushNamed("customerWelcomePage", arguments: changingViewModel) - }, - ) - ], - ), - ), - ); + print(selected); + }), + }), + Divider(), + RaisedButton( + color: Colors.orange, + textColor: Colors.white, + child: InkWell( + child: Text( + AppLocalizations.of(context).translate("Next"))), + onPressed: () => { + changeBloc.add(CustomerSave()), + Navigator.of(context).pop(), + Navigator.of(context).pushNamed("customerWelcomePage", arguments: customerRepository) + }, + ) + ], + ); + })), + )); } - dynamic getShape( CustomerChangingViewModel changingViewModel, String fitnessLevel ) { - String selected = changingViewModel.customer.bodyType; - dynamic returnCode = ( selected == fitnessLevel ) ? - RoundedRectangleBorder( - side: BorderSide(width: 4, color: Colors.orange), - ) - : - RoundedRectangleBorder( - side: BorderSide(width: 1, color: Colors.blue), - ); + dynamic getShape(CustomerRepository customerRepository, String fitnessLevel) { + String selected = customerRepository.bodyType; + dynamic returnCode = (selected == fitnessLevel) + ? RoundedRectangleBorder( + side: BorderSide(width: 4, color: Colors.orange), + ) + : RoundedRectangleBorder( + side: BorderSide(width: 1, color: Colors.blue), + ); //return return returnCode; } -} \ No newline at end of file +} diff --git a/lib/view/customer_fitness_page.dart b/lib/view/customer_fitness_page.dart index 22d7156..e71bc77 100644 --- a/lib/view/customer_fitness_page.dart +++ b/lib/view/customer_fitness_page.dart @@ -1,11 +1,13 @@ +import 'package:aitrainer_app/bloc/customer_change/customer_change_bloc.dart'; import 'package:aitrainer_app/localization/app_localization.dart'; -import 'package:aitrainer_app/viewmodel/customer_changing_view_model.dart'; +import 'package:aitrainer_app/repository/customer_repository.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; // ignore: must_be_immutable -class CustomerFitnessPage extends StatefulWidget{ +class CustomerFitnessPage extends StatefulWidget { _CustomerFitnessPageState _state; _CustomerFitnessPageState createState() { @@ -29,15 +31,15 @@ class _CustomerFitnessPageState extends State { @override Widget build(BuildContext context) { - final double cWidth = MediaQuery.of(context).size.width*0.75; - final CustomerChangingViewModel changingViewModel = ModalRoute.of(context).settings.arguments; - selected = changingViewModel.customer.fitnessLevel; + final double cWidth = MediaQuery.of(context).size.width * 0.75; + final CustomerRepository customerRepository = + ModalRoute.of(context).settings.arguments; + selected = customerRepository.customer.fitnessLevel; return Scaffold( appBar: AppBar( title: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - Image.asset( 'asset/image/WT_long_logo.png', fit: BoxFit.cover, @@ -46,206 +48,243 @@ class _CustomerFitnessPageState extends State { ], ), backgroundColor: Colors.transparent, - ), - body: SingleChildScrollView( - scrollDirection: Axis.vertical, - child: Container( + ), + body: BlocProvider( + create: (context) => + CustomerChangeBloc(customerRepository: customerRepository), + child: Builder(builder: (context) { + // ignore: close_sinks + CustomerChangeBloc changeBloc = + BlocProvider.of(context); - padding: EdgeInsets.only(bottom: 200), - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage('asset/image/WT_light_background.png'), - fit: BoxFit.cover, - alignment: Alignment.center, - ), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Divider(), - Wrap( - //runAlignment: WrapAlignment.center, - alignment: WrapAlignment.center, - children: [ - Text( - AppLocalizations.of(context).translate("Your Fitness State"), - textAlign: TextAlign.center, - style: TextStyle(color: Colors.orange, - fontSize: 42, fontFamily: 'Arial', - fontWeight: FontWeight.w900 ),) - ] + return SingleChildScrollView( + scrollDirection: Axis.vertical, + child: Container( + padding: EdgeInsets.only(bottom: 200), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('asset/image/WT_light_background.png'), + fit: BoxFit.cover, + alignment: Alignment.center, + ), ), - - - Divider(), - FlatButton( - child: Container( - width: cWidth, - child: Column( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Divider(), + Wrap( + //runAlignment: WrapAlignment.center, + alignment: WrapAlignment.center, children: [ - Text(AppLocalizations.of(context).translate("Beginner"), - textWidthBasis: TextWidthBasis.longestLine, - style: TextStyle(color: Colors.blue, - fontSize: 32, fontFamily: 'Arial', - fontWeight: FontWeight.w900 )), - Text(AppLocalizations.of(context).translate("I am beginner"), - style: TextStyle(color: Colors.black, - fontSize: 20, fontFamily: 'Arial', - fontWeight: FontWeight.w100 ),), - ], - ) - ), - padding: EdgeInsets.all(10.0), - shape: getShape(changingViewModel, FitnessItem.beginner ), - onPressed:() => - { - setState((){ - selected = FitnessItem.beginner; - changingViewModel.customer.setFitnessLevel(selected); - print(selected); - }), - - } - - ), - Divider(), - FlatButton( - child: Container( - width: cWidth, - child: Column( - children: [ - InkWell( - child: Text(AppLocalizations.of(context).translate("Intermediate"), - style: TextStyle(color: Colors.blue, - fontSize: 32, fontFamily: 'Arial', - fontWeight: FontWeight.w900 ),), - highlightColor: Colors.white, + Text( + AppLocalizations.of(context) + .translate("Your Fitness State"), + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.orange, + fontSize: 42, + fontFamily: 'Arial', + fontWeight: FontWeight.w900), + ) + ]), + Divider(), + FlatButton( + child: Container( + width: cWidth, + child: Column( + children: [ + Text( + AppLocalizations.of(context) + .translate("Beginner"), + textWidthBasis: + TextWidthBasis.longestLine, + style: TextStyle( + color: Colors.blue, + fontSize: 32, + fontFamily: 'Arial', + fontWeight: FontWeight.w900)), + Text( + AppLocalizations.of(context) + .translate("I am beginner"), + style: TextStyle( + color: Colors.black, + fontSize: 20, + fontFamily: 'Arial', + fontWeight: FontWeight.w100), + ), + ], + )), + padding: EdgeInsets.all(10.0), + shape: getShape( + customerRepository, FitnessItem.beginner), + onPressed: () => { + setState(() { + selected = FitnessItem.beginner; + changeBloc.add(CustomerFitnessChange(fitness: selected)); + print(selected); + }), + }), + Divider(), + FlatButton( + child: Container( + width: cWidth, + child: Column( + children: [ + InkWell( + child: Text( + AppLocalizations.of(context) + .translate("Intermediate"), + style: TextStyle( + color: Colors.blue, + fontSize: 32, + fontFamily: 'Arial', + fontWeight: FontWeight.w900), + ), + highlightColor: Colors.white, + ), + InkWell( + child: Text( + AppLocalizations.of(context) + .translate("I am intermediate"), + style: TextStyle( + color: Colors.black, + fontSize: 20, + fontFamily: 'Arial', + fontWeight: FontWeight.w100), + ), + highlightColor: Colors.white, + ), + ], ), - InkWell( - child: Text(AppLocalizations.of(context).translate("I am intermediate"), - style: TextStyle(color: Colors.black, - fontSize: 20, fontFamily: 'Arial', - fontWeight: FontWeight.w100 ),), - highlightColor: Colors.white, + ), + padding: EdgeInsets.all(10.0), + shape: getShape( + customerRepository, FitnessItem.intermediate), + onPressed: () => { + setState(() { + selected = FitnessItem.intermediate; + changeBloc.add(CustomerFitnessChange(fitness: selected)); + print(selected); + }), + }), + Divider(), + FlatButton( + child: Container( + width: cWidth, + child: Column( + children: [ + InkWell( + child: Text( + AppLocalizations.of(context) + .translate("Advanced"), + style: TextStyle( + color: Colors.blue, + fontSize: 32, + fontFamily: 'Arial', + fontWeight: FontWeight.w900), + ), + highlightColor: Colors.white, + ), + InkWell( + child: Text( + AppLocalizations.of(context) + .translate("I am advanced"), + style: TextStyle( + color: Colors.black, + fontSize: 20, + fontFamily: 'Arial', + fontWeight: FontWeight.w100), + ), + highlightColor: Colors.white, + ), + ], ), - ], - ), - ), - padding: EdgeInsets.all(10.0), - shape: getShape(changingViewModel, FitnessItem.intermediate ), - - onPressed:() => - { - setState((){ - selected = FitnessItem.intermediate; - changingViewModel.customer.setFitnessLevel(selected); - print(selected); - }), - - } - ), - Divider(), - FlatButton( - child: Container( - width: cWidth, - child: Column( - children: [ - InkWell( - child: Text(AppLocalizations.of(context).translate("Advanced"), - style: TextStyle(color: Colors.blue, - fontSize: 32, fontFamily: 'Arial', - fontWeight: FontWeight.w900 ),), - highlightColor: Colors.white, + ), + padding: EdgeInsets.all(10.0), + shape: getShape( + customerRepository, FitnessItem.advanced), + onPressed: () => { + setState(() { + selected = FitnessItem.advanced; + changeBloc.add(CustomerFitnessChange(fitness: selected)); + print(selected); + }), + }), + Divider(), + FlatButton( + child: Container( + width: cWidth, + child: Column( + children: [ + InkWell( + child: Text( + AppLocalizations.of(context) + .translate("Professional"), + style: TextStyle( + color: Colors.blue, + fontSize: 32, + fontFamily: 'Arial', + fontWeight: FontWeight.w900), + ), + highlightColor: Colors.white, + ), + InkWell( + child: Text( + AppLocalizations.of(context) + .translate("I am professional"), + style: TextStyle( + color: Colors.black, + fontSize: 20, + fontFamily: 'Arial', + fontWeight: FontWeight.w100), + ), + highlightColor: Colors.white, + ), + ], ), - InkWell( - child: Text(AppLocalizations.of(context).translate("I am advanced"), - style: TextStyle(color: Colors.black, - fontSize: 20, fontFamily: 'Arial', - fontWeight: FontWeight.w100 ),), - highlightColor: Colors.white, - ), - ], - ), - ), - padding: EdgeInsets.all(10.0), - shape: getShape(changingViewModel, FitnessItem.advanced ), - onPressed:() => - { - setState((){ - selected = FitnessItem.advanced; - changingViewModel.customer.setFitnessLevel(selected); - print(selected); - }), - - } - ), - Divider(), - FlatButton( - child: Container( - width: cWidth, - child: Column( - children: [ - InkWell( - child: Text(AppLocalizations.of(context).translate("Professional"), - style: TextStyle(color: Colors.blue, - fontSize: 32, fontFamily: 'Arial', - fontWeight: FontWeight.w900 ),), - highlightColor: Colors.white, - ), - InkWell( - child: Text(AppLocalizations.of(context).translate("I am professional"), - style: TextStyle(color: Colors.black, - fontSize: 20, fontFamily: 'Arial', - fontWeight: FontWeight.w100 ),), - highlightColor: Colors.white, - ), - ], - ), - ), - padding: EdgeInsets.all(10.0), - shape: getShape(changingViewModel, FitnessItem.professional ), - onPressed:() => - { - setState((){ - selected = FitnessItem.professional; - changingViewModel.customer.setFitnessLevel(selected); - print(selected); - }), - - } - ), - Divider(), - RaisedButton( - - color: Colors.orange, - textColor: Colors.white, - child: InkWell( - child: Text(AppLocalizations.of(context).translate("Next"))), - onPressed: () => { - changingViewModel.saveCustomer(), - Navigator.of(context).pop(), - Navigator.of(context).pushNamed("customerBodyTypePage", arguments: changingViewModel) - }, - ) - ], - ), - ), - ) - ); + ), + padding: EdgeInsets.all(10.0), + shape: getShape( + customerRepository, FitnessItem.professional), + onPressed: () => { + setState(() { + selected = FitnessItem.professional; + changeBloc.add(CustomerFitnessChange(fitness: selected)); + print(selected); + }), + }), + Divider(), + RaisedButton( + color: Colors.orange, + textColor: Colors.white, + child: InkWell( + child: Text(AppLocalizations.of(context) + .translate("Next"))), + onPressed: () => { + changeBloc.add(CustomerSave()), + Navigator.of(context).pop(), + Navigator.of(context).pushNamed( + "customerBodyTypePage", + arguments: customerRepository) + }, + ) + ], + ), + ), + ); + }))); } - dynamic getShape( CustomerChangingViewModel changingViewModel, String fitnessLevel ) { - String selected = changingViewModel.customer.fitnessLevel; - dynamic returnCode = ( selected == fitnessLevel ) ? - RoundedRectangleBorder( - side: BorderSide(width: 4, color: Colors.orange), - ) - : - RoundedRectangleBorder( - side: BorderSide(width: 1, color: Colors.blue), - ); + dynamic getShape(CustomerRepository customerRepository, String fitnessLevel) { + String selected = customerRepository.fitnessLevel; + dynamic returnCode = (selected == fitnessLevel) + ? RoundedRectangleBorder( + side: BorderSide(width: 4, color: Colors.orange), + ) + : RoundedRectangleBorder( + side: BorderSide(width: 1, color: Colors.blue), + ); //return return returnCode; } - -} \ No newline at end of file +} diff --git a/lib/view/customer_goal_page.dart b/lib/view/customer_goal_page.dart index 30320cb..18a8a01 100644 --- a/lib/view/customer_goal_page.dart +++ b/lib/view/customer_goal_page.dart @@ -1,41 +1,37 @@ +import 'package:aitrainer_app/bloc/customer_change/customer_change_bloc.dart'; import 'package:aitrainer_app/localization/app_localization.dart'; -import 'package:aitrainer_app/viewmodel/customer_changing_view_model.dart'; +import 'package:aitrainer_app/repository/customer_repository.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_form_bloc/flutter_form_bloc.dart'; -// ignore: must_be_immutable -class CustomerGoalPage extends StatefulWidget{ - _CustomerGoalPageState _state; - - _CustomerGoalPageState createState() { - _state = _CustomerGoalPageState(); - return _state; - } -} - -class GoalsItem{ +class GoalsItem { static String muscle = "gain_muscle"; static String weight = "weight_loss"; } -class _CustomerGoalPageState extends State { - String selected; +// ignore: must_be_immutable +class CustomerGoalPage extends StatefulWidget { - initState() { - super.initState(); - } + @override + State createState() => _CustomerGoalPage(); +} + + +class _CustomerGoalPage extends State { + String selected; @override Widget build(BuildContext context) { - final double cWidth = MediaQuery.of(context).size.width*0.75; - final CustomerChangingViewModel changingViewModel = ModalRoute.of(context).settings.arguments; - selected = changingViewModel.customer.goal; + final double cWidth = MediaQuery.of(context).size.width * 0.75; + final CustomerRepository customerRepository = + ModalRoute.of(context).settings.arguments; + return Scaffold( appBar: AppBar( title: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - Image.asset( 'asset/image/WT_long_logo.png', fit: BoxFit.cover, @@ -44,116 +40,138 @@ class _CustomerGoalPageState extends State { ], ), backgroundColor: Colors.transparent, - ), - body: Container( - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage('asset/image/WT_light_background.png'), - fit: BoxFit.cover, - - alignment: Alignment.center, - ), + ), + body: Container( + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('asset/image/WT_light_background.png'), + fit: BoxFit.cover, + alignment: Alignment.center, ), - height: double.infinity, - width: double.infinity, - child: SingleChildScrollView( - child: Center( - child: Column( - children: [ - Divider(), - InkWell( - child: Text(AppLocalizations.of(context).translate("Set Your Goals"), - style: TextStyle(color: Colors.orange, - fontSize: 50, fontFamily: 'Arial', - fontWeight: FontWeight.w900 ),), - highlightColor: Colors.white, - ), - - Stack( - alignment: Alignment.bottomLeft, - overflow: Overflow.visible, - children: [ - FlatButton( - child: Image.asset("asset/image/WT_gain_muscle.png", height: 180,), - padding: EdgeInsets.all(0.0), - shape: getShape(changingViewModel, GoalsItem.muscle ), - onPressed:() => - { - print("gain muscle"), - setState((){ - selected = GoalsItem.muscle; - changingViewModel.customer.setGoal(GoalsItem.muscle); - }), - - } - ), - InkWell( - child: Text(AppLocalizations.of(context).translate("Gain Muscle"), - style: TextStyle(color: Colors.white, - fontSize: 32, fontFamily: 'Arial', - fontWeight: FontWeight.w900 ),), - highlightColor: Colors.white, - ) - ] - ), - Divider(), - Stack( - alignment: Alignment.bottomLeft, - overflow: Overflow.visible, - children: [ - FlatButton( - child: Image.asset("asset/image/WT_weight_loss.png", height: 180,), - padding: EdgeInsets.all(0.0), - shape: getShape(changingViewModel, GoalsItem.weight ), - onPressed:() => - { - print("weight_loss"), - setState((){ - selected = GoalsItem.weight; - changingViewModel.customer.setGoal(GoalsItem.weight); - }), - - } - ), - InkWell( - child: Text(AppLocalizations.of(context).translate("Loose Weight"), - style: TextStyle(color: Colors.white, - fontSize: 32, fontFamily: 'Arial', - fontWeight: FontWeight.w900 ),), - highlightColor: Colors.white, - ) - ] - ), - Divider(), - RaisedButton( - - color: Colors.orange, - textColor: Colors.white, - child: InkWell( - child: Text(AppLocalizations.of(context).translate("Next"))), - onPressed: () => { - changingViewModel.saveCustomer(), - Navigator.of(context).pop(), - Navigator.of(context).pushNamed("customerFitnessPage", arguments: changingViewModel) - }, - ) - ], - ), - ) - ) ), - ); + height: double.infinity, + width: double.infinity, + child: BlocProvider( + create: (context) => + CustomerChangeBloc(customerRepository: customerRepository), + child: Builder(builder: (context) { + CustomerChangeBloc changeBloc = + BlocProvider.of(context); + + + return SingleChildScrollView( + child: Center( + child: Column( + children: [ + Divider(), + InkWell( + child: Text( + AppLocalizations.of(context) + .translate("Set Your Goals"), + style: TextStyle( + color: Colors.orange, + fontSize: 50, + fontFamily: 'Arial', + fontWeight: FontWeight.w900), + ), + highlightColor: Colors.white, + ), + Stack( + alignment: Alignment.bottomLeft, + overflow: Overflow.visible, + children: [ + FlatButton( + child: Image.asset( + "asset/image/WT_gain_muscle.png", + height: 180, + ), + padding: EdgeInsets.all(0.0), + shape: getShape(changeBloc, GoalsItem.muscle), + onPressed: () => { + print("gain muscle"), + setState((){ + selected = GoalsItem.muscle; + changeBloc.add(CustomerGoalChange(goal: GoalsItem.muscle)); + }), + + }), + InkWell( + child: Text( + AppLocalizations.of(context) + .translate("Gain Muscle"), + style: TextStyle( + color: Colors.white, + fontSize: 32, + fontFamily: 'Arial', + fontWeight: FontWeight.w900), + ), + highlightColor: Colors.white, + ) + ]), + Divider(), + Stack( + alignment: Alignment.bottomLeft, + overflow: Overflow.visible, + children: [ + FlatButton( + child: Image.asset( + "asset/image/WT_weight_loss.png", + height: 180, + ), + padding: EdgeInsets.all(0.0), + shape: getShape(changeBloc, GoalsItem.weight), + onPressed: () => { + print("weight_loss"), + setState((){ + selected = GoalsItem.muscle; + changeBloc.add(CustomerGoalChange(goal: GoalsItem.weight)); + }), + + }), + InkWell( + child: Text( + AppLocalizations.of(context) + .translate("Loose Weight"), + style: TextStyle( + color: Colors.white, + fontSize: 32, + fontFamily: 'Arial', + fontWeight: FontWeight.w900), + ), + highlightColor: Colors.white, + ) + ]), + Divider(), + RaisedButton( + color: Colors.orange, + textColor: Colors.white, + child: InkWell( + child: Text( + AppLocalizations.of(context).translate("Next"))), + onPressed: () => { + //changingViewModel.saveCustomer(), + changeBloc.add(CustomerSave()), + Navigator.of(context).pop(), + Navigator.of(context).pushNamed("customerFitnessPage", + arguments: changeBloc.customerRepository) + }, + ) + ], + ), + )); + }), + ), + )); } - dynamic getShape( CustomerChangingViewModel changingViewModel, String goal ) { - String selectedGoal = changingViewModel.customer.goal; - dynamic returnCode = ( selectedGoal == goal ) ? - RoundedRectangleBorder( - side: BorderSide(width: 4, color: Colors.red), - ) - : null; + dynamic getShape(CustomerChangeBloc customerBloc, String goal) { + String selectedGoal = customerBloc.customerRepository.goal; + dynamic returnCode = (selectedGoal == goal) + ? RoundedRectangleBorder( + side: BorderSide(width: 4, color: Colors.red), + ) + : null; //return return returnCode; } - -} \ No newline at end of file +} diff --git a/lib/view/customer_list_page.dart b/lib/view/customer_list_page.dart deleted file mode 100644 index fe8eba0..0000000 --- a/lib/view/customer_list_page.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'package:aitrainer_app/viewmodel/customer_changing_view_model.dart'; -import 'package:aitrainer_app/viewmodel/customer_view_model.dart'; -import 'package:aitrainer_app/widgets/nav_drawer.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:aitrainer_app/widgets/customer_list_widget.dart'; - -class CustomerListPage extends StatefulWidget{ - _CustomerListPageState createState() => _CustomerListPageState(); -} - -class _CustomerListPageState extends State { - //final TextEditingController _controller = TextEditingController(); - Future> _customers; - final _customerViewModel = CustomerChangingViewModel(null); - - @override - void initState() { - super.initState(); - _customers = _customerViewModel.getCustomers(); - } - - @override - Widget build(BuildContext context) { - - //final customerViewModel = CustomerChangingViewModel(null); - - return Scaffold( - drawer: NavDrawer(), - appBar: AppBar( - title: Text("Real customers") - ), - body: Center( - child: FutureBuilder>( - future: _customers, - builder: (context, snapshot) { - if (snapshot.hasData) { - return CustomerListWidget(customers: _customerViewModel.customerList); - } else if (snapshot.hasError) { - return Text("${snapshot.error}"); - } - - // By default, show a loading spinner. - return CircularProgressIndicator(); - }, - ), - ), - /* body: Container( - padding: EdgeInsets.all(10), - width: MediaQuery.of(context).size.width, - height: MediaQuery.of(context).size.height, - child: Column(children: [ - Container( - padding: EdgeInsets.only(left: 10), - decoration: BoxDecoration( - color: Colors.grey, - borderRadius: BorderRadius.circular(10) - ), - child: TextField( - controller: _controller, - onSubmitted: (value) { - if(value.isNotEmpty) { - customerViewModel.getCustomers(); - _controller.clear(); - } - }, - style: TextStyle(color: Colors.white), - decoration: InputDecoration( - hintText: "Search", - hintStyle: TextStyle(color: Colors.white), - border: InputBorder.none - ), - - ), - ), - Expanded( - child: CustomerListWidget(customers: customerViewModel.customers)), - ]), - - ), */ - floatingActionButton: FloatingActionButton( - onPressed: () => Navigator.pushNamed( - context, - 'customerNewPage', - ), - child: Icon(Icons.add,), - mini: true, - ) - - ); - } - -} \ No newline at end of file diff --git a/lib/view/customer_modify_page.dart b/lib/view/customer_modify_page.dart index d2c274a..e7d0990 100644 --- a/lib/view/customer_modify_page.dart +++ b/lib/view/customer_modify_page.dart @@ -1,278 +1,302 @@ +import 'package:aitrainer_app/bloc/account/account_bloc.dart'; +import 'package:aitrainer_app/bloc/customer_change_form_bloc.dart'; import 'package:aitrainer_app/localization/app_localization.dart'; -import 'package:aitrainer_app/viewmodel/customer_changing_view_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; -import 'package:provider/provider.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_form_bloc/flutter_form_bloc.dart'; + +import '../library_keys.dart'; + + // ignore: must_be_immutable -class CustomerModifyPage extends StatefulWidget{ - _CustomerModifyPageState _state; - - _CustomerModifyPageState createState() { - _state = _CustomerModifyPageState(); - return _state; - } -} - -class GenderItem { - GenderItem(this.dbValue,this.name); - final String dbValue; - String name; -} - -class _CustomerModifyPageState extends State { +class CustomerModifyPage extends StatelessWidget{ final _formKey = GlobalKey(); - GenderItem selectedGender; - List genders; - @override - void initState() { - super.initState(); - genders = [ - GenderItem("m", "Man"), - GenderItem("w", "Woman"), - ]; - selectedGender = genders[0]; - - } @override Widget build(BuildContext context) { - //final CustomerViewModel model = CustomerViewModel(); - //model.customer = Auth().userLoggedIn; - //final CustomerChangingViewModel customerChangeModel = - // CustomerChangingViewModel(model); - CustomerChangingViewModel customerChangingViewModel = Provider.of(context, listen: false); - customerChangingViewModel.customer.customer.sex = selectedGender.dbValue; - + // ignore: close_sinks + final accountBloc = BlocProvider.of(context); // we cannot initialize the translations in the initState - genders.forEach((GenderItem element) { - if ( element.dbValue == "m") { +/* genders.forEach((GenderItem element) { + if (element.dbValue == "m") { element.name = AppLocalizations.of(context).translate("Man"); } - if ( element.dbValue == "w") { + if (element.dbValue == "w") { element.name = AppLocalizations.of(context).translate("Woman"); } }); +*/ + return BlocProvider( + create: (context) => + CustomerChangeFormBloc(customerRepository: accountBloc.customerRepository), + child: Builder(builder: (context) { + // ignore: close_sinks + final customerBloc = BlocProvider.of(context); - return Scaffold( - resizeToAvoidBottomInset: true, - appBar: AppBar( - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text("Profil"), - Image.asset( - 'asset/image/WT_long_logo.png', - fit: BoxFit.cover, - height: 65.0, - ), - ], - ), - //title: Text(AppLocalizations.of(context).translate('Settings')), - backgroundColor: Colors.transparent, - ), - body: Container( - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage('asset/image/WT_light_background.png'), - fit: BoxFit.cover, - alignment: Alignment.center, + return Scaffold( + resizeToAvoidBottomInset: true, + appBar: AppBar( + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Profil"), + Image.asset( + 'asset/image/WT_long_logo.png', + fit: BoxFit.cover, + height: 65.0, + ), + ], ), + //title: Text(AppLocalizations.of(context).translate('Settings')), + backgroundColor: Colors.transparent, ), - child: Form( - key: _formKey, - child: SingleChildScrollView( - scrollDirection: Axis.vertical, - padding: EdgeInsets.only(top: 40, left: 25, right: 45, bottom:100), + body: Container( + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('asset/image/WT_light_background.png'), + fit: BoxFit.cover, + alignment: Alignment.center, + ), + ), + child: Form( + key: _formKey, + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + padding: EdgeInsets.only( + top: 40, left: 25, right: 45, bottom: 100), - child: Container( - alignment: Alignment.center, + child: Container( + alignment: Alignment.center, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + child: Column( children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ - Expanded( - child: TextFormField( - style: TextStyle(fontSize: 12), - decoration: InputDecoration( - fillColor: Colors.white24, - filled: true, - labelText: AppLocalizations.of(context).translate('Email'), - ), - initialValue: customerChangingViewModel.customer.customer.email, - onFieldSubmitted: (input) => customerChangingViewModel.customer.setEmail(input) - ) - ) - ], - ), - Divider(), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ + Expanded( + child: + TextFieldBlocBuilder( + key: LibraryKeys.loginEmailField, + style: TextStyle(fontSize: 12), + textFieldBloc: customerBloc.emailField, + decoration: InputDecoration( + fillColor: Colors.white24, + filled: true, + labelText: AppLocalizations.of(context) + .translate('Email'), + ), + ), - Expanded( - child: TextFormField( - style: TextStyle(fontSize: 12), - obscureText: true, - decoration: InputDecoration( - fillColor: Colors.white24, - filled: true, - labelText: AppLocalizations.of(context).translate('Password (Leave empty if you don\'t want to change)' ), + /* TextFormField( + style: TextStyle(fontSize: 12), + decoration: InputDecoration( + fillColor: Colors.white24, + filled: true, + labelText: AppLocalizations.of(context) + .translate('Email'), + ), */ + // initialValue: customerChangingViewModel.customer + // .customer.email, + // onFieldSubmitted: (input) => + // customerChangingViewModel.customer.setEmail( + // input) + ) + ], + ), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + + Expanded( + child: + TextFieldBlocBuilder( + style: TextStyle(fontSize: 12), + textFieldBloc: customerBloc.passwordField, + suffixButton: SuffixButton.obscureText, + decoration: InputDecoration( + fillColor: Colors.white24, + filled: true, + labelText: AppLocalizations.of(context) + .translate('Password (Leave empty if no change)'), + ), ), - initialValue: customerChangingViewModel.customer.customer.password, - onFieldSubmitted: (input) => customerChangingViewModel.customer.setPassword(input) + /*TextFormField( + style: TextStyle(fontSize: 12), + obscureText: true, + decoration: InputDecoration( + fillColor: Colors.white24, + filled: true, + labelText: AppLocalizations.of(context) + .translate( + 'Password (Leave empty if you don\'t want to change)'), + ), + //initialValue: customerChangingViewModel.customer.customer.password, + // onFieldSubmitted: (input) => customerChangingViewModel.customer.setPassword(input) + )*/ ) - ) - ], - ), - Divider(), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ + ], + ), - Expanded( - child: TextFormField( - style: TextStyle(fontSize: 12), - decoration: InputDecoration( - fillColor: Colors.white24, - filled: true, - labelText: AppLocalizations.of(context).translate('Name'), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + + Expanded( + child: TextFieldBlocBuilder( + style: TextStyle(fontSize: 12), + textFieldBloc: customerBloc.nameField, + decoration: InputDecoration( + fillColor: Colors.white24, + filled: true, + labelText: AppLocalizations.of(context) + .translate('Name'), + ), ), - initialValue: customerChangingViewModel.customer.customer.name, - onFieldSubmitted: (input) => customerChangingViewModel.customer.setName(input) ) - ) - ], - ), - Divider(), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ + ], + ), - Expanded( - child: TextFormField( - style: TextStyle(fontSize: 12), - decoration: InputDecoration( - fillColor: Colors.white24, - filled: true, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ - labelText: AppLocalizations.of(context).translate('First Name'), + Expanded( + child: TextFieldBlocBuilder( + style: TextStyle(fontSize: 12), + textFieldBloc: customerBloc.firstNameField, + decoration: InputDecoration( + fillColor: Colors.white24, + filled: true, + labelText: AppLocalizations.of(context) + .translate('First Name'), + ), ), - keyboardType: TextInputType.emailAddress, - initialValue: customerChangingViewModel.customer.customer.firstname, - onFieldSubmitted: (input) => customerChangingViewModel.customer.setFirstName(input) ) - ) - ], - ), - Divider(), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ + ], + ), - Expanded( - child: TextFormField( - style: TextStyle(fontSize: 12), - decoration: InputDecoration( - fillColor: Colors.white24, - filled: true, - labelText: AppLocalizations.of(context).translate('Birth Year'), - ), - keyboardType: TextInputType.number, - inputFormatters: [ - WhitelistingTextInputFormatter.digitsOnly + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + + Expanded( + child: TextFieldBlocBuilder( + style: TextStyle(fontSize: 12), + textFieldBloc: customerBloc.birthYearField, + inputFormatters: [ + FilteringTextInputFormatter(RegExp(r"[\d]"), allow: true) ], - initialValue: customerChangingViewModel.customer.customer.birthYear.toString(), - onFieldSubmitted: (input) => customerChangingViewModel.customer.setBirthYear(int.parse(input)) - ) - ) - ], - ), - Divider(), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - - Expanded( - child: TextFormField( - style: TextStyle(fontSize: 12), - decoration: InputDecoration( - fillColor: Colors.white24, - filled: true, - labelText: AppLocalizations.of(context).translate('Weight'), + decoration: InputDecoration( + fillColor: Colors.white24, + filled: true, + labelText: AppLocalizations.of(context) + .translate('Birth Year'), + ), ), - inputFormatters: [ - WhitelistingTextInputFormatter.digitsOnly - ], - initialValue: customerChangingViewModel.customer.customer.weight.toString(), - keyboardType: TextInputType.number, - onFieldSubmitted: (input) => customerChangingViewModel.customer.setWeight(int.parse(input)), ) - ) - ], - ), - Divider(), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ - Expanded( - child: DropdownButtonHideUnderline( + Expanded( + child: TextFieldBlocBuilder( + style: TextStyle(fontSize: 12), + textFieldBloc: customerBloc.weightField, + inputFormatters: [ + FilteringTextInputFormatter(RegExp(r"[\d]"), allow: true) + ], + decoration: InputDecoration( + fillColor: Colors.white24, + filled: true, + labelText: AppLocalizations.of(context) + .translate('Weight'), + ), + ), + ) + ], + ), + Divider(), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + + Expanded( + child: DropdownFieldBlocBuilder( + selectFieldBloc: customerBloc.genderField, + itemBuilder: (context, item) => item, + decoration: InputDecoration( + labelText: AppLocalizations.of(context).translate( + 'Select a gender'), + ) + ), + ) + /* child: DropdownButtonHideUnderline( child: DropdownButton( - hint: Text(AppLocalizations.of(context).translate('Select a gender')), - style: TextStyle(fontSize: 12, color: Colors.black), + hint: Text( + AppLocalizations.of(context).translate( + 'Select a gender')), + style: TextStyle( + fontSize: 12, color: Colors.black), focusColor: Colors.white24, - value: selectedGender, - items: genders.map((GenderItem gender){ + // value: selectedGender, + items: genders.map((GenderItem gender) { return DropdownMenuItem( value: gender, child: Text(gender.name) ); }).toList(), - onChanged:(GenderItem gender) => { - setState(() { - selectedGender = gender; - customerChangingViewModel.customer.setSex(gender.dbValue); + onChanged: (GenderItem gender) => { + // setState(() { + // selectedGender = gender; + // customerChangingViewModel.customer.setSex(gender.dbValue); - print ("Gender " + gender.name); - }) - //model.customer.sex = - }, + print ("Gender " + gender.name); + //}) + //model.customer.sex = + }, + ) + ) */ + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + + Expanded( + child: RaisedButton( + + color: Colors.orange, + textColor: Colors.white, + child: InkWell( + child: Text( + AppLocalizations.of(context).translate( + "Next"))), + onPressed: () => + { + customerBloc.add(SubmitFormBloc()), + Navigator.of(context).pushNamed("customerGoalPage", arguments: customerBloc.customerRepository) + }, ) ) - ) + ], + ), ], ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - - Expanded( - child: RaisedButton( - - color: Colors.orange, - textColor: Colors.white, - child: InkWell( - child: Text(AppLocalizations.of(context).translate("Next"))), - onPressed: () => { - customerChangingViewModel.saveCustomer(), - Navigator.of(context).pushNamed("customerGoalPage", arguments: customerChangingViewModel) - }, - ) - ) - ], - ), - ], + ), ), - ), - ), - ) - ) + ) + ) + ); + }) ); } + } \ No newline at end of file diff --git a/lib/view/exercise_new_page.dart b/lib/view/exercise_new_page.dart index dd6c7c4..9cb47d9 100644 --- a/lib/view/exercise_new_page.dart +++ b/lib/view/exercise_new_page.dart @@ -1,153 +1,156 @@ -import 'package:aitrainer_app/localization/app_language.dart'; +import 'package:aitrainer_app/bloc/exercise_form_bloc.dart'; import 'package:aitrainer_app/localization/app_localization.dart'; -import 'package:aitrainer_app/viewmodel/exercise_changing_view_model.dart'; +import 'package:aitrainer_app/model/auth.dart'; +import 'package:aitrainer_app/model/exercise_type.dart'; +import 'package:aitrainer_app/repository/exercise_repository.dart'; import 'package:flutter/services.dart'; -import 'package:intl/intl.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -//import 'package:datetime_picker_formfield/datetime_picker_formfield.dart'; +import 'package:flutter_form_bloc/flutter_form_bloc.dart'; class ExerciseNewPage extends StatefulWidget{ _ExerciseNewPageState createState() => _ExerciseNewPageState(); } -class _ExerciseNewPageState extends State { - final List excluded = [43,44]; - final _formKey = GlobalKey(); +class _ExerciseNewPageState extends State { @override Widget build(BuildContext context) { + final ExerciseType exerciseType = ModalRoute.of(context).settings.arguments; - return Consumer( - builder: (context, model, child ) { - String exerciseName = ""; - String customerName = ""; - if ( model != null ) { - if ( model.exerciseViewModel == null ) { - model.createNewModel(); - } - model.exerciseViewModel.createNew(); - customerName = model != null && model.customer != null - ? model.customer.name + " " + - model.customer.firstname - : "Please select a customer"; + return BlocProvider( + create: (context) => ExerciseFormBloc(exerciseRepository: ExerciseRepository()), + child: Builder(builder: (context) { + // ignore: close_sinks + final exerciseBloc = BlocProvider.of(context); - exerciseName = model != null && - model.exerciseType != null - ? model.exerciseType.name - : "Please select an exercise"; - } + exerciseBloc.exerciseRepository.setExerciseType(exerciseType); + String exerciseName = exerciseBloc.exerciseRepository.exerciseType.name; - AppLanguage appLanguage = AppLanguage(); - var date = DateTime.now(); - String dateName = DateFormat(DateFormat.YEAR_MONTH_DAY, appLanguage.appLocal.toString()).format(date.toUtc()) + - " " +DateFormat(DateFormat.HOUR_MINUTE, appLanguage.appLocal.toString()).format(date.toUtc()); - - return Form( - key: _formKey, + return Form( autovalidate: true, child: Scaffold( - resizeToAvoidBottomInset: false, + resizeToAvoidBottomInset: true, appBar: AppBar( - leading: IconButton( - icon: Icon(Icons.arrow_back, color: Colors.deepOrange), - onPressed: () => { - Navigator.of(context).pop() - }, + backgroundColor: Colors.transparent, + title: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Image.asset( + 'asset/image/WT_long_logo.png', + fit: BoxFit.cover, + height: 65.0, + ), + ], + ), + leading: IconButton( + icon: Icon(Icons.arrow_back, color: Colors.white), + onPressed: () => Navigator.of(context).pop(), ), - title: Text(AppLocalizations.of(context).translate(exerciseName) + " " + - AppLocalizations.of(context).translate('Save Exercise'), - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18, color: Colors.deepOrange)), - backgroundColor: Colors.white70, ), body: Container( - decoration: BoxDecoration( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + decoration: BoxDecoration( image: DecorationImage( - image: AssetImage('asset/image/WT_login.png'), - fit: BoxFit.cover, - //height: double.infinity, - //width: double.infinity, - alignment: Alignment.center, + image: AssetImage('asset/image/WT_light_background.png'), + fit: BoxFit.fill, + alignment: Alignment.center, ), ), child: Container( - padding: const EdgeInsets.only (top: 65, left:25, right: 100), - child: Column( + padding: const EdgeInsets.only (top: 25, left:25, right: 25), + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - columnQuantityUnit(model), - columnQuantity(model), + Divider(color: Colors.transparent,), + Divider(color: Colors.transparent,), + Divider(color: Colors.transparent,), - Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - new InkWell( - child: new Text(dateName, - style: TextStyle( fontSize: 16,color: Colors.blue)), - ), - ButtonTheme( - minWidth: 30.0, - height: 30.0, - child: FlatButton( + Text(AppLocalizations.of(context).translate(exerciseName) + " " + + AppLocalizations.of(context).translate('Save Exercise'), + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18, color: Colors.deepOrange)), + Divider(color: Colors.transparent,), + Divider(color: Colors.transparent,), + Divider(color: Colors.transparent,), + columnQuantityUnit(exerciseBloc), + Divider(color: Colors.transparent,), + Divider(color: Colors.transparent,), + Divider(color: Colors.transparent,), - padding: EdgeInsets.only(bottom: 0), - color: Colors.transparent, - splashColor: Colors.black26, - child: Row( - children: [ + columnQuantity(exerciseBloc), + Divider(), - Icon(Icons.arrow_forward_ios, color: Colors.orange,) - ]), - onPressed:() { print("date change");}, - - )), - ], - ), - - new InkWell( - child: new Text(AppLocalizations.of(context).translate('Exercise date and time'), - style: TextStyle( fontSize: 16)), - ), - - ]), RaisedButton( textColor: Colors.white, color: Colors.deepOrange, focusColor: Colors.white, onPressed: () => { - if (_formKey.currentState.validate()) { - //model = ExerciseChangingViewModel(model.exerciseViewModel), - - if ( ! excluded.contains(model.exerciseType.exerciseTypeId) ) { - model.addExercise(), - }, - Navigator.pop(context), - } + confirmationDialog( exerciseBloc ), }, - child: Text("Save", style: TextStyle(fontSize: 16),) + child: Text(AppLocalizations.of(context).translate("Save"), style: TextStyle(fontSize: 16),) ), + Divider(color: Colors.transparent,), + Divider(color: Colors.transparent,), + Divider(color: Colors.transparent,), ]), + ) ) ), ), - ); - }); + ); + }) + ); } - Column columnQuantityUnit( ExerciseChangingViewModel model) { + Column columnQuantityUnit( ExerciseFormBloc bloc ) { Column column = Column(); - if ( model.exerciseType != null && model.exerciseType.unitQuantity == "1") { + if ( bloc.exerciseRepository.exerciseType != null && + bloc.exerciseRepository.exerciseType.unitQuantity == "1") { column = Column( children: [ - TextFormField( + TextFieldBlocBuilder( + textFieldBloc: bloc.unitQuantityField, + textAlign: TextAlign.center, + style: TextStyle(fontSize: 30, + color: Colors.lightBlue, + fontWeight: FontWeight.bold), + inputFormatters: [ + FilteringTextInputFormatter(RegExp(r"[\d.]"), allow: true) + ], + onChanged: (input) => { + print ("UnitQuantity value $input"), + bloc.exerciseRepository.setUnitQuantity( + double.parse(input)) + }, + decoration: InputDecoration( + fillColor: Colors.white, + filled: false, + hintStyle: TextStyle(fontSize: 16, color: Colors.black54, fontWeight: FontWeight.w100), + hintText: AppLocalizations.of(context).translate("The number of the exercise done with"), + labelStyle: TextStyle(fontSize: 16, color: Colors.lightBlue), + labelText: AppLocalizations.of(context).translate( + bloc.exerciseRepository.exerciseType.unitQuantityUnit), + ), + ), + /*TextFormField( + decoration: InputDecoration( + fillColor: Colors.white, + filled: false, + hintStyle: TextStyle(fontSize: 16, color: Colors.black54, fontWeight: FontWeight.w100), + hintText: AppLocalizations.of(context).translate("The number of the exercise done with"), + labelStyle: TextStyle(fontSize: 16, color: Colors.lightBlue), + labelText: AppLocalizations.of(context).translate( + bloc.exerciseRepository.exerciseType.unitQuantityUnit), + ), autovalidate: true, textAlign: TextAlign.center, - initialValue: "0", + initialValue: "", style: TextStyle(fontSize: 30, color: Colors.lightBlue, fontWeight: FontWeight.bold), @@ -155,18 +158,19 @@ class _ExerciseNewPageState extends State { return validateNumberInput(input); }, inputFormatters: [ - WhitelistingTextInputFormatter(RegExp(r"[\d.]")) + FilteringTextInputFormatter(RegExp(r"[\d.]")) + //WhitelistingTextInputFormatter(RegExp(r"[\d.]")) ], onChanged: (input) => { print ("UnitQuantity value $input"), - model.exerciseViewModel.setUnitQuantity( + bloc.exerciseRepository.setUnitQuantity( double.parse(input)) }, - ), + ), */ new InkWell( child: new Text(AppLocalizations.of(context).translate( - model.exerciseType.unitQuantityUnit), + bloc.exerciseRepository.exerciseType.unitQuantityUnit), style: TextStyle(fontSize: 16)), ), @@ -175,16 +179,48 @@ class _ExerciseNewPageState extends State { return column; } - Column columnQuantity( ExerciseChangingViewModel model) { - Column column = Column(); - - column = Column( + Column columnQuantity( ExerciseFormBloc bloc ) { + Column column = Column( children: [ - TextFormField( + TextFieldBlocBuilder( + textFieldBloc: bloc.quantityField, + textAlign: TextAlign.center, + style: TextStyle(fontSize: 50, + color: Colors.deepOrange, + fontWeight: FontWeight.bold), + inputFormatters: [ + FilteringTextInputFormatter(RegExp(r"[\d.]"), allow: true) + ], + onChanged: (input) => + { + print ("Quantity value $input"), + bloc.exerciseRepository.setQuantity(double.parse(input)), + bloc.exerciseRepository.setUnit(bloc.exerciseRepository.exerciseType.unit) + }, + decoration: InputDecoration( + fillColor: Colors.white, + filled: false, + hintStyle: TextStyle(fontSize: 16, color: Colors.black54, fontWeight: FontWeight.w100), + hintText: AppLocalizations.of(context).translate("The number of the exercise"), + labelStyle: TextStyle(fontSize: 16, color: Colors.deepOrange), + labelText: AppLocalizations.of(context).translate( + bloc.exerciseRepository.exerciseType.unit), + ), + ), + /* TextFormField( + decoration: InputDecoration( + fillColor: Colors.white, + filled: false, + hintText: AppLocalizations.of(context).translate("The number of the exercise"), + hintStyle: TextStyle(fontSize: 16, color: Colors.black54, fontWeight: FontWeight.w100), + labelStyle: TextStyle(fontSize: 16, color: Colors.deepOrange), + labelText: AppLocalizations.of(context).translate( + bloc.exerciseRepository.exerciseType.unit), + ), autovalidate: true, textAlign: TextAlign.center, - initialValue: "0", - style: TextStyle(fontSize: 60, + initialValue: "", + style: TextStyle(fontSize: 50, color: Colors.deepOrange, fontWeight: FontWeight.bold), validator: (input) { @@ -195,17 +231,12 @@ class _ExerciseNewPageState extends State { ], onChanged: (input) => { - print ("Quantity value $input"), - model.exerciseViewModel.setQuantity( - double.parse(input)), - model.exerciseViewModel.setUnit(model.exerciseType.unit) + print ("Quantity value $input"), + bloc.exerciseRepository.setQuantity(double.parse(input)), + bloc.exerciseRepository.setUnit(bloc.exerciseRepository.exerciseType.unit) } - ), - new InkWell( - child: new Text(AppLocalizations.of(context).translate(model.exerciseType.unit), - style: TextStyle(fontSize: 16)), - ), + ),*/ ]); @@ -238,4 +269,65 @@ class _ExerciseNewPageState extends State { return null; } + + void confirmationDialog( ExerciseFormBloc bloc ) { + + print("exercise validated " + bloc.exerciseRepository.exercise.quantity.toString()); + if ( bloc.exerciseRepository.exercise.quantity == null) { + return; + } + + String quantity = bloc.exerciseRepository.exercise.quantity % 1 == 0? + bloc.exerciseRepository.exercise.quantity.round().toString() : + bloc.exerciseRepository.exercise.quantity.toString(); + + String unitQuantity = bloc.exerciseRepository.exercise.unitQuantity % 1 == 0? + bloc.exerciseRepository.exercise.unitQuantity.round().toString() : + bloc.exerciseRepository.exercise.unitQuantity.toString(); + + + showCupertinoDialog( + useRootNavigator: true, + context: context, + barrierDismissible: false, + builder:(_) => CupertinoAlertDialog( + title: Text(AppLocalizations.of(context).translate("Do you save this exercise with these parameters?")), + content: Column( + + children: [ + Divider(), + Text(AppLocalizations.of(context).translate("Exercise") + ": " + + AppLocalizations.of(context).translate(bloc.exerciseRepository.exerciseType.name), + style: (TextStyle(color: Colors.blue)),), + Text(quantity + " " + + AppLocalizations.of(context).translate(bloc.exerciseRepository.exerciseType.unit), + style: (TextStyle(color: Colors.deepOrange)),), + Text(bloc.exerciseRepository.exerciseType.unitQuantity == "1" ? + AppLocalizations.of(context).translate("with") + " " + + unitQuantity + " " + + AppLocalizations.of(context).translate(bloc.exerciseRepository.exerciseType.unitQuantityUnit) : + "", + style: (TextStyle(color: Colors.deepOrange)), + ), + + ]), + actions: [ + FlatButton( + child: Text(AppLocalizations.of(context).translate("No")), + onPressed: () => Navigator.pop(context), + ), + FlatButton( + child: Text(AppLocalizations.of(context).translate("Yes")), + onPressed: () => { + bloc.exerciseRepository.setCustomer(Auth().userLoggedIn), + bloc.exerciseRepository.addExercise(), + + Navigator.pop(context), + Navigator.pop(context), + }, + ) + ], + ) + ); + } } diff --git a/lib/view/exercise_type_list_page.dart b/lib/view/exercise_type_list_page.dart deleted file mode 100644 index ce19ebb..0000000 --- a/lib/view/exercise_type_list_page.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:aitrainer_app/viewmodel/exercise_type_changing_view_model.dart'; -import 'package:aitrainer_app/viewmodel/exercise_type_view_model.dart'; -import 'package:aitrainer_app/widgets/exercise_type_list_widget.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:aitrainer_app/widgets/nav_drawer.dart'; - -class ExerciseTypeListPage extends StatefulWidget{ - _ExerciseTypeListPageState createState() => _ExerciseTypeListPageState(); -} - -class _ExerciseTypeListPageState extends State { - Future> _exerciseTypes; - final _exerciseTypeViewModel = ExerciseTypeChangingViewModel(null); - - // Push the page and remove everything else - navigateToPage(BuildContext context, String page) { - Navigator.of(context).pushNamedAndRemoveUntil(page, (Route route) => false); - } - - @override - void initState() { - super.initState(); - _exerciseTypes = _exerciseTypeViewModel.getExerciseTypes(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - drawer: NavDrawer(), - appBar: AppBar( - title: Text('Exercises'), - ), - body: Center( - child: FutureBuilder>( - future: _exerciseTypes, - builder: (context, snapshot) { - if (snapshot.hasData) { - return ExerciseTypeListWidget(exerciseTypes: _exerciseTypeViewModel.exerciseTypeList); - } else if (snapshot.hasError) { - return Text("${snapshot.error}"); - } - - // By default, show a loading spinner. - return CircularProgressIndicator(); - }, - ), - ), - floatingActionButton: FloatingActionButton( - onPressed: () => Navigator.pushNamed( - context, - 'exerciseTypeNewPage', - ), - child: Icon(Icons.add,), - mini: true, - ) - ); - } -} \ No newline at end of file diff --git a/lib/view/exercise_type_modify_page.dart b/lib/view/exercise_type_modify_page.dart deleted file mode 100644 index 2ba415a..0000000 --- a/lib/view/exercise_type_modify_page.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:aitrainer_app/viewmodel/exercise_type_changing_view_model.dart'; -import 'package:aitrainer_app/viewmodel/exercise_type_view_model.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:aitrainer_app/widgets/nav_drawer.dart'; - -// ignore: must_be_immutable -class ExerciseTypeModifyPage extends StatefulWidget{ - ExerciseTypeViewModel exerciseTypeViewModel; - ExerciseTypeModifyPage({this.exerciseTypeViewModel}); - _ExerciseTypeModifyPageState createState() => _ExerciseTypeModifyPageState(); -} - -class _ExerciseTypeModifyPageState extends State { - final _formKey = GlobalKey(); - - @override - Widget build(BuildContext context) { - final ExerciseTypeViewModel exerciseType = ModalRoute.of(context).settings.arguments; - ExerciseTypeChangingViewModel changeModel; - return Scaffold( - drawer: NavDrawer(), - appBar: AppBar( - title: Text('Modify "' + exerciseType.name + '"' ), - ), - body: Center( - child: Form( - key:_formKey, - child: Column( - children: [ - TextFormField( - initialValue: exerciseType.name, - decoration: InputDecoration( - border: OutlineInputBorder(), - labelText: 'Exercise', - ), - validator: (input) => input.length == 0 ? "Please type the name of the exercise" : null, - onChanged: (input) => exerciseType.setName(input), - ) , - TextFormField( - initialValue: exerciseType.description, - minLines: 4, - maxLines: 10, - decoration: InputDecoration( - border: OutlineInputBorder(), - labelText: 'Description', - ), - onChanged: (input) => exerciseType.setDescription(input), - ) - ], - ), - ), - ), - floatingActionButton: FloatingActionButton( - onPressed: () => { - if (_formKey.currentState.validate()) { - changeModel = ExerciseTypeChangingViewModel(exerciseType), - changeModel.saveExerciseType(), - Navigator.pop(context), - } - }, - child: Icon(Icons.save,), - mini: true, - ) - ); - } -} diff --git a/lib/view/exercise_type_new_page.dart b/lib/view/exercise_type_new_page.dart deleted file mode 100644 index c568428..0000000 --- a/lib/view/exercise_type_new_page.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:aitrainer_app/viewmodel/exercise_type_changing_view_model.dart'; -import 'package:aitrainer_app/viewmodel/exercise_type_view_model.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:aitrainer_app/widgets/nav_drawer.dart'; - -class ExerciseTypeNewPage extends StatefulWidget{ - _ExerciseTypeNewPageState createState() => _ExerciseTypeNewPageState(); -} - -class _ExerciseTypeNewPageState extends State { - final ExerciseTypeViewModel exerciseType = ExerciseTypeViewModel(); - final _formKey = GlobalKey(); - - @override - Widget build(BuildContext context) { - ExerciseTypeChangingViewModel model; - exerciseType.createNew(); - return Scaffold( - drawer: NavDrawer(), - appBar: AppBar( - title: Text('New exercise'), - ), - body: Center( - child: Form( - key: _formKey, - child: Column( - children: [ - TextFormField( - decoration: InputDecoration( - border: OutlineInputBorder(), - labelText: 'Exercise', - ), - validator: (input) => input.length == 0 ? "Please type the name of the exercise" : null, - onChanged: (input) => exerciseType.setName(input), - ) , - TextFormField( - minLines: 4, - maxLines: 10, - decoration: InputDecoration( - border: OutlineInputBorder(), - labelText: 'Description', - ), - onChanged: (input) => exerciseType.setDescription(input), - ) - ], - ), - ), - ), - floatingActionButton: FloatingActionButton( - onPressed: () => { - if (_formKey.currentState.validate()) { - model = ExerciseTypeChangingViewModel(exerciseType), - model.addExerciseType(), - model.addNewExercise(exerciseType), - Navigator.pop(context), - } - }, - child: Icon(Icons.save,), - mini: true, - ) - ); - } -} \ No newline at end of file diff --git a/lib/view/login.dart b/lib/view/login.dart index 0136261..88e4372 100644 --- a/lib/view/login.dart +++ b/lib/view/login.dart @@ -1,162 +1,201 @@ +import 'package:aitrainer_app/bloc/login_form_bloc.dart'; +import 'package:aitrainer_app/bloc/account/account_bloc.dart'; import 'package:aitrainer_app/localization/app_localization.dart'; -import 'package:aitrainer_app/model/auth.dart'; -import 'package:aitrainer_app/view/account.dart'; -import 'package:aitrainer_app/viewmodel/customer_changing_view_model.dart'; -import 'package:aitrainer_app/viewmodel/user_changing_view_model.dart'; -import 'package:aitrainer_app/viewmodel/user_view_model.dart'; +import 'package:aitrainer_app/repository/user_repository.dart'; +import 'package:aitrainer_app/util/common.dart'; +import 'package:aitrainer_app/widgets/splash.dart'; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; -import 'package:provider/provider.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_facebook_login/flutter_facebook_login.dart'; +import 'package:flutter_form_bloc/flutter_form_bloc.dart'; +import '../library_keys.dart'; -class LoginPage extends StatefulWidget{ - _LoginPageState createState() => _LoginPageState(); +class LoginPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return LoginWidget(); + } } -class _LoginPageState extends State { +class LoginWidget extends StatefulWidget { + LoginWidget(); + + @override + State createState() => _LoginWidget(); +} + +class _LoginWidget extends State { final GlobalKey _scaffoldKey = new GlobalKey(); - final UserViewModel user = UserViewModel(); - final bool _obscureText = true; final _formKey = GlobalKey(); @override Widget build(BuildContext context) { - UserChangingViewModel model = UserChangingViewModel(user); - CustomerChangingViewModel customerChangingViewModel = Provider.of(context, listen: false); - user.createNew(); - Future customer; - final State stateAccount = ModalRoute.of(context).settings.arguments; - - return Scaffold( - key: _scaffoldKey, - body: Container( - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage('asset/image/WT_login.png'), - fit: BoxFit.cover, - //height: double.infinity, - //width: double.infinity, - alignment: Alignment.center, - ), - ), - child: Form( - key: _formKey, + final accountBloc = BlocProvider.of(context); + return BlocProvider( + create: (context) => LoginFormBloc( + userRepository: UserRepository(), + accountBloc: accountBloc + ), + child: Builder(builder: (context) { + final loginBloc = BlocProvider.of(context); + return Scaffold( + key: _scaffoldKey, + body: FormBlocListener( + onSubmitting: (context, state) { + LoadingDialog.show(context); + }, + onSuccess: (context, state) { + LoadingDialog.hide(context); + Navigator.of(context).pushNamed('home'); + }, + onFailure: (context, state) { + LoadingDialog.hide(context); + showInSnackBar(state.failureResponse); + }, child: Container( - padding: const EdgeInsets.only (left: 25, right: 100), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Spacer(flex: 4), - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - new InkWell( - child: new Text( - AppLocalizations.of(context).translate( - 'Login'), - style: TextStyle(fontWeight: FontWeight.bold, - fontSize: 24)), - ), - ], - ), - - TextFormField( - decoration: InputDecoration( - fillColor: Colors.white, - filled: true, - labelText: 'Email', - ), - validator: (String input) { - RegExp exp = new RegExp(r"[\w._]+\@[\w._]+.[a-z]+", - caseSensitive: false, - multiLine: false,); - String ret = exp.hasMatch(input) == true ? - null : - AppLocalizations.of(context).translate( - 'Please type an email address'); - return ret; - }, - onChanged: (input) => user.setEmail(input), - ), - Spacer(flex: 1), - new TextFormField( - decoration: const InputDecoration( - filled: true, - labelText: "Password", - fillColor: Colors.white, - focusColor: Colors.white, - ), - validator: (val) => val.length < 6 - ? AppLocalizations.of(context).translate( - 'Password too short') - : null, - obscureText: _obscureText, - onChanged: (input) => user.setPassword(input), - ), - Spacer(flex: 1), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ new FlatButton( - child: Image.asset('asset/image/WT_OK.png', - width: 100, - height: 100 - ), - onPressed: () => - { - if (_formKey.currentState.validate()) { - model = UserChangingViewModel(user), - model.getUser().then((_) => - { - if ( stateAccount != null ) { - stateAccount.setState(() { - print("update account"); - }), - }, - customerChangingViewModel.customer.setCustomer(Auth().userLoggedIn), - Navigator.pop(context), - }).catchError(( error, stackTrace )=> showInSnackBar(error) - ), - } - }), - ]), - Spacer(flex: 2), - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - new InkWell( - child: new Text( - AppLocalizations.of(context).translate( - 'SignUp')), - onTap: () => - Navigator.of(context).pushNamed( - 'registration'), - ), - Spacer(flex: 1), - new InkWell( - child: new Text( - AppLocalizations.of(context).translate( - 'Privacy')), - onTap: () => - Navigator.of(context).pushNamed('gdpr'), - ), - Spacer(flex: 2), - ]), - Spacer(flex: 2), - ]) + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('asset/image/WT_login.png'), + fit: BoxFit.cover, + //height: double.infinity, + //width: double.infinity, + alignment: Alignment.center, + ), + ), + child: buildLoginForm(loginBloc, accountBloc), ), ), - ) + ); + })); + } + + Widget buildLoginForm(LoginFormBloc formBloc, AccountBloc accountBloc) { + final cWidth = Common.mediaSizeWidth(context); + + return Form( + key: _formKey, + child: Container( + padding: const EdgeInsets.only(left: 25, right: 100), + child: + ListView(shrinkWrap: false, padding: EdgeInsets.only(top: 120.0), + children: [ + FlatButton( + child: new Image.asset( + 'asset/image/login_fb.png', + width: cWidth * .85, + ), + onPressed: () => { + _fbLogin(), + print("Login with FB"), + }, + ), + Text(AppLocalizations.of(context).translate("OR")), + Divider(), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + new InkWell( + child: new Text( + AppLocalizations.of(context).translate('Login'), + style: TextStyle( + fontWeight: FontWeight.bold, fontSize: 24)), + ), + ], + ), + Divider(), + TextFieldBlocBuilder( + key: LibraryKeys.loginEmailField, + textFieldBloc: formBloc.emailField, + decoration: InputDecoration( + fillColor: Colors.white, + filled: true, + labelText: 'Email', + ), + ), + Divider( + color: Colors.transparent, + ), + TextFieldBlocBuilder( + key: LibraryKeys.loginPasswordField, + textFieldBloc: formBloc.passwordField, + decoration: InputDecoration( + fillColor: Colors.white, + filled: true, + labelText: 'Password', + ), + suffixButton: SuffixButton.obscureText, + ), + Divider( + color: Colors.transparent, + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + new FlatButton( + key: LibraryKeys.loginOKButton, + child: Image.asset('asset/image/WT_OK.png', + width: 100, height: 100), + onPressed: () => { + formBloc.add(SubmitFormBloc()) + }), + ]), + Divider( + color: Colors.transparent, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + new InkWell( + child: new Text( + AppLocalizations.of(context).translate('SignUp')), + onTap: () => + Navigator.of(context).pushNamed('registration'), + ), + Spacer(flex: 1), + new InkWell( + child: new Text( + AppLocalizations.of(context).translate('Privacy')), + onTap: () => Navigator.of(context).pushNamed('gdpr'), + ), + Spacer(flex: 2), + ]), + ])), ); } + void showInSnackBar(String error) { - _scaffoldKey.currentState.showSnackBar( - SnackBar( - backgroundColor: Colors.orange, - content: Text( - AppLocalizations.of(context).translate("Customer does not exist or the password is wrong") + " " + error, - style: TextStyle(color: Colors.white)) - ) - ); + _scaffoldKey.currentState.showSnackBar(SnackBar( + backgroundColor: Colors.orange, + content: Text(error, style: TextStyle(color: Colors.white)))); } -} \ No newline at end of file + + Future _fbLogin() async { + final FacebookLogin facebookSignIn = new FacebookLogin(); + final FacebookLoginResult result = await facebookSignIn.logIn(['email']); + + switch (result.status) { + case FacebookLoginStatus.loggedIn: + final FacebookAccessToken accessToken = result.accessToken; + showInSnackBar(''' + Logged in! + + Token: ${accessToken.token} + User id: ${accessToken.userId} + Expires: ${accessToken.expires} + Permissions: ${accessToken.permissions} + Declined permissions: ${accessToken.declinedPermissions} + '''); + break; + case FacebookLoginStatus.cancelledByUser: + showInSnackBar('Login cancelled by the user.'); + break; + case FacebookLoginStatus.error: + showInSnackBar('Something went wrong with the login process.\n' + 'Here\'s the error Facebook gave us: ${result.errorMessage}'); + break; + } + } +} diff --git a/lib/view/menu_page.dart b/lib/view/menu_page.dart index c2f8a4c..f376d82 100644 --- a/lib/view/menu_page.dart +++ b/lib/view/menu_page.dart @@ -1,161 +1,115 @@ +import 'package:aitrainer_app/bloc/menu/menu_bloc.dart'; import 'package:aitrainer_app/localization/app_localization.dart'; -import 'package:aitrainer_app/model/auth.dart'; -import 'package:aitrainer_app/model/exercise_type.dart'; -import 'package:aitrainer_app/model/workout_tree.dart'; -import 'package:aitrainer_app/util/common.dart'; -import 'package:aitrainer_app/util/menu_tests.dart'; -import 'package:aitrainer_app/viewmodel/exercise_changing_view_model.dart'; import 'package:aitrainer_app/widgets/bottom_nav.dart'; +import 'package:aitrainer_app/widgets/menu_page_widget.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'dart:collection'; -import 'package:provider/provider.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + // ignore: must_be_immutable -class MenuPage extends StatefulWidget { - _MenuPageState _state; +class MenuPage extends StatelessWidget { static const routeName = '/menu_page'; int parent; + MenuBloc menuBloc; MenuPage({this.parent}); - @override - _MenuPageState createState() { - _state = new _MenuPageState(); - return _state; - } - -} - -class _MenuPageState extends State { - final BottomNavigator bottomNav = BottomNavigator(); - @override Widget build(BuildContext context) { - final MenuTests menu = MenuTests(context); + menuBloc = BlocProvider.of(context); return Scaffold( - appBar: AppBar( - backgroundColor: Colors.transparent, - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(AppLocalizations.of(context).translate("Tests")), - Image.asset( - 'asset/image/WT_long_logo.png', - fit: BoxFit.cover, - height: 65.0, - ), - ], - ), - leading: IconButton( - icon: Icon(Icons.arrow_back, color: Colors.white), - onPressed: () => { - this.setState(() { - widget.parent = 0; - }, - )}, - ), - ), - - body: Container( - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage('asset/image/WT_menu_dark.png'), - fit: BoxFit.fill, - alignment: Alignment.center, - ), + appBar: AppBar( + backgroundColor: Colors.transparent, + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(AppLocalizations.of(context).translate("Tests")), + Image.asset( + 'asset/image/WT_long_logo.png', + fit: BoxFit.cover, + height: 65.0, ), - child: CustomScrollView( - scrollDirection: Axis.vertical, - slivers: [ - buildMenuColumn(widget.parent, context, menu) - ] - ) + ], ), + leading: IconButton( + icon: Icon(Icons.arrow_back, color: Colors.white), + onPressed: () => + { + menuBloc.add(MenuTreeUp(parent: 0)) + }, + ), + ), + + body: Container( + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('asset/image/WT_menu_dark.png'), + fit: BoxFit.fill, + alignment: Alignment.center, + ), + ), + child: BlocConsumer( + listener: (context, state) { + if (state is MenuError) { + Scaffold.of(context).showSnackBar(SnackBar( + backgroundColor: Colors.orange, + content: Text("error", style: TextStyle(color: Colors.white)))); + } else if ( state is MenuLoading ) { + return MenuPageWidget(); + } + }, + // ignore: missing_return + builder: (context, state) { + if ( state is MenuInitial ) { + return LoadingMenuDialog(); + } else if (state is MenuReady ) { + return MenuPageWidget(); + } else if ( state is MenuLoading ) { + return LoadingMenuDialog(); + } + } + ) + ), + bottomNavigationBar: BottomNavigator(bottomNavIndex: 0) ); } - - SliverList buildMenuColumn(int parent, BuildContext context, MenuTests menu) { - LinkedHashMap tree = menu.getMenuItems(); - List _columnChildren = List(); - ExerciseType exerciseType; - ExerciseChangingViewModel model = Provider.of(context, listen: false); - - tree.forEach((treeName, value) { - WorkoutTree workoutTree = value as WorkoutTree; - - if ( workoutTree.parent == parent ) { - _columnChildren.add( - Container( - padding: EdgeInsets.only(top: 16.0), - child: Center( - child: Stack( - alignment: Alignment.bottomLeft, - overflow: Overflow.visible, - children: [ - FlatButton( - child: _getButtonImage(workoutTree), - padding: EdgeInsets.all(0.0), - onPressed:() => - { - print("Hi!, Menu clicked " + workoutTree.id.toString()), - if ( workoutTree.child == false ) { - this.setState(() { - widget.parent = workoutTree.id; - }, - ), - } else { - exerciseType = Common.getExerciseType(workoutTree.exerciseTypeId), - model.setExerciseType(exerciseType), - model.setCustomer(Auth().userLoggedIn), - if ( Auth().userLoggedIn == null ) { - Scaffold.of(context) - . showSnackBar( - SnackBar( - backgroundColor: Colors.orange, - content: Text( - AppLocalizations.of(context).translate('Please log in'), - style: TextStyle(color: Colors.white)) - )) - } else { - Navigator.of(context).pushNamed('exerciseNewPage'), - } - } - - } - ), - InkWell( - child: Text(workoutTree.name, style: TextStyle(color: workoutTree.color, fontSize: workoutTree.fontSize, fontFamily: 'Arial', fontWeight: FontWeight.w900 ),), - highlightColor: workoutTree.color, - )])))); - - } - }); - //_columnChildren.add(Spacer(flex: 3)); - SliverList sliverList = - SliverList( - delegate: SliverChildListDelegate( - _columnChildren - ) - ); - - return sliverList; - } - - dynamic _getButtonImage(WorkoutTree workoutTree) { - dynamic image; - if ( workoutTree.imageName.startsWith("http") ) { - image = FadeInImage.assetNetwork( - image: workoutTree.imageName, - placeholder: 'asset/image/dots.gif', - //imageScale: 0.1, - height: 180, - placeholderScale: 0.1, - ); - } else { - image = Image.asset(workoutTree.imageName, height: 180,); - } - return image; - } - } + +class LoadingMenuDialog extends StatefulWidget { + + LoadingMenuDialog({Key key}) : super(key: key); + + @override + State createState() => _LoadingMenuDialog(); +} + +class _LoadingMenuDialog extends State { + @override + void initState() { + super.initState(); + + /// We require the initializers to run after the loading screen is rendered + SchedulerBinding.instance.addPostFrameCallback((_) { + BlocProvider.of(context).add(MenuCreate()); + }); + } + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async => false, + child: Center( + child: Card( + child: Container( + width: 80, + height: 80, + padding: EdgeInsets.all(12.0), + child: CircularProgressIndicator(), + color: Colors.transparent, + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/view/registration.dart b/lib/view/registration.dart index 70572e8..946ba4f 100644 --- a/lib/view/registration.dart +++ b/lib/view/registration.dart @@ -1,142 +1,216 @@ - +import 'package:aitrainer_app/bloc/account/account_bloc.dart'; +import 'package:aitrainer_app/bloc/registration_form_bloc.dart'; import 'package:aitrainer_app/localization/app_localization.dart'; -import 'package:aitrainer_app/model/auth.dart'; -import 'package:aitrainer_app/viewmodel/customer_changing_view_model.dart'; -import 'package:aitrainer_app/viewmodel/user_changing_view_model.dart'; -import 'package:aitrainer_app/viewmodel/user_view_model.dart'; +import 'package:aitrainer_app/util/common.dart'; +import 'package:aitrainer_app/repository/customer_repository.dart'; +import 'package:aitrainer_app/repository/user_repository.dart'; +import 'package:aitrainer_app/widgets/splash.dart'; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; -import 'package:provider/provider.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_facebook_login/flutter_facebook_login.dart'; +import 'package:flutter_form_bloc/flutter_form_bloc.dart'; -class RegistrationPage extends StatefulWidget{ - _RegistrationPageState createState() => _RegistrationPageState(); +import '../library_keys.dart'; + +class RegistrationPage extends StatelessWidget { + final UserRepository userRepository = UserRepository(); + final CustomerRepository customerRepository = CustomerRepository(); + + @override + Widget build(BuildContext context) { + return RegistrationWidget( + userRepository: userRepository, customerRepository: customerRepository); + } } -class _RegistrationPageState extends State { - final GlobalKey _scaffoldKey = new GlobalKey(); - final UserViewModel user = UserViewModel(); - bool _obscureText = true; +class RegistrationWidget extends StatefulWidget { + final UserRepository userRepository; + final CustomerRepository customerRepository; + RegistrationWidget({this.userRepository, this.customerRepository}); + + @override + State createState() => _RegistrationWidget(); +} + +class _RegistrationWidget extends State { + final GlobalKey _scaffoldKey = new GlobalKey(); final _formKey = GlobalKey(); @override Widget build(BuildContext context) { - UserChangingViewModel model = UserChangingViewModel(user); - CustomerChangingViewModel customerChangingViewModel = Provider.of(context, listen: false); - user.createNew(); + final cWidth = Common.mediaSizeWidth(context); + // ignore: close_sinks + final accountBloc = BlocProvider.of(context); + return BlocProvider( + create: (context) => RegistrationFormBloc( + userRepository: UserRepository(), + accountBloc: accountBloc), + child: Builder(builder: (context) { + // ignore: close_sinks + final registrationBloc = BlocProvider.of(context); - return Scaffold( - key: _scaffoldKey, - body: Container( - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage('asset/image/WT_login.png'), - fit: BoxFit.cover, - //height: double.infinity, - //width: double.infinity, - alignment: Alignment.center, - ), - ), - child: Form( - key: _formKey, - child: Container( - padding: const EdgeInsets.only (left:25, right: 100), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Spacer(flex:4), - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - new InkWell( - child: new Text(AppLocalizations.of(context).translate('SignUp'), - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 24)), - ), - ], - ), + return Scaffold( + key: _scaffoldKey, + body: FormBlocListener( + onSubmitting: (context, state) { + LoadingDialog.show(context); + }, + onSuccess: (context, state) { + LoadingDialog.hide(context); + Navigator.of(context).pushNamed('customerModifyPage'); + }, + onFailure: (context, state) { + LoadingDialog.hide(context); + showInSnackBar(state.failureResponse); + }, + child: Container( + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('asset/image/WT_login.png'), + fit: BoxFit.cover, + //height: double.infinity, + //width: double.infinity, + alignment: Alignment.center, + ), + ), + child: Form( + key: _formKey, + child: Container( + padding: const EdgeInsets.only(left: 25, right: 100), + child: ListView( + shrinkWrap: false, + padding: EdgeInsets.only(top: 120.0), + //mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + //Spacer(flex:4), - TextFormField( - decoration: InputDecoration( - fillColor: Colors.white, - filled:true, - labelText: 'Email', - ), - validator: (String input) { - RegExp exp = new RegExp(r"[\w._]+\@[\w._]+.[a-z]+", - caseSensitive: false, - multiLine: false,); - String ret = exp.hasMatch(input) == true ? - null: - AppLocalizations.of(context).translate('Please type an email address'); - return ret; - }, - onChanged: (input) => user.setEmail(input), - ), - Spacer(flex:1), - new TextFormField( - decoration: const InputDecoration( - filled:true, - labelText: "Password", - fillColor: Colors.white, - focusColor: Colors.white, - ), - validator: (val) => val.length < 6 ? AppLocalizations.of(context).translate('Password too short') : null, - obscureText: _obscureText, - onChanged: (input) => user.setPassword(input), - ), - Spacer(flex:1), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ new FlatButton( - child: Image.asset('asset/image/WT_OK.png', - width: 100, - height:100 + FlatButton( + child: new Image.asset( + 'asset/image/login_fb.png', + width: cWidth * .85, + ), + onPressed: () => { + _fbLogin(), + print("Login with FB"), + }, ), - onPressed:() => { - if (_formKey.currentState.validate()) { - model = UserChangingViewModel(user), - model.addUser().then((_) => - { - Navigator.of(context).pushNamed("customerModifyPage",), - customerChangingViewModel.customer.setCustomer(Auth().userLoggedIn), - }).catchError(( error, stackTrace )=> showInSnackBar() + Text(AppLocalizations.of(context).translate("OR")), + Divider(), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + new InkWell( + child: new Text( + AppLocalizations.of(context) + .translate('SignUp'), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 24)), ), - - } - }), - ]), - Spacer(flex:2), - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - new InkWell( - child: new Text(AppLocalizations.of(context).translate('Login')), - onTap: () => Navigator.of(context).pushNamed('login'), - ), - Spacer(flex:1), - new InkWell( - child: new Text(AppLocalizations.of(context).translate('Privacy')), - onTap: () => Navigator.of(context).pushNamed('gdpr'), - ), - Spacer(flex:2), - ]), - Spacer(flex:2), - ]) - ), - ), - ), - - ); + ], + ), + Divider(), + TextFieldBlocBuilder( + key: LibraryKeys.loginEmailField, + textFieldBloc: registrationBloc.emailField, + decoration: InputDecoration( + fillColor: Colors.white, + filled: true, + labelText: 'Email', + ), + ), + Divider( + color: Colors.transparent, + ), + TextFieldBlocBuilder( + key: LibraryKeys.loginPasswordField, + textFieldBloc: registrationBloc.passwordField, + decoration: InputDecoration( + fillColor: Colors.white, + filled: true, + labelText: 'Password', + ), + suffixButton: SuffixButton.obscureText, + ), + Divider( + color: Colors.transparent, + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + new FlatButton( + child: Image.asset( + 'asset/image/WT_OK.png', + width: 100, + height: 100), + onPressed: () => { + registrationBloc.add(SubmitFormBloc()) + }), + ]), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceAround, + children: [ + new InkWell( + child: new Text(AppLocalizations.of(context) + .translate('Login')), + onTap: () => Navigator.of(context) + .pushNamed('login'), + ), + Spacer(flex: 1), + new InkWell( + child: new Text(AppLocalizations.of(context) + .translate('Privacy')), + onTap: () => + Navigator.of(context).pushNamed('gdpr'), + ), + Spacer(flex: 2), + ]), + //Spacer(flex:2), + ])), + ), + ), + ), + ); + })); } - void showInSnackBar() { - _scaffoldKey.currentState.showSnackBar( - SnackBar( - backgroundColor: Colors.orange, - content: Text( - AppLocalizations.of(context).translate("Customer exists"), - style: TextStyle(color: Colors.white)) - ) - ); + void showInSnackBar(String error) { + _scaffoldKey.currentState.showSnackBar(SnackBar( + backgroundColor: Colors.orange, + content: Text( + AppLocalizations.of(context) + .translate("There is an error: during registration:") + + " " + + error, + style: TextStyle(color: Colors.white)))); } -} \ No newline at end of file + + Future _fbLogin() async { + final FacebookLogin facebookSignIn = new FacebookLogin(); + final FacebookLoginResult result = await facebookSignIn.logIn(['email']); + + switch (result.status) { + case FacebookLoginStatus.loggedIn: + final FacebookAccessToken accessToken = result.accessToken; + showInSnackBar(''' + Logged in! + Token: ${accessToken.token} + User id: ${accessToken.userId} + Expires: ${accessToken.expires} + Permissions: ${accessToken.permissions} + Declined permissions: ${accessToken.declinedPermissions} + '''); + break; + case FacebookLoginStatus.cancelledByUser: + showInSnackBar('Login cancelled by the user.'); + break; + case FacebookLoginStatus.error: + showInSnackBar('Something went wrong with the login process.\n' + 'Here\'s the error Facebook gave us: ${result.errorMessage}'); + break; + } + } +} diff --git a/lib/view/settings.dart b/lib/view/settings.dart index 27f4adf..cb4e1ff 100644 --- a/lib/view/settings.dart +++ b/lib/view/settings.dart @@ -1,30 +1,21 @@ +import 'package:aitrainer_app/bloc/settings/settings_bloc.dart'; import 'package:aitrainer_app/localization/app_language.dart'; import 'package:aitrainer_app/localization/app_localization.dart'; import 'package:aitrainer_app/widgets/bottom_nav.dart'; +import 'package:aitrainer_app/widgets/splash.dart'; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; -class SettingsPage extends StatefulWidget{ - _SettingsPageState _state; - - _SettingsPageState createState() { - _state = new _SettingsPageState(); - return _state; - } -} - -class _SettingsPageState extends State { - final AppLanguage appLanguage = AppLanguage(); - Locale _locale; - - final _formKey = GlobalKey(); +class SettingsPage extends StatelessWidget{ @override Widget build(BuildContext context) { - BottomNavigator bottomNav = BottomNavigator(); - _locale = appLanguage.appLocal; + // ignore: close_sinks + SettingsBloc settingsBloc = BlocProvider.of(context); + settingsBloc.context = context; return Scaffold( appBar: AppBar( title: Row( @@ -50,54 +41,60 @@ class _SettingsPageState extends State { ), ), child: Form( - key: _formKey, - child: - ListView( + child: BlocConsumer( + listener: (context, state) { + if ( state is SettingsError ) { + + } + }, + // ignore: missing_return + builder: (context, state) { + if ( state is SettingsLoading ) { + return LoadingDialog(); + } else if ( state is SettingsReady || state is SettingsInitial) { + return ListView( padding: EdgeInsets.only(top: 150), children: [ ListTile( leading: Icon(Icons.language), - subtitle: Text(AppLocalizations.of(context).translate("Change App Language")), + subtitle: Text( + AppLocalizations.of(context).translate( + "Change App Language")), title: DropdownButton( - value: _locale == Locale('en') ? AppLocalizations.of(context).translate("English") : AppLocalizations.of(context).translate("Hungarian"), - items: [AppLocalizations.of(context).translate("English"), AppLocalizations.of(context).translate("Hungarian")] - .map>((String value) { + + value: state.props[0] == Locale('en') + ? AppLocalizations.of(context).translate( + "English") + : AppLocalizations.of(context).translate( + "Hungarian"), + items: [AppLocalizations.of(context).translate( + "English"), AppLocalizations.of(context) + .translate("Hungarian") + ] + .map>((String value) { return DropdownMenuItem( value: value, child: Text(value), ); }).toList(), - onChanged:(String lang) => _changeLanguage(lang), + onChanged: (String lang) => + settingsBloc.add( + SettingsChangeLanguage(language: lang) + ), ) ), - ] - + ] + ); + } else { + return Container(); + } + } ), ), ), - bottomNavigationBar: bottomNav.buildBottomNavigator(context, widget._state) + bottomNavigationBar: BottomNavigator(bottomNavIndex: 3) ); } - - _changeLanguage( String lang ) { - - setState(() { - switch ( lang ) { - case "English": - case "Angol": - _locale = Locale('en'); - break; - case "Hungarian": - case "Magyar": - _locale = Locale('hu'); - break; - } - appLanguage.changeLanguage(_locale); - AppLocalizations.of(context).setLocale(_locale); - AppLocalizations.of(context).load(); - - }); - } } \ No newline at end of file diff --git a/lib/viewmodel/customer_changing_view_model.dart b/lib/viewmodel/customer_changing_view_model.dart deleted file mode 100644 index b0d7e0c..0000000 --- a/lib/viewmodel/customer_changing_view_model.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:aitrainer_app/service/customer_service.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:aitrainer_app/model/customer.dart'; - -import 'customer_view_model.dart'; - -class CustomerChangingViewModel extends ChangeNotifier { - CustomerViewModel customer = CustomerViewModel(); - List customerList = List(); - - CustomerChangingViewModel(customer) { - this.customer = customer; - } - - Future addCustomer() async { - this.customer = customer; - final Customer modelCustomer = customer.getCustomer(); - await CustomerApi().addCustomer(modelCustomer); - } - - Future saveCustomer() async { - //this.customer = customer; - final Customer modelCustomer = customer.getCustomer(); - await CustomerApi().saveCustomer(modelCustomer); - } - - Future> getCustomers() async { - final results = await CustomerApi().getRealCustomers(""); - this.customerList = results.map((item) => CustomerViewModel(customer: item)).toList(); - notifyListeners(); - return this.customerList; - } - - addNewCustomerToList(CustomerViewModel customerViewModel) { - customerList.add(customerViewModel); - } -} \ No newline at end of file diff --git a/lib/viewmodel/customer_view_model.dart b/lib/viewmodel/customer_view_model.dart deleted file mode 100644 index 4e8546c..0000000 --- a/lib/viewmodel/customer_view_model.dart +++ /dev/null @@ -1,88 +0,0 @@ -import 'package:aitrainer_app/model/customer.dart'; - -class CustomerViewModel { - Customer customer; - bool visibleDetails = false; - - CustomerViewModel({this.customer}); - - String get name { - return this.customer.name; - } - - String get firstName { - return this.customer.firstname; - } - - String get sex { - return this.customer.sex == "m" ? "Man" : "Woman"; - } - - int get birthYear { - return this.customer.birthYear; - } - - String get goal { - return this.customer.goal; - } - - String get fitnessLevel { - return this.customer.fitnessLevel; - } - - String get bodyType { - return this.customer.bodyType; - } - - setName(String name) { - this.customer.name = name; - } - setFirstName(String firstName) { - this.customer.firstname = firstName; - } - - setPassword( String password ) { - this.customer.password = password; - } - - setEmail(String email) { - this.customer.email = email; - } - - - setSex(String sex) { - this.customer.sex = sex; - } - - setWeight( int weight) { - this.customer.weight = weight; - } - - setBirthYear( int birthYear ) { - this.customer.birthYear = birthYear; - } - - setFitnessLevel( String level ) { - this.customer.fitnessLevel = level; - } - - setGoal( String goal ) { - this.customer.goal = goal; - } - - setBodyType(String bodyType) { - this.customer.bodyType = bodyType; - } - - createNew() { - this.customer = Customer(); - } - - Customer getCustomer() { - return this.customer; - } - - void setCustomer ( Customer customer ) { - this.customer = customer; - } -} diff --git a/lib/viewmodel/exercise_changing_view_model.dart b/lib/viewmodel/exercise_changing_view_model.dart deleted file mode 100644 index 0b3df88..0000000 --- a/lib/viewmodel/exercise_changing_view_model.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:aitrainer_app/model/customer.dart'; -import 'package:aitrainer_app/model/exercise.dart'; -import 'package:aitrainer_app/model/exercise_type.dart'; -import 'package:aitrainer_app/service/exercise_service.dart'; -import 'package:flutter/cupertino.dart'; - -import 'exercise_view_model.dart'; - -class ExerciseChangingViewModel with ChangeNotifier { - Customer customer; - ExerciseType exerciseType; - List exerciseList = List(); - - ExerciseViewModel exerciseViewModel = ExerciseViewModel(); - - ExerciseChangingViewModel(exerciseViewModel) { - this.exerciseViewModel = exerciseViewModel; - } - - int quantity; - - setCustomer(Customer customer) { - this.customer = customer; - notifyListeners(); - } - - setExerciseType( ExerciseType exerciseType) { - this.exerciseType = exerciseType; - notifyListeners(); - } - - setQuantity(int quantity) { - this.quantity = quantity; - } - - addExercise() async { -// this.exerciseViewModel = exerciseViewModel; - final Exercise modelExercise = exerciseViewModel.getExercise(); - modelExercise.customerId = this.customer.customerId; - modelExercise.exerciseTypeId = this.exerciseType.exerciseTypeId; - await ExerciseApi().addExercise(modelExercise); - } - - createNewModel() { - exerciseViewModel = ExerciseViewModel(); - exerciseViewModel.createNew(); - } - - Future> getExercisesByCustomer( int customerId ) async { - final results = await ExerciseApi().getExercisesByCustomer(customerId); - this.exerciseList = results.map((item) => ExerciseViewModel(exercise: item)).toList(); - notifyListeners(); - return this.exerciseList; - } - -} \ No newline at end of file diff --git a/lib/viewmodel/exercise_type_changing_view_model.dart b/lib/viewmodel/exercise_type_changing_view_model.dart deleted file mode 100644 index 3c53aca..0000000 --- a/lib/viewmodel/exercise_type_changing_view_model.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:aitrainer_app/model/exercise_type.dart'; -import 'package:aitrainer_app/viewmodel/exercise_type_view_model.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:aitrainer_app/service/exercisetype_service.dart'; - -class ExerciseTypeChangingViewModel extends ChangeNotifier { - ExerciseTypeViewModel exerciseType = ExerciseTypeViewModel(); - List exerciseTypeList = List(); - - ExerciseTypeChangingViewModel(exerciseType) { - this.exerciseType = exerciseType; - } - - Future addExerciseType() async { - this.exerciseType = exerciseType; - final ExerciseType modelExerciseType = exerciseType.getExerciseType(); - await ExerciseTypeApi().addExerciseType(modelExerciseType); - } - - Future saveExerciseType() async { - this.exerciseType = exerciseType; - final ExerciseType modelExerciseType = exerciseType.getExerciseType(); - await ExerciseTypeApi().saveExerciseType(modelExerciseType); - } - - Future> getExerciseTypes() async { - final results = await ExerciseTypeApi().getExerciseTypes(""); - this.exerciseTypeList = results.map((item) => ExerciseTypeViewModel( exerciseType: item) ).toList(); - notifyListeners(); - return this.exerciseTypeList; - } - - addNewExercise(ExerciseTypeViewModel exerciseTypeViewModel) { - exerciseTypeList.add(exerciseTypeViewModel); - } -} \ No newline at end of file diff --git a/lib/viewmodel/exercise_type_view_model.dart b/lib/viewmodel/exercise_type_view_model.dart deleted file mode 100644 index f666366..0000000 --- a/lib/viewmodel/exercise_type_view_model.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:aitrainer_app/model/exercise_type.dart'; - -class ExerciseTypeViewModel { - ExerciseType exerciseType; - bool visible = false; - - ExerciseTypeViewModel( {this.exerciseType} ); - - String get name { - return this.exerciseType.name; - } - - setName(String name) { - this.exerciseType.name = name; - } - - String get description { - return this.exerciseType.description; - } - - setDescription(String description) { - this.exerciseType.description = description; - } - - int get exerciseTypeId { - return this.exerciseType.exerciseTypeId; - } - - ExerciseType getExerciseType() { - return this.exerciseType; - } - - createNew() { - this.exerciseType = ExerciseType(); - } -} \ No newline at end of file diff --git a/lib/viewmodel/exercise_view_model.dart b/lib/viewmodel/exercise_view_model.dart deleted file mode 100644 index 35fb91d..0000000 --- a/lib/viewmodel/exercise_view_model.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:aitrainer_app/model/exercise.dart'; - -class ExerciseViewModel { - Exercise exercise; - ExerciseViewModel({this.exercise}); - - createNew() { - this.exercise = Exercise(); - exercise.dateAdd = DateTime.now(); - } - - setQuantity(double quantity) { - this.exercise.quantity = quantity; - } - - setUnitQuantity(double unitQuantity) { - this.exercise.unitQuantity = unitQuantity; - } - - setUnit( String unit) { - this.exercise.unit = unit; - } - - setDatetimeExercise(DateTime datetimeExercise) { - this.exercise.dateAdd = datetimeExercise; - } - - Exercise getExercise() { - return this.exercise; - } -} \ No newline at end of file diff --git a/lib/viewmodel/user_changing_view_model.dart b/lib/viewmodel/user_changing_view_model.dart deleted file mode 100644 index bfc4b9d..0000000 --- a/lib/viewmodel/user_changing_view_model.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:aitrainer_app/model/user.dart'; -import 'package:aitrainer_app/service/customer_service.dart'; -import 'package:aitrainer_app/viewmodel/user_view_model.dart'; -import 'package:flutter/cupertino.dart'; - -class UserChangingViewModel extends ChangeNotifier { - UserViewModel userViewModel = UserViewModel(); - - UserChangingViewModel(userViewModel) { - this.userViewModel = userViewModel; - } - - Future addUser() async { - final User modelUser = userViewModel.getUser(); - await CustomerApi().addUser(modelUser); - } - - Future getUser() async { - final User modelUser = userViewModel.getUser(); - await CustomerApi().getUser(modelUser); - } -} \ No newline at end of file diff --git a/lib/viewmodel/user_view_model.dart b/lib/viewmodel/user_view_model.dart deleted file mode 100644 index 20586d3..0000000 --- a/lib/viewmodel/user_view_model.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:aitrainer_app/model/user.dart'; - -class UserViewModel { - User user; - - UserViewModel({this.user}); - - setEmail(String email) { - this.user.email = email; - } - - setPassword(String password) { - this.user.password = password; - } - - User getUser() { - return this.user; - } - - createNew() { - this.user = User(); - } -} diff --git a/lib/widgets/bottom_nav.dart b/lib/widgets/bottom_nav.dart index 31099fd..bf0c86a 100644 --- a/lib/widgets/bottom_nav.dart +++ b/lib/widgets/bottom_nav.dart @@ -1,15 +1,35 @@ import 'package:aitrainer_app/localization/app_localization.dart'; import 'package:flutter/material.dart'; -class BottomNavigator { - BottomNavigationBar buildBottomNavigator(BuildContext context, State state) { +class BottomNavigator extends StatefulWidget { + int bottomNavIndex = 0; + BottomNavigator({this.bottomNavIndex}) { + this.bottomNavIndex = bottomNavIndex; + } + + @override + _NawDrawerWidget createState() => _NawDrawerWidget(); +} + +class _NawDrawerWidget extends State { + + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BottomNavigationBar( - currentIndex: 0, // this will be set when a new tab is tapped - backgroundColor: Colors.black12, + currentIndex: widget.bottomNavIndex, // this will be set when a new tab is tapped + backgroundColor: Colors.transparent, selectedItemColor: Colors.yellow, unselectedItemColor: Colors.lightGreen, - type: BottomNavigationBarType.shifting, + //type: BottomNavigationBarType.shifting, showSelectedLabels: true, + showUnselectedLabels: true, items: [ BottomNavigationBarItem( backgroundColor: Colors.black12, @@ -18,23 +38,29 @@ class BottomNavigator { title: new Text(AppLocalizations.of(context).translate("Home")), ), BottomNavigationBarItem( - icon: new Icon(Icons.event, color: Colors.lightGreen), + backgroundColor: Colors.black12, + icon: new Icon(Icons.confirmation_number, color: Colors.lightGreen), activeIcon: new Icon(Icons.event, color: Colors.yellow,), title: new Text(AppLocalizations.of(context).translate("Events")), ), + BottomNavigationBarItem( + backgroundColor: Colors.black12, icon: Icon(Icons.person, color: Colors.lightGreen,), activeIcon: new Icon(Icons.person, color: Colors.yellow,), title: Text(AppLocalizations.of(context).translate("Account")) ), BottomNavigationBarItem( + backgroundColor: Colors.black12, icon: Icon(Icons.settings, color: Colors.lightGreen), activeIcon: new Icon(Icons.settings, color: Colors.yellow,), title: Text(AppLocalizations.of(context).translate("Settings")) ) ], onTap:(index) { - // ignore: invalid_use_of_protected_member + setState(() { + widget.bottomNavIndex = index; + }); switch (index) { case 0: Navigator.of(context).pop(); @@ -59,4 +85,6 @@ class BottomNavigator { } ); } + + } \ No newline at end of file diff --git a/lib/widgets/customer_list_widget.dart b/lib/widgets/customer_list_widget.dart deleted file mode 100644 index 332783b..0000000 --- a/lib/widgets/customer_list_widget.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:aitrainer_app/viewmodel/exercise_changing_view_model.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:aitrainer_app/viewmodel/customer_view_model.dart'; -import 'package:provider/provider.dart'; - -class CustomerListWidget extends StatefulWidget { - static const routeName = '/customer_list'; - final List customers; - - CustomerListWidget({this.customers}); - - @override - _CustomerListWidget createState() => _CustomerListWidget(); - -} - -class _CustomerListWidget extends State { - - - @override - Widget build(BuildContext context) { - return ListView.builder( - itemCount: widget.customers.length, - itemBuilder: (context, index) { - - final customer = widget.customers[index]; - - return ListTile( - contentPadding: EdgeInsets.all(10), - leading:Icon(Icons.accessibility), - title: Text(customer.name + " " + customer.firstName), - subtitle: - Container( - child: Visibility( - visible: customer.visibleDetails, - child: Row( - children: [ - Text(customer.birthYear.toString() + " years, " + customer.sex), - new RaisedButton( - child: new Text('Modify'), - color: Color.fromRGBO(244, 122, 22, 0.9), - onPressed: () => { - - }, - ), - new RaisedButton( - child: new Text('Select'), - color: Colors.blueGrey, - onPressed: () => { - Provider.of(context, listen: false).setCustomer(customer.customer), - Navigator.pop(context) - }, - ), - ], - ), - ), - ), - onTap: () { setState( () { - customer.visibleDetails = customer.visibleDetails ? false : true; - }); - }, - ); - }, - ); - } -} \ No newline at end of file diff --git a/lib/widgets/datetime_picker.dart b/lib/widgets/datetime_picker.dart deleted file mode 100644 index 2f5b5d6..0000000 --- a/lib/widgets/datetime_picker.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:flutter_datetime_picker/flutter_datetime_picker.dart'; - -class CustomPicker extends CommonPickerModel { - String digits(int value, int length) { - return '$value'.padLeft(length, "0"); - } - - CustomPicker({DateTime currentTime, LocaleType locale}) : super(locale: locale) { - this.currentTime = currentTime ?? DateTime.now(); - this.setLeftIndex(this.currentTime.hour); - this.setMiddleIndex(this.currentTime.minute); - this.setRightIndex(this.currentTime.second); - } - - @override - String leftStringAtIndex(int index) { - if (index >= 0 && index < 24) { - return this.digits(index, 2); - } else { - return null; - } - } - - @override - String middleStringAtIndex(int index) { - if (index >= 0 && index < 60) { - return this.digits(index, 2); - } else { - return null; - } - } - - @override - String rightStringAtIndex(int index) { - if (index >= 0 && index < 60) { - return this.digits(index, 2); - } else { - return null; - } - } - - @override - String leftDivider() { - return "|"; - } - - @override - String rightDivider() { - return "|"; - } - - @override - List layoutProportions() { - return [1, 2, 1]; - } - - @override - DateTime finalTime() { - return currentTime.isUtc - ? DateTime.utc(currentTime.year, currentTime.month, currentTime.day, - this.currentLeftIndex(), this.currentMiddleIndex(), this.currentRightIndex()) - : DateTime(currentTime.year, currentTime.month, currentTime.day, this.currentLeftIndex(), - this.currentMiddleIndex(), this.currentRightIndex()); - } -} \ No newline at end of file diff --git a/lib/widgets/exercise_type_list_widget.dart b/lib/widgets/exercise_type_list_widget.dart deleted file mode 100644 index 9e3cb3d..0000000 --- a/lib/widgets/exercise_type_list_widget.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'package:aitrainer_app/viewmodel/exercise_changing_view_model.dart'; -import 'package:aitrainer_app/viewmodel/exercise_type_view_model.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -class ExerciseTypeListWidget extends StatefulWidget { - final List exerciseTypes; - ExerciseTypeListWidget({this.exerciseTypes}); - - @override - _ExerciseTypeListWidgetState createState() => _ExerciseTypeListWidgetState(); -} - -class _ExerciseTypeListWidgetState extends State { - //static const routeName = '/exercise_type_list'; - bool visible = false; - - // Push the page and remove everything else - navigateToPage(BuildContext context, String page) { - Navigator.of(context).pushNamedAndRemoveUntil(page, (Route route) => false); - } - - @override - Widget build(BuildContext context) { - - return ListView.builder( - itemCount: widget.exerciseTypes.length, - itemBuilder: (context, index) { - - final exerciseType = widget.exerciseTypes[index]; - - return ListTile( - contentPadding: EdgeInsets.all(10), - leading: Icon(Icons.directions_run), - title: Text(exerciseType.name), - subtitle: - Container( - child: Visibility( - visible: exerciseType.visible, - child: Column( - children: [ - Text(exerciseType.description), - new RaisedButton( - child: new Text('Modify'), - color: Color.fromRGBO(244, 122, 22, 0.9), - onPressed: () => { - Navigator.pushNamed( - context, - 'exerciseTypeModifyPage', - arguments: exerciseType - ) - }, - ), - new RaisedButton( - child: new Text('Select'), - color: Colors.blueGrey, - onPressed: () => { - Navigator.pop(context), - Provider.of(context, listen: false).setExerciseType(exerciseType.exerciseType), - }, - ), - ], - ), - ), - ), - onTap: () { setState( () { - exerciseType.visible = true; - }); - }, - ); - }, - ); - } -} diff --git a/lib/widgets/home.dart b/lib/widgets/home.dart index 35fe2ed..6112843 100644 --- a/lib/widgets/home.dart +++ b/lib/widgets/home.dart @@ -1,12 +1,17 @@ +import 'package:aitrainer_app/bloc/session/session_bloc.dart'; +import 'package:aitrainer_app/bloc/settings/settings_bloc.dart'; import 'package:aitrainer_app/localization/app_language.dart'; -import 'package:aitrainer_app/localization/app_localization.dart'; +import 'package:aitrainer_app/model/auth.dart'; +import 'package:aitrainer_app/view/login.dart'; import 'package:aitrainer_app/view/menu_page.dart'; -import 'package:aitrainer_app/widgets/bottom_nav.dart'; +import 'package:aitrainer_app/view/registration.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; -import 'bottom_nav.dart'; -import 'nav_drawer.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'loading.dart'; + class AitrainerHome extends StatefulWidget { _HomePageState _state; @@ -15,10 +20,6 @@ class AitrainerHome extends StatefulWidget { _state = new _HomePageState(); return _state; } - - void callback() { - _state.setLangNoContext(); - } } class _HomePageState extends State { @@ -28,30 +29,61 @@ class _HomePageState extends State { @override void initState() { super.initState(); - } - @override - Widget build(BuildContext context) { - MenuPage menu = MenuPage(); - BottomNavigator bottomNav = BottomNavigator(); - return Scaffold( - key: _scaffoldKey, - drawer: NavDrawer(), - body:Container( - child: MenuPage(parent: 0), - ), - bottomNavigationBar: bottomNav.buildBottomNavigator(context, widget._state) - ); - } - - void setLangNoContext() { - print("--- Callback "); - setState(() { - final AppLanguage appLanguage = AppLanguage(); - AppLocalizations.of(context).setLocale(appLanguage.appLocal); - AppLocalizations.of(context).load(); - print("--- Lang for context reloaded"); + /// We require the initializers to run after the loading screen is rendered + SchedulerBinding.instance.addPostFrameCallback((_) { + runDelayedEvent(); }); } + Future runDelayedEvent() async { + await Future.delayed(Duration(seconds: 3), () async { + if ( context != null) { + // ignore: close_sinks + SessionBloc sessionBloc = BlocProvider.of(context); + if (sessionBloc.state != SessionReady()) { + sessionBloc.add(SessionStart()); + // ignore: close_sinks + SettingsBloc settingsBloc = BlocProvider.of(context); + settingsBloc.loadLang(); + } + } + }); + } + @override + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + body: Container( + child: BlocConsumer( + listener: (context, state) { + if ( state is SessionFailure) { + + } + }, + builder: (context, state) { + if (state is SessionInitial) { + return LoadingScreenMain(); + } else if (state is SessionLoading) { + print("loading"); + return LoadingScreenMain(); + } else if (state is SessionReady) { + print("ready"); + if (Auth().startPage == 'login') { + return LoginPage(); + } else if (Auth().startPage == 'registration') { + return RegistrationPage(); + } else { + return MenuPage(parent: 0); + } + } else { + print("else"); + return MenuPage(parent: 0); + } + } + ), + ), + //bottomNavigationBar: BottomNavigator(bottomNavIndex: 0), + ); + } } diff --git a/lib/widgets/loading.dart b/lib/widgets/loading.dart index 0e50540..974b39b 100644 --- a/lib/widgets/loading.dart +++ b/lib/widgets/loading.dart @@ -1,17 +1,7 @@ -import 'package:aitrainer_app/util/message_state.dart'; -import 'package:aitrainer_app/util/session.dart'; -import 'package:aitrainer_app/widgets/home.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:aitrainer_app/util/loading_screen.dart'; - -class LoadingScreenMain extends StatefulWidget { - @override - LoadingScreenMainState createState() => LoadingScreenMainState(); -} - -class LoadingScreenMainState extends State { +class LoadingScreenMain extends StatelessWidget { @override Widget build(BuildContext context) { Image _backgroundImage = Image.asset('asset/image/WT01_loading_layers.png', @@ -20,34 +10,60 @@ class LoadingScreenMainState extends State { width: double.infinity, alignment: Alignment.center, ); - dynamic _timer = [TimeMessages.timer]; return Scaffold( - body: LoadingScreen( - initializers: _timer, - navigateToWidget: AitrainerHome(), - loaderColor: Colors.yellow, - image: _backgroundImage, - backgroundColor: Colors.black, - styleTextUnderTheLoader: TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.bold, - color: Colors.lightGreenAccent), - ) + backgroundColor: Colors.white, + body: new InkWell( + child: new Stack( + fit: StackFit.expand, + children: [ + /// Paint the area where the inner widgets are loaded with the + /// background to keep consistency with the screen background + new Container( + decoration: BoxDecoration(color: Colors.white), + ), + /// Render the background image + new Container( + child: _backgroundImage, + ), + /// Render the Title widget, loader and messages below each other + new Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + new Expanded( + flex: 3, + child: new Container( + child: new Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + new Padding( + padding: const EdgeInsets.only(top: 30.0), + ), + + ], + )), + ), + Expanded( + flex: 1, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + /// Loader Animation Widget + CircularProgressIndicator( + valueColor: new AlwaysStoppedAnimation( + Colors.lightGreenAccent), + ), + Padding( + padding: const EdgeInsets.only(top: 20.0), + ), + //Text(getMessage, style: widget.styleTextUnderTheLoader), + ], + ), + ), + ], + ), + ], + ), + ), ); } - - -} - -class TimeMessages { - static Future timer(MessageState state, Function callback) async { - //while (true) { - await Future.delayed(Duration(seconds: 2), () { - state.setMessage = DateTime.now().toIso8601String(); - print("---- TimeMessages initializer"); - Session session = Session(); - session.fetchSessionAndNavigate(callback); - }); - //} - } } diff --git a/lib/widgets/menu_page_widget.dart b/lib/widgets/menu_page_widget.dart new file mode 100644 index 0000000..8e5eff8 --- /dev/null +++ b/lib/widgets/menu_page_widget.dart @@ -0,0 +1,136 @@ +import 'dart:ui'; + +import 'package:aitrainer_app/bloc/menu/menu_bloc.dart'; +import 'package:aitrainer_app/localization/app_localization.dart'; +import 'package:aitrainer_app/model/auth.dart'; +import 'package:aitrainer_app/model/workout_tree.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + + +// ignore: must_be_immutable +class MenuPageWidget extends StatelessWidget { + int parent; + + MenuPageWidget({this.parent}); + + + @override + Widget build(BuildContext context) { + MenuBloc menuBloc = BlocProvider.of(context); + + return CustomScrollView( + scrollDirection: Axis.vertical, + slivers: [ + buildMenuColumn(parent, context, menuBloc) + ] + ); + } + + SliverList buildMenuColumn(int parent, BuildContext context, MenuBloc menuBloc) { + final List _columnChildren = List(); + + menuBloc.menuTreeRepository.getBranch(menuBloc.parent).forEach((treeName, value) { + WorkoutTree workoutTree = value as WorkoutTree; + _columnChildren.add( + Container( + padding: EdgeInsets.only(top: 16.0), + child: Center( + child: Stack( + alignment: Alignment.bottomLeft, + overflow: Overflow.visible, + children: [ + FlatButton( + child: _getButtonImage(workoutTree), + padding: EdgeInsets.all(0.0), + onPressed:() => + { + print("Hi!, Menu clicked " + workoutTree.id.toString()), + if ( workoutTree.child == false ) { + menuBloc.add(MenuTreeDown(parent: workoutTree.id)), + + } else { + menuBloc.add(MenuClickExercise(exerciseTypeId: workoutTree.id)), + if ( Auth().userLoggedIn == null ) { + Scaffold.of(context).showSnackBar( + SnackBar( + backgroundColor: Colors.orange, + content: Text( + AppLocalizations.of(context).translate('Please log in'), + style: TextStyle(color: Colors.white)) + )) + } else { + if ( workoutTree.exerciseType.name == "Custom") { + Navigator.of(context).pushNamed( + 'exerciseCustomPage', + arguments: workoutTree.exerciseType), + } else { + Navigator.of(context).pushNamed( + 'exerciseNewPage', + arguments: workoutTree.exerciseType), + } + } + } + + } + ), + InkWell( + child: Text(workoutTree.name, style: TextStyle(color: workoutTree.color, fontSize: workoutTree.fontSize, fontFamily: 'Arial', fontWeight: FontWeight.w900 ),), + highlightColor: workoutTree.color, + )] + ) + ) + ) + ); + }); + //_columnChildren.add(Spacer(flex: 3)); + SliverList sliverList = + SliverList( + delegate: SliverChildListDelegate( + _columnChildren + ) + ); + + return sliverList; + } + + dynamic _getButtonImage(WorkoutTree workoutTree) { + dynamic image; + /*String url = workoutTree.imageName; + if ( workoutTree.imageName.startsWith("https") ) { + image = FadeInImage.assetNetwork( + placeholder: 'asset/image/dots.gif', + image: url, + height: 180, + ); + } else { + image = Image.asset(workoutTree.imageName, height: 180,); + }*/ + + try { + image = Image.asset( + workoutTree.imageName, + height: 180, + errorBuilder: (context, error, stackTrace) { + String url = Auth.mediaUrl + 'images/' + workoutTree.imageName.substring(11); + Widget image = FadeInImage.assetNetwork( + placeholder: 'asset/image/dots.gif', + image: url, + height: 180, + ); + return image; + }, + ); + } on Exception catch(_) { + String url = Auth.mediaUrl + '/images/' + workoutTree.imageName; + image = FadeInImage.assetNetwork( + placeholder: 'asset/image/dots.gif', + image: url, + height: 180, + ); + } + + return image; + } +} \ No newline at end of file diff --git a/lib/widgets/nav_drawer.dart b/lib/widgets/nav_drawer.dart deleted file mode 100644 index 5dd8c79..0000000 --- a/lib/widgets/nav_drawer.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'package:aitrainer_app/localization/app_language.dart'; -import 'package:aitrainer_app/localization/app_localization.dart'; -import 'package:aitrainer_app/model/auth.dart'; -import 'package:flutter/material.dart'; - -class NavDrawer extends StatefulWidget { - - @override - _NawDrawerWidget createState() => _NawDrawerWidget(); -} - -class _NawDrawerWidget extends State { - final Auth auth = Auth(); - final AppLanguage appLanguage = AppLanguage(); - Locale _locale; - - - @override - void initState() { - super.initState(); - } - - @override - Widget build(BuildContext context) { - return Drawer( - child: ListView( - padding: EdgeInsets.zero, - children: [ - DrawerHeader( - child: Text( - AppLocalizations.of(context).translate('Customers And Exercises'), - style: TextStyle(color: Colors.blue, fontSize: 25), - ), - - ), - ListTile( - leading: Icon(Icons.home), - title: Text( AppLocalizations.of(context).translate('Home')), - onTap: () => Navigator.of(context).pushNamed('home'), - ), - - ListTile( - leading: Icon(Icons.people), - title: Text( AppLocalizations.of(context).translate('Customers')), - onTap: () => Navigator.of(context).pushNamed('customersPage'), - ), - - ListTile( - leading: Icon(Icons.directions_run), - title: Text(AppLocalizations.of(context).translate('Exercises')), - onTap: () => - Navigator.of(context).pushNamed('exerciseTypeListPage'), - ), - ListTile( - leading: Icon(Icons.arrow_upward), - title: Text(AppLocalizations.of(context).translate("TRAINING!")), - onTap: () => Navigator.of(context).pushNamed('exerciseNewPage'), - ), - ListTile( - leading: Icon(Icons.perm_identity), - title: Text(AppLocalizations.of(context).translate('Login')), - onTap: () => Navigator.of(context).pushNamed('login'), - ), - ListTile( - leading: Icon(Icons.cancel), - title: Text(AppLocalizations.of(context).translate('Logout')), - onTap: () => - { - auth.logout(), - Navigator.of(context).pushNamed('home'), - } - ), - ListTile( - leading: Icon(Icons.hearing), - title: Text(AppLocalizations.of(context).translate('Change Language')), - onTap: () => _tapped(), - - ), - ], - ), - ); - } - - void _tapped() => { - /* _locale = Locale("hu"), - appLanguage.changeLanguage(_locale), - AppLocalizations.of(context).setLocale(_locale), - AppLocalizations.of(context).load() */ - }; - -} \ No newline at end of file diff --git a/lib/widgets/splash.dart b/lib/widgets/splash.dart new file mode 100644 index 0000000..99f2cc2 --- /dev/null +++ b/lib/widgets/splash.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; + +class LoadingDialog extends StatelessWidget { + static void show(BuildContext context, {Key key}) => showDialog( + context: context, + useRootNavigator: false, + barrierDismissible: false, + builder: (_) => LoadingDialog(key: key), + ).then((_) => FocusScope.of(context).requestFocus(FocusNode())); + + static void hide(BuildContext context) => Navigator.pop(context); + + LoadingDialog({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async => false, + child: Center( + child: Card( + child: Container( + width: 80, + height: 80, + padding: EdgeInsets.all(12.0), + child: CircularProgressIndicator(backgroundColor: Colors.transparent,), + color: Colors.transparent, + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 03e3c90..0f8c960 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,14 +7,14 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "5.0.0" + version: "7.0.0" analyzer: dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "0.39.12" + version: "0.39.17" archive: dependency: transitive description: @@ -35,7 +35,21 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.4.1" + version: "2.4.2" + bloc: + dependency: transitive + description: + name: bloc + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.1" + bloc_test: + dependency: "direct dev" + description: + name: bloc_test + url: "https://pub.dartlang.org" + source: hosted + version: "7.0.1" boolean_selector: dependency: transitive description: @@ -43,6 +57,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.0" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" charcode: dependency: transitive description: @@ -50,13 +71,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.3" + cli_util: + dependency: transitive + description: + name: cli_util + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" collection: dependency: transitive description: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.14.12" + version: "1.14.13" convert: dependency: transitive description: @@ -70,21 +105,21 @@ packages: name: coverage url: "https://pub.dartlang.org" source: hosted - version: "0.13.11" + version: "0.14.0" crypto: dependency: transitive description: name: crypto url: "https://pub.dartlang.org" source: hosted - version: "2.1.4" + version: "2.1.5" csslib: dependency: transitive description: name: csslib url: "https://pub.dartlang.org" source: hosted - version: "0.16.1" + version: "0.16.2" cupertino_icons: dependency: "direct main" description: @@ -92,13 +127,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.1.3" - datetime_picker_formfield: - dependency: "direct dev" - description: - name: datetime_picker_formfield - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" devicelocale: dependency: "direct main" description: @@ -106,13 +134,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.3.1" + equatable: + dependency: "direct main" + description: + name: equatable + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.3" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" file: dependency: transitive description: name: file url: "https://pub.dartlang.org" source: hosted - version: "5.1.0" + version: "5.2.1" firebase_messaging: dependency: "direct main" description: @@ -125,18 +167,39 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_datetime_picker: - dependency: "direct dev" + flutter_bloc: + dependency: "direct main" description: - name: flutter_datetime_picker + name: flutter_bloc url: "https://pub.dartlang.org" source: hosted - version: "1.3.8" + version: "6.0.1" flutter_driver: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_facebook_login: + dependency: "direct main" + description: + name: flutter_facebook_login + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + flutter_form_bloc: + dependency: "direct main" + description: + name: flutter_form_bloc + url: "https://pub.dartlang.org" + source: hosted + version: "0.19.0" + flutter_keyboard_visibility: + dependency: transitive + description: + name: flutter_keyboard_visibility + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.1" flutter_launcher_icons: dependency: "direct dev" description: @@ -168,6 +231,18 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + form_bloc: + dependency: transitive + description: + name: form_bloc + url: "https://pub.dartlang.org" + source: hosted + version: "0.19.1" fuchsia_remote_debug_protocol: dependency: transitive description: flutter @@ -214,7 +289,7 @@ packages: name: image url: "https://pub.dartlang.org" source: hosted - version: "2.1.12" + version: "2.1.14" intl: dependency: "direct dev" description: @@ -242,7 +317,7 @@ packages: name: json_rpc_2 url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.2.1" logging: dependency: transitive description: @@ -256,7 +331,7 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.6" + version: "0.12.8" meta: dependency: transitive description: @@ -270,7 +345,7 @@ packages: name: mime url: "https://pub.dartlang.org" source: hosted - version: "0.9.6+3" + version: "0.9.7" mockito: dependency: "direct main" description: @@ -278,13 +353,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.1.1" - multi_server_socket: + nested: dependency: transitive description: - name: multi_server_socket + name: nested url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "0.0.4" node_interop: dependency: transitive description: @@ -319,7 +394,21 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.6.4" + version: "1.7.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.1+2" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" pedantic: dependency: transitive description: @@ -333,7 +422,7 @@ packages: name: petitparser url: "https://pub.dartlang.org" source: hosted - version: "2.4.0" + version: "3.0.4" platform: dependency: transitive description: @@ -361,14 +450,14 @@ packages: name: process url: "https://pub.dartlang.org" source: hosted - version: "3.0.12" + version: "3.0.13" provider: dependency: "direct dev" description: name: provider url: "https://pub.dartlang.org" source: hosted - version: "3.2.0" + version: "4.3.2" pub_semver: dependency: transitive description: @@ -383,6 +472,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.3" + rxdart: + dependency: transitive + description: + name: rxdart + url: "https://pub.dartlang.org" + source: hosted + version: "0.24.1" sentry: dependency: "direct main" description: @@ -396,14 +492,42 @@ packages: name: shared_preferences url: "https://pub.dartlang.org" source: hosted - version: "0.4.3" + version: "0.5.8" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.2+1" + shared_preferences_macos: + dependency: transitive + description: + name: shared_preferences_macos + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.1+10" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.2+7" shelf: dependency: transitive description: name: shelf url: "https://pub.dartlang.org" source: hosted - version: "0.7.7" + version: "0.7.8" shelf_packages_handler: dependency: transitive description: @@ -451,13 +575,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.7.0" + spider_chart: + dependency: "direct main" + description: + name: spider_chart + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.5" stack_trace: dependency: transitive description: name: stack_trace url: "https://pub.dartlang.org" source: hosted - version: "1.9.3" + version: "1.9.5" stream_channel: dependency: transitive description: @@ -492,28 +623,28 @@ packages: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.14.4" + version: "1.15.2" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.15" + version: "0.2.17" test_core: dependency: transitive description: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.3.4" + version: "0.3.10" typed_data: dependency: transitive description: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.1.6" + version: "1.2.0" usage: dependency: transitive description: @@ -521,6 +652,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.4.2" + uuid: + dependency: transitive + description: + name: uuid + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" vector_math: dependency: transitive description: @@ -534,7 +672,7 @@ packages: name: vm_service url: "https://pub.dartlang.org" source: hosted - version: "4.1.0" + version: "4.2.0" vm_service_client: dependency: transitive description: @@ -570,13 +708,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.7.3" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.0" xml: dependency: transitive description: name: xml url: "https://pub.dartlang.org" source: hosted - version: "3.6.1" + version: "4.2.0" yaml: dependency: transitive description: @@ -585,5 +730,5 @@ packages: source: hosted version: "2.2.1" sdks: - dart: ">=2.7.0 <3.0.0" - flutter: ">=1.12.13+hotfix.6 <2.0.0" + dart: ">=2.9.0-14.0.dev <3.0.0" + flutter: ">=1.16.0 <2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 59d94a6..cde5a4d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.1.0+1 +version: 1.1.0+12 environment: sdk: ">=2.7.0 <3.0.0" @@ -31,6 +31,11 @@ dependencies: sentry: ^3.0.1 firebase_messaging: ^6.0.16 flutter_local_notifications: 1.1.1 + flutter_facebook_login: ^3.0.0 + flutter_bloc: ^6.0.1 + equatable: ^1.2.3 + flutter_form_bloc: ^0.19.0 + spider_chart: ^0.1.5 mockito: ^4.1.1 @@ -41,15 +46,14 @@ dev_dependencies: flutter_driver: sdk: flutter test: any + bloc_test: ^7.0.1 http: 0.12.1 - provider: ^3.2.0 + provider: ^4.3.2 intl: 0.16.1 - flutter_datetime_picker: ^1.3.8 - datetime_picker_formfield: ^1.0.0 - shared_preferences: ^0.4.1 + shared_preferences: ^0.5.8 - flutter_launcher_icons: "^0.7.3" + flutter_launcher_icons: ^0.7.5 flutter_icons: android: "launcher_icon" @@ -81,6 +85,7 @@ flutter: - asset/image/WT_gain_muscle.png - asset/image/WT_weight_loss.png - asset/image/WT_welcome.png + - asset/image/login_fb.png - asset/menu/1.cardio.png - asset/menu/1.1.aerob.png - asset/menu/1.2.anaerob.png diff --git a/test/account_bloc_test.dart b/test/account_bloc_test.dart new file mode 100644 index 0000000..562111d --- /dev/null +++ b/test/account_bloc_test.dart @@ -0,0 +1,79 @@ +import 'package:aitrainer_app/bloc/account/account_bloc.dart'; +import 'package:aitrainer_app/repository/customer_repository.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart' as test; +import 'package:flutter_test/flutter_test.dart'; + +class MockCustomerRepository extends Mock implements CustomerRepository { + +} + +void main() { + MockCustomerRepository customerRepository; + AccountBloc accountBloc; + + TestWidgetsFlutterBinding.ensureInitialized(); + + test.setUp(() { + customerRepository = MockCustomerRepository(); + accountBloc = AccountBloc(customerRepository: customerRepository); + }); + + test.tearDown(() { + accountBloc?.close(); + }); + + test.test('initial state is correct', () { + expect(accountBloc.state, AccountInitial()); + }); + + group('Account', () { + test.test( + 'emits [loading, logged in] when the customer clicked login', + () { + final expectedResponse = [ + AccountLoading(), + AccountLoggedIn(), + ]; + + //verify(accountBloc.customerRepository.customer == null); + + expectLater( + accountBloc, emitsInOrder(expectedResponse), + ); + + accountBloc.add(AccountLogin()); + }); + }); + + test.test( + 'emits [loading, logged out] when the customer clicked logout', + () { + final expectedResponse = [ + AccountLoading(), + AccountLoggedOut(), + ]; + + expectLater( + accountBloc, emitsInOrder(expectedResponse), + ); + + accountBloc.add(AccountLogout()); + }); + + test.test( + 'emits [loading, logged out] when the customer data changed', + () { + final expectedResponse = [ + AccountLoading(), + AccountReady(), + ]; + + expectLater( + accountBloc, emitsInOrder(expectedResponse), + ); + + accountBloc.add(AccountChangeCustomer()); + }); + +} \ No newline at end of file diff --git a/test/widget_test.dart b/test/widget_test.dart deleted file mode 100644 index ec3fb30..0000000 --- a/test/widget_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:aitrainer_app/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(AitrainerApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -} diff --git a/test/widget_test.login.dart b/test/widget_test.login.dart new file mode 100644 index 0000000..724a47f --- /dev/null +++ b/test/widget_test.login.dart @@ -0,0 +1,107 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility that Flutter provides. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + + +import 'package:aitrainer_app/bloc/login_form_bloc.dart'; +import 'package:aitrainer_app/library_keys.dart'; +import 'package:aitrainer_app/localization/app_localization.dart'; +import 'package:aitrainer_app/model/user.dart'; +import 'package:aitrainer_app/repository/user_repository.dart'; +import 'package:aitrainer_app/util/common.dart'; +import 'package:aitrainer_app/view/login.dart'; +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_form_bloc/flutter_form_bloc.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + + +class MockUserRepository extends Mock implements UserRepository { + final User user = User(); + + setEmail(String email) { + this.user.email = email; + } + + setPassword(String password) { + this.user.password = password; + } + + Future getUser() async { + if ( this.user.email == "sw@andio.biz" && this.user.password == "PalKataPeter1") { + //OK + } else { + throw new Exception("Customer does not exist"); + } + } +} +class MockLoginBloc extends MockBloc + implements LoginFormBloc {} + +void main() { + group('LoginScreen', () { + MockLoginBloc loginBloc; + MockUserRepository userRepository; + Widget loginWidget; + + setUp(() { + loginBloc = MockLoginBloc(); + userRepository = MockUserRepository(); + + loginWidget = + MaterialApp( + home: LoginPage(), + localizationsDelegates: [ + AppLocalizationsDelegate(isTest: true), + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + routes: { + 'home': (context) => LoginPage(), + } + ); + }); + + tearDown(() { + loginBloc.close(); + }); + + testWidgets('Display login page', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(loginWidget); + + await tester.pumpAndSettle(); + + expect(find.byKey(LibraryKeys.loginEmailField), findsOneWidget); + expect(find.byKey(LibraryKeys.loginPasswordField), findsOneWidget); + expect(find.byKey(LibraryKeys.loginOKButton), findsOneWidget); + }); + + testWidgets('Add zero length email', (WidgetTester tester) async { + await tester.pumpWidget(loginWidget); + await tester.pumpAndSettle(); + + var emailField = find.byKey(LibraryKeys.loginEmailField); + var pwdField = find.byKey(LibraryKeys.loginPasswordField); + var okButton = find.byKey(LibraryKeys.loginOKButton); + expect(emailField, findsOneWidget); + expect(pwdField, findsOneWidget); + expect(okButton, findsOneWidget); + + await tester.tap(emailField); + await tester.enterText(emailField, "sw"); + await tester.enterText(pwdField, "123"); + + await tester.pumpAndSettle(); + await tester.tap(okButton); + await tester.pump(const Duration(milliseconds: 500)); // add delay + final emailErrorFinder = find.text(Common.EMAIL_ERROR); + expect(emailErrorFinder, findsOneWidget); + }); + }); +} diff --git a/test_driver/app.dart b/test_driver/app.dart new file mode 100644 index 0000000..b3698db --- /dev/null +++ b/test_driver/app.dart @@ -0,0 +1,11 @@ +import 'package:flutter_driver/driver_extension.dart'; +import 'package:aitrainer_app/main.dart' as app; + +void main() { + // This line enables the extension. + enableFlutterDriverExtension(); + + // Call the `main()` function of the app, or call `runApp` with + // any widget you are interested in testing. + app.main(); +} \ No newline at end of file diff --git a/test_driver/app_test.dart b/test_driver/app_test.dart new file mode 100644 index 0000000..eab3403 --- /dev/null +++ b/test_driver/app_test.dart @@ -0,0 +1,9 @@ +// Imports the Flutter Driver API. +import 'package:flutter_driver/flutter_driver.dart'; +import 'package:test/test.dart'; + +void main() { + group('Login App', () { + + }); +} \ No newline at end of file