Merge pull request 'Water intake Module Completed (faiz_dev)' (#135) from faiz_dev into master
Reviewed-on: https://34.17.182.140/Haroon6138/HMG_Patient_App_New/pulls/135pull/139/head
@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.85417 0C4.41186 0 0 4.41186 0 9.85417C0 15.2965 4.41186 19.7083 9.85417 19.7083C15.2965 19.7083 19.7083 15.2965 19.7083 9.85417C19.7083 4.41186 15.2965 0 9.85417 0ZM10.7708 6.1875C10.7708 5.68124 10.3604 5.27083 9.85417 5.27083C9.34791 5.27083 8.9375 5.68124 8.9375 6.1875V8.9375H6.1875C5.68124 8.9375 5.27083 9.34791 5.27083 9.85417C5.27083 10.3604 5.68124 10.7708 6.1875 10.7708H8.9375V13.5208C8.9375 14.0271 9.34791 14.4375 9.85417 14.4375C10.3604 14.4375 10.7708 14.0271 10.7708 13.5208V10.7708H13.5208C14.0271 10.7708 14.4375 10.3604 14.4375 9.85417C14.4375 9.34791 14.0271 8.9375 13.5208 8.9375H10.7708V6.1875Z" fill="#2B353E"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 790 B |
@ -1,3 +1,3 @@
|
||||
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
add_<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.1905 5.77665C20.59 6.15799 20.6047 6.79098 20.2234 7.19048L9.72336 18.1905C9.53745 18.3852 9.28086 18.4968 9.01163 18.4999C8.7424 18.5031 8.48328 18.3975 8.29289 18.2071L4.79289 14.7071C4.40237 14.3166 4.40237 13.6834 4.79289 13.2929C5.18342 12.9024 5.81658 12.9024 6.20711 13.2929L8.98336 16.0692L18.7766 5.80953C19.158 5.41003 19.791 5.39531 20.1905 5.77665Z" fill="white"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 533 B After Width: | Height: | Size: 537 B |
@ -0,0 +1,5 @@
|
||||
<svg width="44" height="32" viewBox="0 0 44 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 1H31.2656L28.4282 31H7.6206L1 1Z" stroke="#8F9AA3" stroke-width="2" stroke-linejoin="round" stroke-dasharray="4 4"/>
|
||||
<path d="M31.2656 3.14062H36.9404C40.1561 3.14062 42.6152 5.92634 42.6152 9.5692V15.9978C42.6152 19.6406 40.1561 22.4263 36.9404 22.4263H33.1572" stroke="#8F9AA3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="4 4"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.9171 10.1057C20.1307 9.99999 19.1234 9.99999 17.8411 10H17.7741C16.4918 9.99999 15.4846 9.99999 14.6982 10.1057C13.8923 10.2141 13.253 10.4406 12.7506 10.943C12.2482 11.4453 12.0217 12.0847 11.9133 12.8906C11.8076 13.6769 11.8076 14.6842 11.8076 15.9665V16.0335C11.8076 17.3158 11.8076 18.3231 11.9133 19.1094C12.0217 19.9153 12.2482 20.5547 12.7506 21.057C13.253 21.5594 13.8923 21.7859 14.6982 21.8943C15.4846 22 16.4918 22 17.7741 22H17.8411C19.1234 22 20.1307 22 20.9171 21.8943C21.7229 21.7859 22.3623 21.5594 22.8646 21.057C23.367 20.5547 23.5936 19.9153 23.7019 19.1094C23.8076 18.3231 23.8076 17.3158 23.8076 16.0335V15.9665C23.8076 14.6842 23.8076 13.6769 23.7019 12.8906C23.5936 12.0847 23.367 11.4453 22.8646 10.943C22.3623 10.4406 21.7229 10.2141 20.9171 10.1057ZM18.393 13.6585C18.393 13.3352 18.1309 13.0732 17.8076 13.0732C17.4843 13.0732 17.2223 13.3352 17.2223 13.6585V15.4146H15.4662C15.1429 15.4146 14.8808 15.6767 14.8808 16C14.8808 16.3233 15.1429 16.5854 15.4662 16.5854H17.2223V18.3415C17.2223 18.6648 17.4843 18.9268 17.8076 18.9268C18.1309 18.9268 18.393 18.6648 18.393 18.3415V16.5854H20.1491C20.4724 16.5854 20.7344 16.3233 20.7344 16C20.7344 15.6767 20.4724 15.4146 20.1491 15.4146H18.393V13.6585Z" fill="#8F9AA3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@ -0,0 +1,4 @@
|
||||
<svg width="44" height="32" viewBox="0 0 44 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 1H31.2656L28.4282 31H7.6206L1 1Z" stroke="#2E3039" stroke-width="2" stroke-linejoin="round"/>
|
||||
<path d="M31.2656 3.14062H36.9404C40.1561 3.14062 42.6152 5.92634 42.6152 9.5692V15.9978C42.6152 19.6406 40.1561 22.4263 36.9404 22.4263H33.1572" stroke="#2E3039" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 438 B |
@ -0,0 +1,5 @@
|
||||
<svg width="44" height="32" viewBox="0 0 44 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 1H31.2656L28.4282 31H7.6206L1 1Z" stroke="#2E3039" stroke-width="2" stroke-linejoin="round"/>
|
||||
<path d="M31.2656 3.14062H36.9404C40.1561 3.14062 42.6152 5.92634 42.6152 9.5692V15.9978C42.6152 19.6406 40.1561 22.4263 36.9404 22.4263H33.1572" stroke="#2E3039" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7.80762 14.561C14.141 11.4732 20.4743 22.2805 26.8076 19.1927V30H9.91873L7.80762 14.561Z" fill="#4EB5FF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 555 B |
@ -0,0 +1,5 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18.582 1.25C18.1253 1.25 17.7623 1.44409 17.4564 1.6775C17.4426 1.68802 17.4288 1.69876 17.4149 1.70972C17.1988 1.87986 17.0908 1.96493 17.0829 2.09948C17.0749 2.23403 17.1799 2.33908 17.39 2.54917L21.4509 6.61006C21.661 6.82016 21.7661 6.92521 21.9006 6.91724C22.0352 6.90928 22.1202 6.80127 22.2904 6.58526C22.3013 6.57135 22.3121 6.5575 22.3226 6.54371C22.556 6.23778 22.7501 5.87482 22.7501 5.41811C22.7501 4.96139 22.556 4.59843 22.3226 4.2925C22.1683 4.0902 21.9664 3.87507 21.7389 3.64395L22.4645 2.91831C22.8461 2.53666 22.8461 1.91789 22.4645 1.53624C22.0828 1.15459 21.4641 1.15459 21.0824 1.53624L20.3568 2.26187C20.1254 2.03412 19.9101 1.83199 19.7076 1.6775C19.4017 1.44409 19.0387 1.25 18.582 1.25Z" fill="#8F9AA3"/>
|
||||
<path d="M15.8025 2.55451C15.4544 2.41032 15.0732 2.37717 14.7099 2.45499C14.3791 2.52586 14.1459 2.7182 14.0063 2.85774L12.9207 3.94343C12.7811 4.08298 12.5888 4.31616 12.5179 4.64697C12.4401 5.01026 12.4732 5.39146 12.6174 5.73957C12.7033 5.94681 12.8276 6.11274 12.9534 6.25694L14.8296 8.13314C14.6603 8.37678 14.4295 8.56478 14.1399 8.69315C11.6325 9.80468 9.80541 11.6318 8.69388 14.1392C8.56556 14.4286 8.37765 14.6594 8.13414 14.8286L6.2565 12.951C6.2565 12.951 5.94637 12.7009 5.73913 12.6151C5.39102 12.4709 5.00982 12.4377 4.64653 12.5155C4.31572 12.5864 4.08254 12.7787 3.94299 12.9183L2.8573 14.004C2.71776 14.1435 2.52542 14.3767 2.45455 14.7075C2.37673 15.0708 2.40988 15.452 2.55407 15.8001C2.63991 16.0074 2.76424 16.1733 2.89002 16.3175L7.68129 21.1088C7.82549 21.2345 7.99142 21.3589 8.19866 21.4447C8.54677 21.5889 8.92797 21.6221 9.29126 21.5442C9.62207 21.4734 9.85525 21.281 9.9948 21.1415L11.0805 20.0558C11.22 19.9162 11.4124 19.6831 11.4832 19.3522C11.5611 18.989 11.5279 18.6078 11.3837 18.2597C11.2979 18.0524 11.1736 17.8865 11.0478 17.7423L9.55828 16.2528C9.96485 15.9054 10.2931 15.4666 10.5223 14.9497C11.4326 12.8962 12.8969 11.4319 14.9504 10.5215C15.4675 10.2923 15.9063 9.96398 16.2537 9.5573L17.7446 11.0482C17.7446 11.0482 18.0548 11.2983 18.262 11.3842C18.6101 11.5284 18.9913 11.5615 19.3546 11.4837C19.6854 11.4128 19.9186 11.2205 20.0582 11.0809L21.1438 9.99524C21.2834 9.85569 21.4757 9.62251 21.5466 9.2917C21.6244 8.92841 21.5913 8.54721 21.4471 8.1991C21.3612 7.99186 21.2369 7.82593 21.1111 7.68173L16.3199 2.89046C16.1757 2.76468 16.0097 2.64035 15.8025 2.55451Z" fill="#8F9AA3"/>
|
||||
<path d="M5.41914 22.7492C5.87586 22.7492 6.23882 22.5551 6.54475 22.3217C6.55854 22.3112 6.57238 22.3005 6.58629 22.2895C6.80231 22.1194 6.91032 22.0343 6.91828 21.8997C6.92624 21.7652 6.8212 21.6601 6.6111 21.45L2.55022 17.3892C2.34012 17.1791 2.23508 17.074 2.10053 17.082C1.96598 17.0899 1.8809 17.1979 1.71076 17.414C1.69981 17.4279 1.68906 17.4417 1.67854 17.4555C1.44513 17.7614 1.25104 18.1244 1.25104 18.5811C1.25104 19.0378 1.44513 19.4008 1.67854 19.7067C1.83274 19.9088 2.03441 20.1237 2.26163 20.3546L1.53697 21.0793C1.15532 21.4609 1.15532 22.0797 1.53697 22.4613C1.91862 22.843 2.53739 22.843 2.91904 22.4613L3.64369 21.7367C3.8753 21.9647 4.09087 22.1671 4.29354 22.3217C4.59947 22.5551 4.96243 22.7492 5.41914 22.7492Z" fill="#8F9AA3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
@ -0,0 +1,3 @@
|
||||
<svg width="18" height="24" viewBox="0 0 18 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.00124 0C11.3713 0 13.5174 0.164601 15.0979 0.443605C15.8881 0.58307 16.5336 0.748865 17.0233 0.953187C17.2682 1.05523 17.4752 1.16174 17.6522 1.31444C17.795 1.43765 17.9265 1.61567 17.9745 1.83394C17.997 1.90181 18.0048 1.9739 17.9972 2.04501L17.0012 11.996L15.9975 22.043C15.9672 22.5395 15.5942 22.827 15.1929 23.0391C14.7917 23.2511 14.276 23.417 13.658 23.5565C12.4221 23.8362 10.7723 24.0027 8.95569 23.9999C7.13906 23.9971 5.49826 23.8251 4.27681 23.5411C3.6661 23.3993 3.16459 23.2334 2.77121 23.0177C2.57471 22.9099 2.40312 22.7875 2.26148 22.6271C2.11978 22.4665 2.00417 22.247 2.0038 21.998V22.0511L0.00409739 2.04501C-0.00616554 1.96529 0.00292169 1.88427 0.030564 1.80879C0.0830907 1.60168 0.215339 1.43369 0.352891 1.3149C0.529744 1.16221 0.734914 1.05546 0.979786 0.953417C1.46953 0.748945 2.11501 0.58315 2.90522 0.443836C4.48567 0.164907 6.63123 0 9.00124 0ZM9.00124 0.99817C6.67896 0.99817 4.57689 1.17015 3.08229 1.4337C2.33498 1.56561 1.73987 1.72046 1.36969 1.875C1.25708 1.92187 1.21402 1.95496 1.14902 1.99426C1.21402 2.03395 1.25557 2.07167 1.36969 2.11929C1.73965 2.27312 2.33498 2.4308 3.08229 2.56267C4.57689 2.82648 6.67896 2.9982 9.00124 2.9982C11.3236 2.9982 13.4256 2.82629 14.9202 2.56267C15.6675 2.43076 16.2705 2.27346 16.6406 2.11929C16.7547 2.07167 16.7959 2.03395 16.8613 1.99426C16.7963 1.95458 16.7532 1.92187 16.6406 1.875C16.2706 1.72042 15.6675 1.56557 14.9202 1.4337C13.4256 1.16989 11.3236 0.99817 9.00124 0.99817ZM16.8886 3.08794C16.4149 3.27049 15.8253 3.42253 15.0979 3.55092C13.5174 3.82985 11.3713 3.99429 9.00124 3.99429C6.63123 3.99429 4.48503 3.82992 2.90458 3.55092C2.17718 3.42242 1.58755 3.27037 1.11391 3.08794L1.90087 10.9552C2.34156 10.75 2.91315 10.5837 3.60563 10.4454C5.01028 10.1646 6.90406 9.99623 9.00124 9.99623C11.0985 9.99623 13 10.1644 14.4046 10.4454C15.0971 10.5841 15.6612 10.75 16.1017 10.9552L16.8886 3.08794ZM9.00124 11.0002C6.95818 11.0002 5.10756 11.1661 3.79897 11.4279C3.14469 11.5586 2.62265 11.7139 2.30704 11.8634C2.19594 11.9159 2.14793 11.9563 2.09427 11.996C2.14793 12.0357 2.19518 12.0754 2.30704 12.1287C2.62258 12.278 3.14469 12.4332 3.79897 12.5642C5.10756 12.8258 6.95818 12.994 9.00124 12.994C11.0443 12.994 12.8969 12.8261 14.2055 12.5642C14.8597 12.4334 15.3798 12.2778 15.6954 12.1287C15.8077 12.0758 15.8565 12.0361 15.9101 11.996C15.8561 11.9563 15.8062 11.9155 15.6954 11.8634C15.3799 11.7141 14.8597 11.5588 14.2055 11.4279C12.8969 11.1663 11.0443 11.0002 9.00124 11.0002ZM13.9145 18.4944C13.7965 18.493 13.6817 18.5333 13.5906 18.6084C13.4994 18.6834 13.4378 18.7882 13.4165 18.9043L12.9811 21.0901L11.2997 21.5099C11.2352 21.525 11.1744 21.5526 11.1206 21.5913C11.0669 21.63 11.0214 21.6789 10.9867 21.7353C10.952 21.7917 10.9288 21.8544 10.9184 21.9198C10.9081 21.9852 10.9108 22.052 10.9264 22.1164C10.942 22.1807 10.9702 22.2413 11.0093 22.2947C11.0484 22.3481 11.0978 22.3931 11.1544 22.4273C11.2111 22.4616 11.2739 22.4842 11.3394 22.494C11.4049 22.5038 11.4717 22.5007 11.5359 22.4845L13.5277 21.9826C13.6193 21.9598 13.7025 21.9116 13.7679 21.8435C13.8332 21.7754 13.8779 21.6902 13.8969 21.5978L14.3988 19.1036C14.414 19.0349 14.4145 18.9636 14.4004 18.8946C14.3862 18.8257 14.3576 18.7606 14.3165 18.7034C14.2754 18.6463 14.2227 18.5985 14.1618 18.5631C14.101 18.5278 14.0333 18.5055 13.9633 18.4981C13.9471 18.496 13.9308 18.4949 13.9145 18.4944Z" fill="#4EB5FF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
@ -0,0 +1,5 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 2C3.55229 2 4 2.44772 4 3L4 14C4 15.6782 4.00213 16.8362 4.11923 17.7072C4.17092 18.0917 4.23383 18.5232 4.45131 18.8563C4.60954 18.5142 4.76255 18.1696 4.91553 17.8252C5.29664 16.967 5.67762 16.1092 6.13888 15.2891C6.58328 14.499 7.11953 13.7058 7.77477 13.1011C8.43871 12.4882 9.28219 12.0189 10.3058 12.0189C11.6975 12.0189 12.5705 12.9129 13.1308 13.4867L13.1883 13.5455C13.8415 14.2124 14.1538 14.4717 14.6132 14.4717C15.0859 14.4717 15.4115 14.2943 15.733 13.9424C16.1205 13.5181 16.396 12.9986 16.6658 12.49C16.7134 12.4003 16.7608 12.3109 16.8086 12.2224C17.5056 10.9328 18.5501 9 20.9995 9C21.5518 9 21.9995 9.44772 21.9995 10C21.9995 10.5523 21.5518 11 20.9995 11C19.8601 11 19.3296 11.7648 18.5475 13.2114L18.4738 13.348C18.1403 13.9674 17.7401 14.7105 17.2097 15.2911C16.5941 15.9651 15.7622 16.4717 14.6132 16.4717C13.2476 16.4717 12.3831 15.5847 11.8309 15.0182C11.7865 14.9725 11.7412 14.925 11.6952 14.8766C11.3159 14.4783 10.8784 14.0189 10.3058 14.0189C9.93769 14.0189 9.55589 14.1788 9.13128 14.5707C8.69797 14.9707 8.28242 15.5577 7.88206 16.2696C7.45308 17.0322 7.09983 17.8308 6.7465 18.6295C6.56362 19.043 6.38072 19.4565 6.18729 19.865C6.22215 19.8708 6.25731 19.876 6.2928 19.8808C7.16378 19.9979 8.32182 20 10 20H21C21.5523 20 22 20.4477 22 21C22 21.5523 21.5523 22 21 22H9.928C8.33933 22 7.04616 22.0001 6.0263 21.8629C4.96232 21.7199 4.04736 21.4113 3.31802 20.682C2.58869 19.9526 2.28011 19.0377 2.13706 17.9737C1.99995 16.9539 1.99997 15.6607 2 14.0721L2 3C2 2.44772 2.44772 2 3 2Z" fill="#2E3039"/>
|
||||
<path d="M8 4C8.55229 4 9 3.55228 9 3C9 2.44772 8.55229 2 8 2L7 2C6.44772 2 6 2.44772 6 3C6 3.55228 6.44772 4 7 4L8 4Z" fill="#2E3039"/>
|
||||
<path d="M11 8C11.5523 8 12 7.55228 12 7C12 6.44772 11.5523 6 11 6H7C6.44772 6 6 6.44772 6 7C6 7.55228 6.44772 8 7 8L11 8Z" fill="#2E3039"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@ -0,0 +1,3 @@
|
||||
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 8.0625C0 12.5153 3.60975 16.125 8.0625 16.125C12.5153 16.125 16.125 12.5153 16.125 8.0625C16.125 3.60975 12.5153 0 8.0625 0C3.60975 0 0 3.60975 0 8.0625ZM11.5695 5.25974C11.8748 5.53949 11.895 6.01423 11.6153 6.31948L7.49026 10.8195C7.35226 10.9702 7.158 11.058 6.954 11.0625C6.74925 11.067 6.552 10.9875 6.40725 10.8428L4.53225 8.96777C4.239 8.67527 4.239 8.19973 4.53225 7.90723C4.82475 7.61398 5.30026 7.61398 5.59276 7.90723L6.91425 9.228L10.5097 5.30552C10.7895 5.00027 11.2643 4.97999 11.5695 5.25974Z" fill="#18C273"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 681 B |
@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.5378 19.5227C15.7479 19.3066 15.8082 18.9857 15.6909 18.7081C15.5736 18.4304 15.3014 18.25 15 18.25H13L13 5.75L15 5.75C15.3014 5.75 15.5736 5.56957 15.6909 5.29193C15.8082 5.01429 15.7479 4.69339 15.5378 4.47726C15.4246 4.36071 15.2441 4.13501 14.9904 3.81197C14.7621 3.52108 14.4399 3.11067 14.1661 2.78758C13.8723 2.44084 13.5478 2.08603 13.2266 1.81244C13.0657 1.67538 12.888 1.54343 12.6999 1.44275C12.5177 1.34525 12.2758 1.25 12 1.25C11.7242 1.25 11.4824 1.34525 11.3002 1.44275C11.112 1.54343 10.9344 1.67538 10.7734 1.81244C10.4522 2.08603 10.1277 2.44084 9.8339 2.78758C9.56013 3.11067 9.23797 3.52107 9.00963 3.81197C8.7559 4.13501 8.57549 4.36071 8.46221 4.47726C8.25213 4.69339 8.19185 5.01429 8.30917 5.29193C8.42649 5.56957 8.69861 5.75 9.00002 5.75L11 5.75L11 18.25H9.00002C8.69861 18.25 8.42649 18.4304 8.30917 18.7081C8.19185 18.9857 8.25213 19.3066 8.46221 19.5227C8.57549 19.6393 8.7559 19.865 9.00963 20.188C9.23797 20.4789 9.56014 20.8893 9.8339 21.2124C10.1277 21.5592 10.4522 21.914 10.7734 22.1876C10.9344 22.3246 11.112 22.4566 11.3002 22.5573C11.4824 22.6547 11.7242 22.75 12 22.75C12.2758 22.75 12.5177 22.6547 12.6999 22.5572C12.888 22.4566 13.0657 22.3246 13.2266 22.1876C13.5478 21.914 13.8723 21.5592 14.1661 21.2124C14.4399 20.8893 14.7621 20.4789 14.9904 20.188C15.2441 19.865 15.4246 19.6393 15.5378 19.5227Z" fill="#8F9AA3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@ -0,0 +1,8 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 4C2.44772 4 2 4.44772 2 5C2 5.55228 2.44772 6 3 6L5 6C5.55228 6 6 5.55229 6 5C6 4.44772 5.55228 4 5 4L3 4Z" fill="#2E3039"/>
|
||||
<path d="M9 4C8.44772 4 8 4.44772 8 5C8 5.55228 8.44772 6 9 6L21 6C21.5523 6 22 5.55229 22 5C22 4.44772 21.5523 4 21 4L9 4Z" fill="#2E3039"/>
|
||||
<path d="M8 12C8 11.4477 8.44772 11 9 11L21 11C21.5523 11 22 11.4477 22 12C22 12.5523 21.5523 13 21 13L9 13C8.44772 13 8 12.5523 8 12Z" fill="#2E3039"/>
|
||||
<path d="M3 11C2.44772 11 2 11.4477 2 12C2 12.5523 2.44772 13 3 13H5C5.55228 13 6 12.5523 6 12C6 11.4477 5.55228 11 5 11L3 11Z" fill="#2E3039"/>
|
||||
<path d="M8 19C8 18.4477 8.44772 18 9 18L21 18C21.5523 18 22 18.4477 22 19C22 19.5523 21.5523 20 21 20L9 20C8.44772 20 8 19.5523 8 19Z" fill="#2E3039"/>
|
||||
<path d="M3 18C2.44772 18 2 18.4477 2 19C2 19.5523 2.44772 20 3 20H5C5.55228 20 6 19.5523 6 19C6 18.4477 5.55228 18 5 18H3Z" fill="#2E3039"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 975 B |
@ -0,0 +1,3 @@
|
||||
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.125 0C4.53312 0 0 4.53312 0 10.125C0 15.7169 4.53312 20.25 10.125 20.25C15.7169 20.25 20.25 15.7169 20.25 10.125C20.25 4.53312 15.7169 0 10.125 0ZM6.35756 9.18314C5.83738 9.18314 5.4157 9.60482 5.4157 10.125C5.4157 10.6452 5.83738 11.0669 6.35756 11.0669H13.8924C14.4126 11.0669 14.8343 10.6452 14.8343 10.125C14.8343 9.60482 14.4126 9.18314 13.8924 9.18314H6.35756Z" fill="#ED1C2B"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 541 B |
@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.9998 1.25C7.54394 1.25 3.92765 4.84151 3.92761 9.27697C3.92752 10.3087 3.85814 11.0873 3.38247 11.7872C3.31632 11.8831 3.22855 12.0032 3.13265 12.1345C2.9661 12.3625 2.77503 12.624 2.63009 12.8505C2.37428 13.2503 2.12435 13.7324 2.03843 14.2942C1.75812 16.127 3.05031 17.3136 4.33721 17.8454C8.86992 19.7182 15.1296 19.7182 19.6623 17.8454C20.9492 17.3136 22.2414 16.127 21.9611 14.2942C21.8752 13.7324 21.6252 13.2503 21.3694 12.8505C21.2245 12.624 21.0334 12.3625 20.8669 12.1345C20.771 12.0033 20.6832 11.8832 20.6171 11.7873C20.1414 11.0874 20.072 10.3088 20.0719 9.27703C20.0719 4.84155 16.4556 1.25 11.9998 1.25Z" fill="#8F9AA3"/>
|
||||
<path d="M11.9982 22.7477C13.0185 22.7477 13.974 22.4563 14.7773 21.9511C15.4904 21.5027 15.8469 21.2785 15.7222 20.9057C15.5974 20.533 15.1054 20.5747 14.1215 20.6582C12.7143 20.7776 11.2821 20.7776 9.87493 20.6582C8.89101 20.5747 8.39905 20.533 8.27429 20.9057C8.14953 21.2784 8.50605 21.5027 9.2191 21.9511C10.0225 22.4563 10.978 22.7477 11.9982 22.7477Z" fill="#8F9AA3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@ -0,0 +1,9 @@
|
||||
<svg width="102" height="85" viewBox="0 0 102 85" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M35.6143 34.2284C37.6104 34.2284 39.2286 32.696 39.2286 30.8056C39.2286 28.9153 37.6104 27.3828 35.6143 27.3828C33.6182 27.3828 32 28.9153 32 30.8056C32 32.696 33.6182 34.2284 35.6143 34.2284Z" fill="#CBE7FF" fill-opacity="0.7"/>
|
||||
<path d="M2.40952 61.6106C3.74026 61.6106 4.81904 60.589 4.81904 59.3287C4.81904 58.0685 3.74026 57.0469 2.40952 57.0469C1.07878 57.0469 0 58.0685 0 59.3287C0 60.589 1.07878 61.6106 2.40952 61.6106Z" fill="#CBE7FF" fill-opacity="0.6"/>
|
||||
<path d="M11.4452 85.0015C13.1087 85.0015 14.4571 83.7245 14.4571 82.1492C14.4571 80.5739 13.1087 79.2969 11.4452 79.2969C9.78182 79.2969 8.43335 80.5739 8.43335 82.1492C8.43335 83.7245 9.78182 85.0015 11.4452 85.0015Z" fill="#CBE7FF" fill-opacity="0.5"/>
|
||||
<path d="M72.6927 32.3844C74.3561 32.3844 75.7046 31.1073 75.7046 29.532C75.7046 27.9567 74.3561 26.6797 72.6927 26.6797C71.0293 26.6797 69.6808 27.9567 69.6808 29.532C69.6808 31.1073 71.0293 32.3844 72.6927 32.3844Z" fill="#CBE7FF" fill-opacity="0.7"/>
|
||||
<path d="M99.4095 53.0559C100.74 53.0559 101.819 52.0343 101.819 50.7741C101.819 49.5138 100.74 48.4922 99.4095 48.4922C98.0788 48.4922 97 49.5138 97 50.7741C97 52.0343 98.0788 53.0559 99.4095 53.0559Z" fill="#CBE7FF" fill-opacity="0.6"/>
|
||||
<path d="M88.8681 77.0097C90.8642 77.0097 92.4824 75.4772 92.4824 73.5869C92.4824 71.6965 90.8642 70.1641 88.8681 70.1641C86.872 70.1641 85.2538 71.6965 85.2538 73.5869C85.2538 75.4772 86.872 77.0097 88.8681 77.0097Z" fill="#CBE7FF" fill-opacity="0.5"/>
|
||||
<path d="M13.4095 4.56373C14.7403 4.56373 15.819 3.54211 15.819 2.28187C15.819 1.02163 14.7403 0 13.4095 0C12.0788 0 11 1.02163 11 2.28187C11 3.54211 12.0788 4.56373 13.4095 4.56373Z" fill="#D7EEFF" fill-opacity="0.8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@ -0,0 +1,3 @@
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.75 0C4.81294 0 0 4.81294 0 10.75C0 16.6871 4.81294 21.5 10.75 21.5C16.6871 21.5 21.5 16.6871 21.5 10.75C21.5 4.81294 16.6871 0 10.75 0ZM10.7416 5C8.94576 5 7.48808 6.45407 7.48808 8.25C7.48808 10.0459 8.94576 11.5 10.7416 11.5C12.5375 11.5 13.9952 10.0459 13.9952 8.25C13.9952 6.45407 12.5375 5 10.7416 5ZM15.7909 15.2302C13.1235 12.3502 8.32472 12.4987 5.71382 15.2256L5.52631 15.4131C5.38104 15.5584 5.30172 15.7569 5.30688 15.9623C5.31204 16.1677 5.40122 16.362 5.5536 16.4998C6.92769 17.7423 8.7513 18.5 10.7501 18.5C12.7489 18.5 14.5725 17.7423 15.9466 16.4998C16.099 16.362 16.1882 16.1677 16.1933 15.9623C16.1985 15.7569 16.1192 15.5584 15.9739 15.4131L15.7909 15.2302Z" fill="#8F9AA3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 851 B |
@ -0,0 +1,34 @@
|
||||
<svg width="143" height="315" viewBox="0 0 143 315" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_7567_21248)">
|
||||
<path d="M91.32 30.3281H64.357C56.9114 30.3281 50.8755 35.6934 50.8755 42.3118V52.5836C50.8755 59.202 56.9114 64.5673 64.357 64.5673H91.32C98.7656 64.5673 104.801 59.202 104.801 52.5836V42.3118C104.801 35.6934 98.7656 30.3281 91.32 30.3281Z" fill="#F9FDFF"/>
|
||||
<path d="M96.5104 12.7188H58.0881C51.7221 12.7188 46.5614 17.5366 46.5614 23.4796C46.5614 29.4227 51.7221 34.2405 58.0881 34.2405H96.5104C102.876 34.2405 108.037 29.4227 108.037 23.4796C108.037 17.5366 102.876 12.7188 96.5104 12.7188Z" fill="#CFEDFF"/>
|
||||
<path d="M87.3176 64.5625H68.084C41.5279 64.5625 20 83.9067 20 107.769V280.595C20 304.457 41.5279 323.802 68.084 323.802H87.3176C113.874 323.802 135.402 304.457 135.402 280.595V107.769C135.402 83.9067 113.874 64.5625 87.3176 64.5625Z" fill="url(#paint0_linear_7567_21248)"/>
|
||||
<mask id="mask0_7567_21248" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="22" y="91" width="112" height="229">
|
||||
<path d="M91.626 91.9531H63.7756C40.7036 91.9531 22 109.616 22 131.403V280.438C22 302.225 40.7036 319.888 63.7756 319.888H91.626C114.698 319.888 133.402 302.225 133.402 280.438V131.403C133.402 109.616 114.698 91.9531 91.626 91.9531Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_7567_21248)">
|
||||
<path d="M133.402 92H20.9685V320H133.402V92Z" fill="url(#paint1_linear_7567_21248)"/>
|
||||
</g>
|
||||
<path d="M122.696 130.671C125.516 130.671 127.802 128.506 127.802 125.835C127.802 123.165 125.516 121 122.696 121C119.877 121 117.591 123.165 117.591 125.835C117.591 128.506 119.877 130.671 122.696 130.671Z" fill="#CBE7FF" fill-opacity="0.7"/>
|
||||
<path d="M63.6143 207.228C65.6104 207.228 67.2286 205.696 67.2286 203.806C67.2286 201.915 65.6104 200.383 63.6143 200.383C61.6182 200.383 60 201.915 60 203.806C60 205.696 61.6182 207.228 63.6143 207.228Z" fill="#CBE7FF" fill-opacity="0.7"/>
|
||||
<path d="M30.4095 234.611C31.7403 234.611 32.819 233.589 32.819 232.329C32.819 231.069 31.7403 230.047 30.4095 230.047C29.0788 230.047 28 231.069 28 232.329C28 233.589 29.0788 234.611 30.4095 234.611Z" fill="#CBE7FF" fill-opacity="0.6"/>
|
||||
<path d="M39.4452 258.002C41.1087 258.002 42.4571 256.725 42.4571 255.149C42.4571 253.574 41.1087 252.297 39.4452 252.297C37.7818 252.297 36.4333 253.574 36.4333 255.149C36.4333 256.725 37.7818 258.002 39.4452 258.002Z" fill="#CBE7FF" fill-opacity="0.5"/>
|
||||
<path d="M100.693 205.384C102.356 205.384 103.705 204.107 103.705 202.532C103.705 200.957 102.356 199.68 100.693 199.68C99.0293 199.68 97.6808 200.957 97.6808 202.532C97.6808 204.107 99.0293 205.384 100.693 205.384Z" fill="#CBE7FF" fill-opacity="0.7"/>
|
||||
<path d="M127.41 226.056C128.74 226.056 129.819 225.034 129.819 223.774C129.819 222.514 128.74 221.492 127.41 221.492C126.079 221.492 125 222.514 125 223.774C125 225.034 126.079 226.056 127.41 226.056Z" fill="#CBE7FF" fill-opacity="0.6"/>
|
||||
<path d="M116.868 250.01C118.864 250.01 120.482 248.477 120.482 246.587C120.482 244.697 118.864 243.164 116.868 243.164C114.872 243.164 113.254 244.697 113.254 246.587C113.254 248.477 114.872 250.01 116.868 250.01Z" fill="#CBE7FF" fill-opacity="0.5"/>
|
||||
<path d="M41.4095 177.564C42.7403 177.564 43.819 176.542 43.819 175.282C43.819 174.022 42.7403 173 41.4095 173C40.0788 173 39 174.022 39 175.282C39 176.542 40.0788 177.564 41.4095 177.564Z" fill="#D7EEFF" fill-opacity="0.8"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_7567_21248" x1="77.7008" y1="29.9973" x2="77.7008" y2="323.802" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F9FDFF"/>
|
||||
<stop offset="1" stop-color="#CBE7FF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_7567_21248" x1="77.185" y1="65.1765" x2="77.185" y2="333.412" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#DAF2FF"/>
|
||||
<stop offset="1" stop-color="#AEDCFF"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_7567_21248">
|
||||
<rect width="143" height="315" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.6439 1.25L13.3561 1.25C14.259 1.24997 15.0211 1.24994 15.6249 1.32042C16.255 1.39396 16.8707 1.55847 17.3706 2.00915C17.5958 2.21222 17.7873 2.44899 17.9376 2.71113C18.2326 3.22573 18.2812 3.77334 18.2347 4.31609C19.2103 4.4287 20.0196 4.68604 20.6521 5.31966C21.2538 5.92238 21.5125 6.68113 21.6335 7.58241C21.75 8.4507 21.75 9.55477 21.75 10.9249V16.043C21.75 17.4131 21.75 18.5172 21.6335 19.3855C21.5125 20.2867 21.2538 21.0455 20.6521 21.6482C20.0502 22.2511 19.2923 22.5105 18.3919 22.6318C17.5249 22.7486 16.4225 22.7486 15.055 22.7485L8.94504 22.7485C7.57752 22.7486 6.47513 22.7486 5.60808 22.6318C4.70774 22.5105 3.94976 22.2511 3.3479 21.6482C2.74621 21.0455 2.48747 20.2867 2.3665 19.3855C2.24997 18.5172 2.24998 17.4131 2.25 16.0431L2.25 10.9248C2.24998 9.55478 2.24997 8.45069 2.3665 7.58241C2.48747 6.68113 2.74621 5.92238 3.3479 5.31966C3.98043 4.68604 4.78975 4.4287 5.76534 4.31608C5.71879 3.77334 5.76736 3.22573 6.06239 2.71113C6.21268 2.44899 6.40422 2.21222 6.62944 2.00915C7.1293 1.55847 7.74503 1.39396 8.37506 1.32042C8.97893 1.24994 9.74097 1.24997 10.6439 1.25ZM8.60067 3.2073C8.13641 3.26149 7.99252 3.35241 7.92575 3.4126C7.8493 3.48154 7.78534 3.56095 7.73568 3.64756C7.69637 3.71613 7.64027 3.8608 7.69746 4.30137C7.75673 4.75797 7.91092 5.36081 8.14823 6.27796C8.33725 7.00848 8.46246 7.48783 8.59555 7.84684C8.72171 8.18716 8.82647 8.33584 8.9287 8.43308C9.00978 8.51021 9.10016 8.57847 9.1982 8.63631C9.32461 8.71091 9.5056 8.77465 9.88389 8.81094C10.1378 8.83531 10.4406 8.84448 10.8301 8.84793L11.5514 6.68404C11.726 6.1601 12.2924 5.87694 12.8163 6.05159C13.3402 6.22624 13.6234 6.79256 13.4488 7.3165L12.9378 8.84935C13.4424 8.84743 13.8138 8.83994 14.1161 8.81094C14.4944 8.77465 14.6754 8.71091 14.8018 8.63631C14.8998 8.57847 14.9902 8.51021 15.0713 8.43308C15.1735 8.33584 15.2783 8.18716 15.4045 7.84684C15.5375 7.48783 15.6628 7.00848 15.8518 6.27796C16.0891 5.36081 16.2433 4.75797 16.3025 4.30137C16.3597 3.8608 16.3036 3.71613 16.2643 3.64756C16.2147 3.56095 16.1507 3.48153 16.0742 3.4126C16.0075 3.35241 15.8636 3.26149 15.3993 3.2073C14.9218 3.15157 14.2747 3.15 13.2979 3.15L10.7021 3.15C9.72532 3.15 9.07816 3.15157 8.60067 3.2073ZM10 17C9.44772 17 9 17.4477 9 18C9 18.5523 9.44772 19 10 19H14C14.5523 19 15 18.5523 15 18C15 17.4477 14.5523 17 14 17L10 17Z" fill="#8F9AA3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@ -0,0 +1,3 @@
|
||||
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.0625 16.125C3.6097 16.125 0 12.5153 0 8.0625C0 3.6097 3.6097 0 8.0625 0C12.5153 0 16.125 3.6097 16.125 8.0625C16.125 12.5153 12.5153 16.125 8.0625 16.125ZM8.0625 4.3125C8.47671 4.3125 8.8125 4.64829 8.8125 5.0625V8.43749H8.93401C9.15181 8.43742 9.38226 8.43734 9.56413 8.45912C9.69673 8.47499 10.1846 8.53481 10.4097 8.99153C10.635 9.4486 10.3763 9.85561 10.3061 9.96553C10.2094 10.1169 10.0646 10.2914 9.92764 10.4564L9.90413 10.4848C9.68853 10.7449 9.43178 11.0409 9.1743 11.2764C9.04572 11.394 8.89699 11.5151 8.73553 11.6109C8.58584 11.6996 8.34945 11.8125 8.0625 11.8125C7.77556 11.8125 7.53916 11.6996 7.38947 11.6109C7.22801 11.5151 7.07929 11.394 6.95071 11.2764C6.69322 11.0409 6.43647 10.7449 6.22087 10.4848L6.19736 10.4564C6.06041 10.2914 5.91565 10.1169 5.81894 9.96552C5.74871 9.85561 5.49001 9.4486 5.71529 8.99153C5.94038 8.53481 6.42827 8.47499 6.56087 8.45912C6.74274 8.43734 6.9732 8.43742 7.191 8.43749H7.3125L7.3125 5.0625C7.3125 4.64829 7.64829 4.3125 8.0625 4.3125Z" fill="#FF9E15"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@ -1,191 +0,0 @@
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
|
||||
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
|
||||
|
||||
class LocalNotification {
|
||||
Function(String payload)? _onNotificationClick;
|
||||
static LocalNotification? _instance;
|
||||
|
||||
static LocalNotification? getInstance() {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
static init({required Function(String payload) onNotificationClick}) {
|
||||
if (_instance == null) {
|
||||
_instance = LocalNotification();
|
||||
_instance?._onNotificationClick = onNotificationClick;
|
||||
_instance?._initialize();
|
||||
} else {
|
||||
// assert(false,(){
|
||||
// //TODO fix it
|
||||
// "LocalNotification Already Initialized";
|
||||
// });
|
||||
}
|
||||
}
|
||||
|
||||
_initialize() async {
|
||||
try {
|
||||
var initializationSettingsAndroid = new AndroidInitializationSettings('app_icon');
|
||||
var initializationSettingsIOS = DarwinInitializationSettings();
|
||||
var initializationSettings = InitializationSettings(android: initializationSettingsAndroid, iOS: initializationSettingsIOS);
|
||||
await flutterLocalNotificationsPlugin.initialize(
|
||||
initializationSettings,
|
||||
onDidReceiveNotificationResponse: (NotificationResponse notificationResponse) {
|
||||
switch (notificationResponse.notificationResponseType) {
|
||||
case NotificationResponseType.selectedNotification:
|
||||
// selectNotificationStream.add(notificationResponse.payload);
|
||||
break;
|
||||
case NotificationResponseType.selectedNotificationAction:
|
||||
// if (notificationResponse.actionId == navigationActionId) {
|
||||
// selectNotificationStream.add(notificationResponse.payload);
|
||||
// }
|
||||
break;
|
||||
}
|
||||
},
|
||||
// onDidReceiveBackgroundNotificationResponse: notificationTapBackground,
|
||||
);
|
||||
} catch (ex) {
|
||||
print(ex.toString());
|
||||
}
|
||||
// flutterLocalNotificationsPlugin.initialize(initializationSettings, onDidReceiveNotificationResponse: (NotificationResponse notificationResponse)
|
||||
// {
|
||||
// switch (notificationResponse.notificationResponseType) {
|
||||
// case NotificationResponseType.selectedNotification:
|
||||
// // selectNotificationStream.add(notificationResponse.payload);
|
||||
// break;
|
||||
// case NotificationResponseType.selectedNotificationAction:
|
||||
// // if (notificationResponse.actionId == navigationActionId) {
|
||||
// // selectNotificationStream.add(notificationResponse.payload);
|
||||
// }
|
||||
// // break;
|
||||
// },}
|
||||
//
|
||||
// ,
|
||||
//
|
||||
// );
|
||||
}
|
||||
|
||||
// void notificationTapBackground(NotificationResponse notificationResponse) {
|
||||
// // ignore: avoid_print
|
||||
// print('notification(${notificationResponse.id}) action tapped: '
|
||||
// '${notificationResponse.actionId} with'
|
||||
// ' payload: ${notificationResponse.payload}');
|
||||
// if (notificationResponse.input?.isNotEmpty ?? false) {
|
||||
// // ignore: avoid_print
|
||||
// print('notification action tapped with input: ${notificationResponse.input}');
|
||||
// }
|
||||
// }
|
||||
|
||||
var _random = new Random();
|
||||
|
||||
_randomNumber({int from = 100000}) {
|
||||
return _random.nextInt(from);
|
||||
}
|
||||
|
||||
_vibrationPattern() {
|
||||
var vibrationPattern = Int64List(4);
|
||||
vibrationPattern[0] = 0;
|
||||
vibrationPattern[1] = 1000;
|
||||
vibrationPattern[2] = 5000;
|
||||
vibrationPattern[3] = 2000;
|
||||
|
||||
return vibrationPattern;
|
||||
}
|
||||
|
||||
Future? showNow({required String title, required String subtitle, required String payload}) {
|
||||
Future.delayed(Duration(seconds: 1)).then((result) async {
|
||||
var androidPlatformChannelSpecifics = AndroidNotificationDetails(
|
||||
'com.hmg.local_notification',
|
||||
'HMG',
|
||||
channelDescription: 'HMG',
|
||||
importance: Importance.max,
|
||||
priority: Priority.high,
|
||||
ticker: 'ticker',
|
||||
vibrationPattern: _vibrationPattern(),
|
||||
ongoing: true,
|
||||
autoCancel: false,
|
||||
usesChronometer: true,
|
||||
when: DateTime.now().millisecondsSinceEpoch - 120 * 1000,
|
||||
);
|
||||
var iOSPlatformChannelSpecifics = DarwinNotificationDetails();
|
||||
var platformChannelSpecifics = NotificationDetails(android: androidPlatformChannelSpecifics, iOS: iOSPlatformChannelSpecifics);
|
||||
await flutterLocalNotificationsPlugin.show(25613, title, subtitle, platformChannelSpecifics, payload: payload).catchError((err) {
|
||||
print(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future scheduleNotification({required DateTime scheduledNotificationDateTime, required String title, required String description}) async {
|
||||
///vibrationPattern
|
||||
var vibrationPattern = Int64List(4);
|
||||
vibrationPattern[0] = 0;
|
||||
vibrationPattern[1] = 1000;
|
||||
vibrationPattern[2] = 5000;
|
||||
vibrationPattern[3] = 2000;
|
||||
|
||||
// var androidPlatformChannelSpecifics = AndroidNotificationDetails('active-prescriptions', 'ActivePrescriptions',
|
||||
// channelDescription: 'ActivePrescriptionsDescription',
|
||||
// // icon: 'secondary_icon',
|
||||
// sound: RawResourceAndroidNotificationSound('slow_spring_board'),
|
||||
//
|
||||
// ///change it to be as ionic
|
||||
// // largeIcon: DrawableResourceAndroidBitmap('sample_large_icon'),///change it to be as ionic
|
||||
// vibrationPattern: vibrationPattern,
|
||||
// enableLights: true,
|
||||
// color: const Color.fromARGB(255, 255, 0, 0),
|
||||
// ledColor: const Color.fromARGB(255, 255, 0, 0),
|
||||
// ledOnMs: 1000,
|
||||
// ledOffMs: 500);
|
||||
// var iOSPlatformChannelSpecifics = DarwinNotificationDetails(sound: 'slow_spring_board.aiff');
|
||||
|
||||
// /change it to be as ionic
|
||||
// var platformChannelSpecifics = NotificationDetails(android: androidPlatformChannelSpecifics, iOS: iOSPlatformChannelSpecifics);
|
||||
// await flutterLocalNotificationsPlugin.schedule(0, title, description, scheduledNotificationDateTime, platformChannelSpecifics);
|
||||
}
|
||||
|
||||
///Repeat notification every day at approximately 10:00:00 am
|
||||
Future showDailyAtTime() async {
|
||||
// var time = Time(10, 0, 0);
|
||||
// var androidPlatformChannelSpecifics = AndroidNotificationDetails('repeatDailyAtTime channel id', 'repeatDailyAtTime channel name', channelDescription: 'repeatDailyAtTime description');
|
||||
// var iOSPlatformChannelSpecifics = DarwinNotificationDetails();
|
||||
// var platformChannelSpecifics = NotificationDetails(
|
||||
// androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics);
|
||||
// await flutterLocalNotificationsPlugin.showDailyAtTime(
|
||||
// 0,
|
||||
// 'show daily title',
|
||||
// 'Daily notification shown at approximately ${_toTwoDigitString(time.hour)}:${_toTwoDigitString(time.minute)}:${_toTwoDigitString(time.second)}',
|
||||
// time,
|
||||
// platformChannelSpecifics);
|
||||
}
|
||||
|
||||
///Repeat notification weekly on Monday at approximately 10:00:00 am
|
||||
Future showWeeklyAtDayAndTime() async {
|
||||
// var time = Time(10, 0, 0);
|
||||
// var androidPlatformChannelSpecifics = AndroidNotificationDetails('show weekly channel id', 'show weekly channel name', channelDescription: 'show weekly description');
|
||||
// var iOSPlatformChannelSpecifics = DarwinNotificationDetails();
|
||||
// var platformChannelSpecifics = NotificationDetails(
|
||||
// androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics);
|
||||
// await flutterLocalNotificationsPlugin.showWeeklyAtDayAndTime(
|
||||
// 0,
|
||||
// 'show weekly title',
|
||||
// 'Weekly notification shown on Monday at approximately ${_toTwoDigitString(time.hour)}:${_toTwoDigitString(time.minute)}:${_toTwoDigitString(time.second)}',
|
||||
// Day.Monday,
|
||||
// time,
|
||||
// platformChannelSpecifics);
|
||||
}
|
||||
|
||||
String _toTwoDigitString(int value) {
|
||||
return value.toString().padLeft(2, '0');
|
||||
}
|
||||
|
||||
Future cancelNotification() async {
|
||||
await flutterLocalNotificationsPlugin.cancel(0);
|
||||
}
|
||||
|
||||
Future cancelAllNotifications() async {
|
||||
await flutterLocalNotificationsPlugin.cancelAll();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
class RiskAndSuggestionsResponseModel {
|
||||
final List<RiskAndSuggestionsItemModel>? dataDetails;
|
||||
|
||||
RiskAndSuggestionsResponseModel({this.dataDetails});
|
||||
|
||||
factory RiskAndSuggestionsResponseModel.fromJson(Map<String, dynamic> json) {
|
||||
return RiskAndSuggestionsResponseModel(
|
||||
dataDetails:
|
||||
json['dataDetails'] != null ? (json['dataDetails'] as List).map((item) => RiskAndSuggestionsItemModel.fromJson(item)).toList() : null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'dataDetails': dataDetails?.map((item) => item.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class RiskAndSuggestionsItemModel {
|
||||
final String? id;
|
||||
final String? type;
|
||||
final String? name;
|
||||
final String? commonName;
|
||||
final String? language;
|
||||
|
||||
RiskAndSuggestionsItemModel({
|
||||
this.id,
|
||||
this.type,
|
||||
this.name,
|
||||
this.commonName,
|
||||
this.language,
|
||||
});
|
||||
|
||||
factory RiskAndSuggestionsItemModel.fromJson(Map<String, dynamic> json) {
|
||||
return RiskAndSuggestionsItemModel(
|
||||
id: json['id'],
|
||||
type: json['type'],
|
||||
name: json['name'],
|
||||
commonName: json['common_name'],
|
||||
language: json['language'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'type': type,
|
||||
'name': name,
|
||||
'common_name': commonName,
|
||||
'language': language,
|
||||
};
|
||||
}
|
||||
|
||||
// Helper method to get display name
|
||||
String getDisplayName() {
|
||||
return commonName ?? name ?? '';
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
class InsertUserActivityRequestModel {
|
||||
String? identificationNo;
|
||||
String? mobileNumber;
|
||||
num? quantityIntake;
|
||||
|
||||
InsertUserActivityRequestModel({
|
||||
this.identificationNo,
|
||||
this.mobileNumber,
|
||||
this.quantityIntake,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
if (identificationNo != null) map['IdentificationNo'] = identificationNo;
|
||||
if (mobileNumber != null) map['MobileNumber'] = mobileNumber;
|
||||
if (quantityIntake != null) map['QuantityIntake'] = quantityIntake;
|
||||
return map;
|
||||
}
|
||||
|
||||
factory InsertUserActivityRequestModel.fromJson(Map<String, dynamic> json) => InsertUserActivityRequestModel(
|
||||
identificationNo: json['IdentificationNo']?.toString(),
|
||||
mobileNumber: json['MobileNumber']?.toString(),
|
||||
quantityIntake: json['QuantityIntake'],
|
||||
);
|
||||
}
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
class UndoUserActivityRequestModel {
|
||||
num? progress;
|
||||
String? mobileNumber;
|
||||
String? identificationNo;
|
||||
|
||||
UndoUserActivityRequestModel({
|
||||
this.progress,
|
||||
this.mobileNumber,
|
||||
this.identificationNo,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
if (progress != null) map['Progress'] = progress;
|
||||
if (mobileNumber != null) map['MobileNumber'] = mobileNumber;
|
||||
if (identificationNo != null) map['IdentificationNo'] = identificationNo;
|
||||
return map;
|
||||
}
|
||||
|
||||
factory UndoUserActivityRequestModel.fromJson(Map<String, dynamic> json) => UndoUserActivityRequestModel(
|
||||
progress: json['Progress'],
|
||||
mobileNumber: json['MobileNumber']?.toString(),
|
||||
identificationNo: json['IdentificationNo']?.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
@ -0,0 +1,88 @@
|
||||
class UpdateUserDetailRequestModel {
|
||||
// User detail fields
|
||||
String? activityID;
|
||||
String? dOB;
|
||||
String? email;
|
||||
String? firstName;
|
||||
String? lastName;
|
||||
String? firstNameN;
|
||||
String? middleName;
|
||||
String? middleNameN;
|
||||
String? lastNameN;
|
||||
String? gender;
|
||||
num? height;
|
||||
bool? isHeightInCM;
|
||||
bool? isWeightInKG;
|
||||
String? zipCode;
|
||||
num? weight;
|
||||
bool? isNotificationOn;
|
||||
String? mobileNumber;
|
||||
String? identificationNo;
|
||||
|
||||
UpdateUserDetailRequestModel({
|
||||
this.activityID,
|
||||
this.dOB,
|
||||
this.email,
|
||||
this.firstName,
|
||||
this.lastName,
|
||||
this.firstNameN,
|
||||
this.middleName,
|
||||
this.middleNameN,
|
||||
this.lastNameN,
|
||||
this.gender,
|
||||
this.height,
|
||||
this.isHeightInCM,
|
||||
this.isWeightInKG,
|
||||
this.zipCode,
|
||||
this.weight,
|
||||
this.isNotificationOn,
|
||||
this.mobileNumber,
|
||||
this.identificationNo,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
|
||||
// User detail fields
|
||||
if (activityID != null) map['ActivityID'] = activityID;
|
||||
if (dOB != null) map['DOB'] = dOB;
|
||||
if (email != null) map['Email'] = email;
|
||||
if (firstName != null) map['FirstName'] = firstName;
|
||||
if (lastName != null) map['LastName'] = lastName;
|
||||
if (firstNameN != null) map['FirstNameN'] = firstNameN;
|
||||
if (middleName != null) map['MiddleName'] = middleName;
|
||||
if (middleNameN != null) map['MiddleNameN'] = middleNameN;
|
||||
if (lastNameN != null) map['LastNameN'] = lastNameN;
|
||||
if (gender != null) map['Gender'] = gender;
|
||||
if (height != null) map['Height'] = height;
|
||||
if (isHeightInCM != null) map['IsHeightInCM'] = isHeightInCM;
|
||||
if (isWeightInKG != null) map['IsWeightInKG'] = isWeightInKG;
|
||||
if (zipCode != null) map['ZipCode'] = zipCode;
|
||||
if (weight != null) map['Weight'] = weight;
|
||||
if (isNotificationOn != null) map['IsNotificationOn'] = isNotificationOn;
|
||||
if (mobileNumber != null) map['MobileNumber'] = mobileNumber;
|
||||
if (identificationNo != null) map['IdentificationNo'] = identificationNo;
|
||||
return map;
|
||||
}
|
||||
|
||||
factory UpdateUserDetailRequestModel.fromJson(Map<String, dynamic> json) => UpdateUserDetailRequestModel(
|
||||
activityID: json['ActivityID']?.toString(),
|
||||
dOB: json['DOB']?.toString(),
|
||||
email: json['Email']?.toString(),
|
||||
firstName: json['FirstName']?.toString(),
|
||||
lastName: json['LastName']?.toString(),
|
||||
firstNameN: json['FirstNameN']?.toString(),
|
||||
middleName: json['MiddleName']?.toString(),
|
||||
middleNameN: json['MiddleNameN']?.toString(),
|
||||
lastNameN: json['LastNameN']?.toString(),
|
||||
gender: json['Gender']?.toString(),
|
||||
height: json['Height'],
|
||||
isHeightInCM: json['IsHeightInCM'] as bool?,
|
||||
isWeightInKG: json['IsWeightInKG'] as bool?,
|
||||
zipCode: json['ZipCode']?.toString(),
|
||||
weight: json['Weight'],
|
||||
isNotificationOn: json['IsNotificationOn'] as bool?,
|
||||
mobileNumber: json['MobileNumber']?.toString(),
|
||||
identificationNo: json['IdentificationNo']?.toString(),
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,111 @@
|
||||
/// Model for today's water progress data
|
||||
class UserProgressForTodayModel {
|
||||
num? quantityConsumed;
|
||||
num? percentageConsumed;
|
||||
num? percentageLeft;
|
||||
num? quantityLimit;
|
||||
|
||||
UserProgressForTodayModel({
|
||||
this.quantityConsumed,
|
||||
this.percentageConsumed,
|
||||
this.percentageLeft,
|
||||
this.quantityLimit,
|
||||
});
|
||||
|
||||
factory UserProgressForTodayModel.fromJson(Map<String, dynamic> json) => UserProgressForTodayModel(
|
||||
quantityConsumed: json['QuantityConsumed'],
|
||||
percentageConsumed: json['PercentageConsumed'],
|
||||
percentageLeft: json['PercentageLeft'],
|
||||
quantityLimit: json['QuantityLimit'],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'QuantityConsumed': quantityConsumed,
|
||||
'PercentageConsumed': percentageConsumed,
|
||||
'PercentageLeft': percentageLeft,
|
||||
'QuantityLimit': quantityLimit,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Model for weekly water progress data
|
||||
class UserProgressForWeekModel {
|
||||
int? dayNumber;
|
||||
String? dayDate;
|
||||
String? dayName;
|
||||
num? percentageConsumed;
|
||||
|
||||
UserProgressForWeekModel({
|
||||
this.dayNumber,
|
||||
this.dayDate,
|
||||
this.dayName,
|
||||
this.percentageConsumed,
|
||||
});
|
||||
|
||||
factory UserProgressForWeekModel.fromJson(Map<String, dynamic> json) => UserProgressForWeekModel(
|
||||
dayNumber: json['DayNumber'] as int?,
|
||||
dayDate: json['DayDate']?.toString(),
|
||||
dayName: json['DayName']?.toString(),
|
||||
percentageConsumed: json['PercentageConsumed'],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'DayNumber': dayNumber,
|
||||
'DayDate': dayDate,
|
||||
'DayName': dayName,
|
||||
'PercentageConsumed': percentageConsumed,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Model for monthly water progress data
|
||||
class UserProgressForMonthModel {
|
||||
int? monthNumber;
|
||||
String? monthName;
|
||||
num? percentageConsumed;
|
||||
|
||||
UserProgressForMonthModel({
|
||||
this.monthNumber,
|
||||
this.monthName,
|
||||
this.percentageConsumed,
|
||||
});
|
||||
|
||||
factory UserProgressForMonthModel.fromJson(Map<String, dynamic> json) => UserProgressForMonthModel(
|
||||
monthNumber: json['MonthNumber'] as int?,
|
||||
monthName: json['MonthName']?.toString(),
|
||||
percentageConsumed: json['PercentageConsumed'],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'MonthNumber': monthNumber,
|
||||
'MonthName': monthName,
|
||||
'PercentageConsumed': percentageConsumed,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Model for user progress history data
|
||||
class UserProgressHistoryModel {
|
||||
num? quantity;
|
||||
String? createdDate;
|
||||
|
||||
UserProgressHistoryModel({
|
||||
this.quantity,
|
||||
this.createdDate,
|
||||
});
|
||||
|
||||
factory UserProgressHistoryModel.fromJson(Map<String, dynamic> json) => UserProgressHistoryModel(
|
||||
quantity: json['Quantity'],
|
||||
createdDate: json['CreatedDate']?.toString(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'Quantity': quantity,
|
||||
'CreatedDate': createdDate,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
class WaterCupModel {
|
||||
final String id;
|
||||
final String name;
|
||||
final int capacityMl;
|
||||
final String iconPath; // or use IconData if you prefer
|
||||
final bool isDefault;
|
||||
|
||||
WaterCupModel({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.capacityMl,
|
||||
required this.iconPath,
|
||||
this.isDefault = false,
|
||||
});
|
||||
|
||||
WaterCupModel copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
int? capacityMl,
|
||||
String? iconPath,
|
||||
bool? isDefault,
|
||||
}) {
|
||||
return WaterCupModel(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
capacityMl: capacityMl ?? this.capacityMl,
|
||||
iconPath: iconPath ?? this.iconPath,
|
||||
isDefault: isDefault ?? this.isDefault,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'capacityMl': capacityMl,
|
||||
'iconPath': iconPath,
|
||||
'isDefault': isDefault,
|
||||
};
|
||||
|
||||
factory WaterCupModel.fromJson(Map<String, dynamic> json) => WaterCupModel(
|
||||
id: json['id'],
|
||||
name: json['name'],
|
||||
capacityMl: json['capacityMl'],
|
||||
iconPath: json['iconPath'],
|
||||
isDefault: json['isDefault'] ?? false,
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,333 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:hmg_patient_app_new/core/api/api_client.dart';
|
||||
import 'package:hmg_patient_app_new/core/api_consts.dart';
|
||||
import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart';
|
||||
import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart';
|
||||
import 'package:hmg_patient_app_new/features/water_monitor/models/insert_user_activity_request_model.dart';
|
||||
import 'package:hmg_patient_app_new/features/water_monitor/models/undo_user_activity_request_model.dart';
|
||||
import 'package:hmg_patient_app_new/features/water_monitor/models/update_user_detail_request_model.dart';
|
||||
import 'package:hmg_patient_app_new/services/logger_service.dart';
|
||||
|
||||
/// Progress types to request different ranges from the progress API.
|
||||
enum ProgressType { today, week, month }
|
||||
|
||||
abstract class WaterMonitorRepo {
|
||||
/// Fetch user details for water monitoring.
|
||||
/// The request will include the standard payload injected by ApiClient and
|
||||
/// additionally these three parameters: Progress, MobileNumber, IdentificationNo.
|
||||
Future<Either<Failure, GenericApiModel<dynamic>>> getUserDetailsForWaterMonitoring({
|
||||
required num progress,
|
||||
required String mobileNumber,
|
||||
required String identificationNo,
|
||||
});
|
||||
|
||||
/// Fetch user progress for water monitoring (H2O_GetUserProgress).
|
||||
Future<Either<Failure, GenericApiModel<dynamic>>> getUserProgressForWaterMonitoring({
|
||||
required ProgressType progressType,
|
||||
required String mobileNumber,
|
||||
required String identificationNo,
|
||||
});
|
||||
|
||||
/// Update user details for water monitoring.
|
||||
Future<Either<Failure, GenericApiModel<dynamic>>> updateOrInsertUserDetailForWaterMonitoring(
|
||||
{required UpdateUserDetailRequestModel requestModel, bool isUpdate = false});
|
||||
|
||||
/// Insert user activity (water intake).
|
||||
Future<Either<Failure, GenericApiModel<dynamic>>> insertUserActivity({
|
||||
required InsertUserActivityRequestModel requestModel,
|
||||
});
|
||||
|
||||
/// Undo last user activity.
|
||||
Future<Either<Failure, GenericApiModel<dynamic>>> undoUserActivity({
|
||||
required UndoUserActivityRequestModel requestModel,
|
||||
});
|
||||
}
|
||||
|
||||
class WaterMonitorRepoImp implements WaterMonitorRepo {
|
||||
final ApiClient apiClient;
|
||||
final LoggerService loggerService;
|
||||
|
||||
WaterMonitorRepoImp({required this.loggerService, required this.apiClient});
|
||||
|
||||
@override
|
||||
Future<Either<Failure, GenericApiModel<dynamic>>> getUserDetailsForWaterMonitoring({
|
||||
required num progress,
|
||||
required String mobileNumber,
|
||||
required String identificationNo,
|
||||
}) async {
|
||||
Map<String, dynamic> request = {
|
||||
"Progress": progress,
|
||||
"MobileNumber": mobileNumber,
|
||||
"IdentificationNo": identificationNo,
|
||||
};
|
||||
|
||||
try {
|
||||
GenericApiModel<dynamic>? apiResponse;
|
||||
Failure? failure;
|
||||
await apiClient.post(
|
||||
ApiConsts.h2oGetUserDetail,
|
||||
body: request,
|
||||
onFailure: (error, statusCode, {messageStatus, failureType}) {
|
||||
failure = failureType;
|
||||
},
|
||||
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
|
||||
try {
|
||||
// Extract only the specific key from the API response as requested
|
||||
dynamic extracted;
|
||||
if (response is Map && response.containsKey('UserDetailData_New')) {
|
||||
extracted = response['UserDetailData_New'];
|
||||
} else {
|
||||
extracted = null;
|
||||
}
|
||||
|
||||
apiResponse = GenericApiModel<dynamic>(
|
||||
messageStatus: messageStatus,
|
||||
statusCode: statusCode,
|
||||
errorMessage: errorMessage,
|
||||
data: extracted,
|
||||
);
|
||||
} catch (e) {
|
||||
failure = DataParsingFailure(e.toString());
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (failure != null) return Left(failure!);
|
||||
if (apiResponse == null) return Left(ServerFailure("Unknown error"));
|
||||
return Right(apiResponse!);
|
||||
} catch (e) {
|
||||
return Left(UnknownFailure(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, GenericApiModel<dynamic>>> getUserProgressForWaterMonitoring({
|
||||
required ProgressType progressType,
|
||||
required String mobileNumber,
|
||||
required String identificationNo,
|
||||
}) async {
|
||||
final progressValue = progressType == ProgressType.today ? 1 : (progressType == ProgressType.week ? 2 : 3);
|
||||
|
||||
Map<String, dynamic> request = {
|
||||
"Progress": progressValue,
|
||||
"MobileNumber": mobileNumber,
|
||||
"IdentificationNo": identificationNo,
|
||||
};
|
||||
|
||||
try {
|
||||
GenericApiModel<dynamic>? apiResponse;
|
||||
Failure? failure;
|
||||
await apiClient.post(
|
||||
ApiConsts.h2oGetUserProgress,
|
||||
body: request,
|
||||
onFailure: (error, statusCode, {messageStatus, failureType}) {
|
||||
failure = failureType;
|
||||
},
|
||||
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
|
||||
try {
|
||||
// Extract progress data and history data
|
||||
dynamic extracted;
|
||||
dynamic historyData;
|
||||
|
||||
if (response is Map) {
|
||||
// Extract history data (available for all progress types)
|
||||
if (response.containsKey('UserProgressHistoryData')) {
|
||||
historyData = response['UserProgressHistoryData'];
|
||||
}
|
||||
|
||||
// Extract progress data based on type
|
||||
switch (progressType) {
|
||||
case ProgressType.today:
|
||||
if (response.containsKey('UserProgressForTodayData')) {
|
||||
extracted = response['UserProgressForTodayData'];
|
||||
}
|
||||
break;
|
||||
case ProgressType.week:
|
||||
if (response.containsKey('UserProgressForWeekData')) {
|
||||
extracted = response['UserProgressForWeekData'];
|
||||
}
|
||||
break;
|
||||
case ProgressType.month:
|
||||
if (response.containsKey('UserProgressForMonthData')) {
|
||||
extracted = response['UserProgressForMonthData'];
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// fallbacks
|
||||
if (extracted == null) {
|
||||
if (response.containsKey('UserProgress')) {
|
||||
extracted = response['UserProgress'];
|
||||
} else if (response.containsKey('UserProgressData_New')) {
|
||||
extracted = response['UserProgressData_New'];
|
||||
} else {
|
||||
extracted = response;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
extracted = response;
|
||||
}
|
||||
|
||||
// Package both progress data and history data
|
||||
final combinedData = {
|
||||
'progressData': extracted,
|
||||
'historyData': historyData,
|
||||
};
|
||||
|
||||
apiResponse = GenericApiModel<dynamic>(
|
||||
messageStatus: messageStatus,
|
||||
statusCode: statusCode,
|
||||
errorMessage: errorMessage,
|
||||
data: combinedData,
|
||||
);
|
||||
} catch (e) {
|
||||
failure = DataParsingFailure(e.toString());
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (failure != null) return Left(failure!);
|
||||
if (apiResponse == null) return Left(ServerFailure("Unknown error"));
|
||||
return Right(apiResponse!);
|
||||
} catch (e) {
|
||||
return Left(UnknownFailure(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, GenericApiModel<dynamic>>> updateOrInsertUserDetailForWaterMonitoring({
|
||||
required UpdateUserDetailRequestModel requestModel,
|
||||
bool isUpdate = false,
|
||||
}) async {
|
||||
try {
|
||||
GenericApiModel<dynamic>? apiResponse;
|
||||
Failure? failure;
|
||||
|
||||
// Use different endpoint based on isUpdate flag
|
||||
final endpoint = isUpdate ? ApiConsts.h2oUpdateUserDetail : ApiConsts.h2oInsertUserDetailsNew;
|
||||
await apiClient.post(
|
||||
endpoint,
|
||||
body: requestModel.toJson(),
|
||||
onFailure: (error, statusCode, {messageStatus, failureType}) {
|
||||
failure = failureType;
|
||||
},
|
||||
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
|
||||
try {
|
||||
dynamic extracted;
|
||||
if (response is Map && response.containsKey('UserDetailData_New')) {
|
||||
extracted = response['UserDetailData_New'];
|
||||
} else {
|
||||
extracted = null;
|
||||
}
|
||||
|
||||
apiResponse = GenericApiModel<dynamic>(
|
||||
messageStatus: messageStatus,
|
||||
statusCode: statusCode,
|
||||
errorMessage: errorMessage,
|
||||
data: extracted,
|
||||
);
|
||||
} catch (e) {
|
||||
failure = DataParsingFailure(e.toString());
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (failure != null) return Left(failure!);
|
||||
if (apiResponse == null) return Left(ServerFailure("Unknown error"));
|
||||
return Right(apiResponse!);
|
||||
} catch (e) {
|
||||
return Left(UnknownFailure(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, GenericApiModel<dynamic>>> insertUserActivity({
|
||||
required InsertUserActivityRequestModel requestModel,
|
||||
}) async {
|
||||
try {
|
||||
GenericApiModel<dynamic>? apiResponse;
|
||||
Failure? failure;
|
||||
|
||||
await apiClient.post(
|
||||
ApiConsts.h2oInsertUserActivity,
|
||||
body: requestModel.toJson(),
|
||||
onFailure: (error, statusCode, {messageStatus, failureType}) {
|
||||
failure = failureType;
|
||||
},
|
||||
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
|
||||
try {
|
||||
// Extract UserProgressForTodayData from the response
|
||||
dynamic extracted;
|
||||
if (response is Map && response.containsKey('UserProgressForTodayData')) {
|
||||
extracted = response['UserProgressForTodayData'];
|
||||
} else {
|
||||
extracted = response;
|
||||
}
|
||||
|
||||
apiResponse = GenericApiModel<dynamic>(
|
||||
messageStatus: messageStatus,
|
||||
statusCode: statusCode,
|
||||
errorMessage: errorMessage,
|
||||
data: extracted,
|
||||
);
|
||||
} catch (e) {
|
||||
failure = DataParsingFailure(e.toString());
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (failure != null) return Left(failure!);
|
||||
if (apiResponse == null) return Left(ServerFailure("Unknown error"));
|
||||
return Right(apiResponse!);
|
||||
} catch (e) {
|
||||
return Left(UnknownFailure(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, GenericApiModel<dynamic>>> undoUserActivity({
|
||||
required UndoUserActivityRequestModel requestModel,
|
||||
}) async {
|
||||
try {
|
||||
GenericApiModel<dynamic>? apiResponse;
|
||||
Failure? failure;
|
||||
|
||||
await apiClient.post(
|
||||
ApiConsts.h2oUndoUserActivity,
|
||||
body: requestModel.toJson(),
|
||||
onFailure: (error, statusCode, {messageStatus, failureType}) {
|
||||
failure = failureType;
|
||||
},
|
||||
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
|
||||
try {
|
||||
log("response h2oUndoUserActivity: ${response.toString()}");
|
||||
// Extract UserProgressForTodayData from the response
|
||||
dynamic extracted;
|
||||
if (response is Map && response.containsKey('UserProgressForTodayData')) {
|
||||
extracted = response['UserProgressForTodayData'];
|
||||
} else {
|
||||
extracted = response;
|
||||
}
|
||||
|
||||
apiResponse = GenericApiModel<dynamic>(
|
||||
messageStatus: messageStatus,
|
||||
statusCode: statusCode,
|
||||
errorMessage: errorMessage,
|
||||
data: extracted,
|
||||
);
|
||||
} catch (e) {
|
||||
failure = DataParsingFailure(e.toString());
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (failure != null) return Left(failure!);
|
||||
if (apiResponse == null) return Left(ServerFailure("Unknown error"));
|
||||
return Right(apiResponse!);
|
||||
} catch (e) {
|
||||
return Left(UnknownFailure(e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,829 @@
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hmg_patient_app_new/core/app_assets.dart';
|
||||
import 'package:hmg_patient_app_new/core/app_export.dart';
|
||||
import 'package:hmg_patient_app_new/core/common_models/data_points.dart';
|
||||
import 'package:hmg_patient_app_new/core/dependencies.dart';
|
||||
import 'package:hmg_patient_app_new/core/utils/date_util.dart';
|
||||
import 'package:hmg_patient_app_new/core/utils/utils.dart';
|
||||
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
|
||||
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
|
||||
import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart';
|
||||
import 'package:hmg_patient_app_new/presentation/water_monitor/widgets/hydration_tips_widget.dart';
|
||||
import 'package:hmg_patient_app_new/presentation/water_monitor/widgets/water_intake_summary_widget.dart';
|
||||
import 'package:hmg_patient_app_new/services/dialog_service.dart';
|
||||
import 'package:hmg_patient_app_new/theme/colors.dart';
|
||||
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
|
||||
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
|
||||
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
|
||||
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
|
||||
import 'package:hmg_patient_app_new/widgets/graph/custom_graph.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
|
||||
class WaterConsumptionScreen extends StatefulWidget {
|
||||
const WaterConsumptionScreen({super.key});
|
||||
|
||||
@override
|
||||
State<WaterConsumptionScreen> createState() => _WaterConsumptionScreenState();
|
||||
}
|
||||
|
||||
class _WaterConsumptionScreenState extends State<WaterConsumptionScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
await _refreshData();
|
||||
});
|
||||
}
|
||||
|
||||
/// Refresh data by calling initialize on the view model
|
||||
Future<void> _refreshData() async {
|
||||
final vm = context.read<WaterMonitorViewModel>();
|
||||
await vm.initialize();
|
||||
}
|
||||
|
||||
Widget _buildLoadingShimmer({bool isForHistory = true}) {
|
||||
return ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
padding: EdgeInsets.all(0.w),
|
||||
itemCount: 4,
|
||||
separatorBuilder: (_, __) => SizedBox(height: 12.h),
|
||||
itemBuilder: (context, index) {
|
||||
return Shimmer.fromColors(
|
||||
baseColor: AppColors.shimmerBaseColor,
|
||||
highlightColor: AppColors.shimmerHighlightColor,
|
||||
child: Container(
|
||||
height: isForHistory ? 60.h : 40.h,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.whiteColor,
|
||||
borderRadius: BorderRadius.circular(10.r),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildHistoryListTile({required String title, required String subTitle}) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
title.toText14(weight: FontWeight.w500, color: AppColors.labelTextColor),
|
||||
subTitle.toText18(weight: FontWeight.w600),
|
||||
],
|
||||
),
|
||||
Utils.buildSvgWithAssets(icon: AppAssets.greenTickIcon)
|
||||
],
|
||||
).paddingSymmetrical(0, 8.h);
|
||||
}
|
||||
|
||||
Widget _buildHistoryGraphOrList() {
|
||||
return Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 24.w),
|
||||
padding: EdgeInsets.all(16.h),
|
||||
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
|
||||
color: AppColors.whiteColor,
|
||||
borderRadius: 24.r,
|
||||
hasShadow: true,
|
||||
),
|
||||
child: Consumer(builder: (BuildContext context, WaterMonitorViewModel viewModel, Widget? child) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
"History".needTranslation.toText16(isBold: true),
|
||||
Row(
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () => viewModel.setGraphView(!viewModel.isGraphView),
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
transitionBuilder: (Widget child, Animation<double> animation) {
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: ScaleTransition(
|
||||
scale: animation,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
key: ValueKey<bool>(viewModel.isGraphView),
|
||||
child: Utils.buildSvgWithAssets(
|
||||
icon: viewModel.isGraphView ? AppAssets.listIcon : AppAssets.graphIcon,
|
||||
height: 24.h,
|
||||
width: 24.h,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
InkWell(
|
||||
onTap: () => _showHistoryDurationBottomsheet(context, viewModel),
|
||||
child: Utils.buildSvgWithAssets(icon: AppAssets.doctor_calendar_icon, height: 24.h, width: 24.h))
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 12.h),
|
||||
if (!viewModel.isGraphView) _buildHistoryListView(viewModel) else _buildHistoryGraph()
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHistoryListView(WaterMonitorViewModel viewModel) {
|
||||
final selectedDuration = viewModel.selectedDurationFilter;
|
||||
|
||||
// Build list items based on duration
|
||||
List<Widget> listItems = [];
|
||||
|
||||
if (selectedDuration == 'Daily') {
|
||||
if (viewModel.todayProgressList.isNotEmpty) {
|
||||
final todayData = viewModel.todayProgressList.first;
|
||||
listItems.add(
|
||||
buildHistoryListTile(
|
||||
title: "Today's Progress",
|
||||
subTitle: "${todayData.quantityConsumed?.toStringAsFixed(0) ?? '0'} ml / ${todayData.quantityLimit?.toStringAsFixed(0) ?? '0'} ml",
|
||||
),
|
||||
);
|
||||
listItems.add(Divider(height: 1, color: AppColors.dividerColor));
|
||||
listItems.add(
|
||||
buildHistoryListTile(
|
||||
title: "Percentage Completed",
|
||||
subTitle: "${todayData.percentageConsumed?.toStringAsFixed(1) ?? '0'}%",
|
||||
),
|
||||
);
|
||||
|
||||
// Add history data if available (show ALL entries)
|
||||
if (viewModel.historyList.isNotEmpty) {
|
||||
listItems.add(Divider(height: 1, color: AppColors.dividerColor));
|
||||
listItems.add(
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8.h),
|
||||
child: "Water Intake History".toText14(
|
||||
weight: FontWeight.w600,
|
||||
color: AppColors.textColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Show all history entries
|
||||
for (var history in viewModel.historyList) {
|
||||
final quantity = "${history.quantity?.toStringAsFixed(0) ?? '0'} ml";
|
||||
final time = _formatHistoryDate(history.createdDate ?? '');
|
||||
|
||||
listItems.add(
|
||||
buildHistoryListTile(
|
||||
title: quantity,
|
||||
subTitle: time,
|
||||
),
|
||||
);
|
||||
|
||||
if (history != viewModel.historyList.last) {
|
||||
listItems.add(Divider(height: 1, color: AppColors.dividerColor));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
listItems.add(
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16.h),
|
||||
child: "No data available for today".toText14(color: AppColors.greyTextColor),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (selectedDuration == 'Weekly') {
|
||||
if (viewModel.weekProgressList.isNotEmpty) {
|
||||
// Show previous 6 days + today (total 7 days)
|
||||
// API returns data in reverse order (today first), so we reverse it to show oldest to newest (top to bottom)
|
||||
// This ensures today appears at the end (bottom)
|
||||
final totalDays = viewModel.weekProgressList.length;
|
||||
final startIndex = totalDays > 7 ? totalDays - 7 : 0;
|
||||
final weekDataToShow = viewModel.weekProgressList.skip(startIndex).toList().reversed.toList();
|
||||
|
||||
for (var dayData in weekDataToShow) {
|
||||
listItems.add(
|
||||
buildHistoryListTile(
|
||||
title: dayData.dayName ?? 'Unknown',
|
||||
subTitle: "${dayData.percentageConsumed?.toStringAsFixed(1) ?? '0'}%",
|
||||
),
|
||||
);
|
||||
if (dayData != weekDataToShow.last) {
|
||||
listItems.add(Divider(height: 1, color: AppColors.dividerColor));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
listItems.add(
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16.h),
|
||||
child: "No data available for this week".toText14(color: AppColors.greyTextColor),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (selectedDuration == 'Monthly') {
|
||||
if (viewModel.monthProgressList.isNotEmpty) {
|
||||
// Show last 6 months + current month (total 7 months)
|
||||
// Show in chronological order: oldest to newest (top to bottom)
|
||||
final totalMonths = viewModel.monthProgressList.length;
|
||||
final startIndex = totalMonths > 7 ? totalMonths - 7 : 0;
|
||||
final monthDataToShow = viewModel.monthProgressList.skip(startIndex).toList();
|
||||
|
||||
for (var monthData in monthDataToShow) {
|
||||
listItems.add(
|
||||
buildHistoryListTile(
|
||||
title: monthData.monthName ?? 'Unknown',
|
||||
subTitle: "${monthData.percentageConsumed?.toStringAsFixed(1) ?? '0'}%",
|
||||
),
|
||||
);
|
||||
if (monthData != monthDataToShow.last) {
|
||||
listItems.add(Divider(height: 1, color: AppColors.dividerColor));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
listItems.add(
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16.h),
|
||||
child: "No data available for this year".toText14(color: AppColors.greyTextColor),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Return scrollable list with min and max height constraints
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(minHeight: 80.h, maxHeight: 270.h),
|
||||
child: viewModel.isLoading
|
||||
? _buildLoadingShimmer().paddingOnly(top: 16.h)
|
||||
: listItems.isEmpty
|
||||
? Center(
|
||||
child: "No history data available".toText14(color: AppColors.greyTextColor),
|
||||
)
|
||||
: ListView.separated(
|
||||
padding: EdgeInsets.only(top: 16.h),
|
||||
shrinkWrap: true,
|
||||
itemCount: listItems.length,
|
||||
separatorBuilder: (context, index) => SizedBox.shrink(),
|
||||
itemBuilder: (context, index) => listItems[index],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHistoryGraph() {
|
||||
return Consumer<WaterMonitorViewModel>(
|
||||
builder: (context, viewModel, _) {
|
||||
final selectedDuration = viewModel.selectedDurationFilter;
|
||||
|
||||
// Build dynamic data points based on selected duration
|
||||
List<DataPoint> dataPoints = [];
|
||||
|
||||
if (selectedDuration == 'Daily') {
|
||||
// For daily, show last 7 history entries with at least 5 minutes difference
|
||||
if (viewModel.historyList.isNotEmpty) {
|
||||
// Filter entries with at least 5 minutes difference
|
||||
List<DataPoint> filteredPoints = [];
|
||||
DateTime? lastTime;
|
||||
|
||||
for (var historyItem in viewModel.historyList) {
|
||||
final currentTime = _parseHistoryDate(historyItem.createdDate ?? '');
|
||||
|
||||
// Add if first entry OR if more than 5 minutes difference from last added entry
|
||||
if (lastTime == null || currentTime.difference(lastTime).inMinutes.abs() >= 5) {
|
||||
final quantity = historyItem.quantity?.toDouble() ?? 0.0;
|
||||
final time = _formatHistoryDate(historyItem.createdDate ?? '');
|
||||
|
||||
filteredPoints.add(
|
||||
DataPoint(
|
||||
value: quantity,
|
||||
actualValue: quantity.toStringAsFixed(0),
|
||||
label: time,
|
||||
displayTime: time,
|
||||
unitOfMeasurement: 'ml',
|
||||
time: currentTime,
|
||||
),
|
||||
);
|
||||
lastTime = currentTime;
|
||||
}
|
||||
}
|
||||
|
||||
// Take only last 7 filtered entries
|
||||
final totalFiltered = filteredPoints.length;
|
||||
final startIndex = totalFiltered > 7 ? totalFiltered - 7 : 0;
|
||||
dataPoints = filteredPoints.skip(startIndex).toList();
|
||||
} else if (viewModel.todayProgressList.isNotEmpty) {
|
||||
// Fallback: show today's percentage if no history
|
||||
final todayData = viewModel.todayProgressList.first;
|
||||
final percentage = todayData.percentageConsumed?.toDouble() ?? 0.0;
|
||||
dataPoints.add(
|
||||
DataPoint(
|
||||
value: percentage,
|
||||
actualValue: percentage.toStringAsFixed(1),
|
||||
label: 'Today',
|
||||
displayTime: 'Today',
|
||||
unitOfMeasurement: '%',
|
||||
time: DateTime.now(),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (selectedDuration == 'Weekly') {
|
||||
// For weekly, show previous 6 days + today (total 7 days)
|
||||
// API returns data in reverse order (today first), so we reverse it to show oldest to newest (left to right)
|
||||
// This ensures today appears at the end (right side)
|
||||
if (viewModel.weekProgressList.isNotEmpty) {
|
||||
final totalDays = viewModel.weekProgressList.length;
|
||||
final startIndex = totalDays > 7 ? totalDays - 7 : 0;
|
||||
final weekDataToShow = viewModel.weekProgressList.skip(startIndex).toList().reversed.toList();
|
||||
|
||||
for (var dayData in weekDataToShow) {
|
||||
final percentage = dayData.percentageConsumed?.toDouble() ?? 0.0;
|
||||
final dayName = dayData.dayName ?? 'Day ${dayData.dayNumber}';
|
||||
dataPoints.add(
|
||||
DataPoint(
|
||||
value: percentage,
|
||||
actualValue: percentage.toStringAsFixed(1),
|
||||
label: DateUtil.getShortWeekDayName(dayName),
|
||||
displayTime: dayName,
|
||||
unitOfMeasurement: '%',
|
||||
time: DateTime.now(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (selectedDuration == 'Monthly') {
|
||||
// For monthly, show last 6 months + current month (total 7 months)
|
||||
// Show in chronological order: oldest to newest (left to right)
|
||||
if (viewModel.monthProgressList.isNotEmpty) {
|
||||
final totalMonths = viewModel.monthProgressList.length;
|
||||
final startIndex = totalMonths > 7 ? totalMonths - 7 : 0;
|
||||
final monthDataToShow = viewModel.monthProgressList.skip(startIndex).toList();
|
||||
|
||||
for (var monthData in monthDataToShow) {
|
||||
final percentage = monthData.percentageConsumed?.toDouble() ?? 0.0;
|
||||
final monthName = monthData.monthName ?? 'Month ${monthData.monthNumber}';
|
||||
dataPoints.add(
|
||||
DataPoint(
|
||||
value: percentage,
|
||||
actualValue: percentage.toStringAsFixed(1),
|
||||
label: DateUtil.getShortMonthName(monthName),
|
||||
displayTime: monthName,
|
||||
unitOfMeasurement: '%',
|
||||
time: DateTime.now(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no data, show empty state
|
||||
if (dataPoints.isEmpty) {
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(vertical: 80.h),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Utils.buildSvgWithAssets(
|
||||
icon: AppAssets.graphIcon,
|
||||
iconColor: AppColors.greyTextColor.withValues(alpha: 0.5),
|
||||
height: 32.w,
|
||||
width: 32.w,
|
||||
),
|
||||
SizedBox(height: 12.h),
|
||||
"No graph data available".toText14(color: AppColors.greyTextColor),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Show loading shimmer while fetching data
|
||||
if (viewModel.isLoading) {
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(vertical: 40.h),
|
||||
child: _buildLoadingShimmer(),
|
||||
);
|
||||
}
|
||||
|
||||
// Configure graph based on selected duration
|
||||
double maxY;
|
||||
double minY;
|
||||
double horizontalInterval;
|
||||
double leftLabelInterval;
|
||||
|
||||
if (selectedDuration == 'Daily') {
|
||||
// For daily (quantity in ml), use max available cup size
|
||||
// Get the biggest cup from available cups
|
||||
final maxCupSize = viewModel.cups.isEmpty ? 500.0 : viewModel.cups.map((cup) => cup.capacityMl.toDouble()).reduce((a, b) => a > b ? a : b);
|
||||
|
||||
maxY = maxCupSize;
|
||||
minY = 0;
|
||||
// Divide into 4 intervals (5 labels: 0, 1/4, 1/2, 3/4, max)
|
||||
horizontalInterval = maxY / 4;
|
||||
leftLabelInterval = maxY / 4;
|
||||
} else {
|
||||
// For weekly/monthly (percentage), use 0-100%
|
||||
maxY = 100.0;
|
||||
minY = 0;
|
||||
horizontalInterval = 25;
|
||||
leftLabelInterval = 25;
|
||||
}
|
||||
|
||||
return CustomGraph(
|
||||
bottomLabelReservedSize: 30,
|
||||
dataPoints: dataPoints,
|
||||
makeGraphBasedOnActualValue: true,
|
||||
leftLabelReservedSize: 50.h,
|
||||
showGridLines: true,
|
||||
maxY: maxY,
|
||||
minY: minY,
|
||||
maxX: dataPoints.length > 1 ? dataPoints.length.toDouble() - 0.75 : 1.0,
|
||||
horizontalInterval: horizontalInterval,
|
||||
leftLabelInterval: leftLabelInterval,
|
||||
showShadow: true,
|
||||
getDrawingHorizontalLine: (value) {
|
||||
// Draw dashed lines at intervals
|
||||
if (selectedDuration == 'Daily') {
|
||||
// For daily, draw lines every 50 or 100 ml
|
||||
if (value % horizontalInterval == 0 && value > 0) {
|
||||
return FlLine(
|
||||
color: AppColors.greyTextColor.withValues(alpha: 0.3),
|
||||
strokeWidth: 1.5,
|
||||
dashArray: [8, 4],
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// For weekly/monthly, draw lines at 25%, 50%, 75%
|
||||
if (value == 25 || value == 50 || value == 75) {
|
||||
return FlLine(
|
||||
color: AppColors.successColor.withValues(alpha: 0.3),
|
||||
strokeWidth: 1.5,
|
||||
dashArray: [8, 4],
|
||||
);
|
||||
}
|
||||
}
|
||||
return FlLine(color: AppColors.transparent, strokeWidth: 0);
|
||||
},
|
||||
leftLabelFormatter: (value) {
|
||||
if (selectedDuration == 'Daily') {
|
||||
// Show exactly 5 labels: 0, 1/4, 1/2, 3/4, max
|
||||
// Check if value matches one of the 5 positions
|
||||
final interval = maxY / 4;
|
||||
final positions = [0.0, interval, interval * 2, interval * 3, maxY];
|
||||
|
||||
for (var position in positions) {
|
||||
if ((value - position).abs() < 1) {
|
||||
return '${value.toInt()}ml'.toText10(weight: FontWeight.w600);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Show percentage labels
|
||||
if (value == 0) return '0%'.toText10(weight: FontWeight.w600);
|
||||
if (value == 25) return '25%'.toText10(weight: FontWeight.w600);
|
||||
if (value == 50) return '50%'.toText10(weight: FontWeight.w600);
|
||||
if (value == 75) return '75%'.toText10(weight: FontWeight.w600);
|
||||
if (value == 100) return '100%'.toText10(weight: FontWeight.w600);
|
||||
}
|
||||
return SizedBox.shrink();
|
||||
},
|
||||
graphColor: AppColors.successColor,
|
||||
graphShadowColor: AppColors.successColor.withValues(alpha: 0.15),
|
||||
bottomLabelFormatter: (value, data) {
|
||||
if (data.isEmpty) return SizedBox.shrink();
|
||||
|
||||
// Only show labels for whole number positions (not fractional)
|
||||
if ((value - value.round()).abs() > 0.01) {
|
||||
return SizedBox.shrink();
|
||||
}
|
||||
|
||||
int index = value.round();
|
||||
if (index < 0 || index >= data.length) return SizedBox.shrink();
|
||||
|
||||
// For daily, show all 7 time labels (last 7 entries)
|
||||
if (selectedDuration == 'Daily' && index < 7) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(top: 5.h),
|
||||
child: data[index].label.toText8(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.labelTextColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// For weekly, show all 7 days (today + last 6 days)
|
||||
if (selectedDuration == 'Weekly' && index < 7) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(top: 5.h),
|
||||
child: data[index].label.toText10(
|
||||
weight: FontWeight.w600,
|
||||
color: AppColors.labelTextColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// For monthly, show all 7 months (current month + last 6 months)
|
||||
if (selectedDuration == 'Monthly' && index < 7) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(top: 5.h),
|
||||
child: data[index].label.toText10(
|
||||
weight: FontWeight.w600,
|
||||
color: AppColors.labelTextColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SizedBox.shrink();
|
||||
},
|
||||
scrollDirection: selectedDuration == 'Monthly' ? Axis.horizontal : Axis.vertical,
|
||||
height: 250.h,
|
||||
spotColor: AppColors.successColor,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Reusable method to build selection row widget
|
||||
Widget _buildSelectionRow({
|
||||
required String value,
|
||||
required String groupValue,
|
||||
required VoidCallback onTap,
|
||||
bool useUpperCase = false,
|
||||
}) {
|
||||
return SizedBox(
|
||||
height: 70.h,
|
||||
child: Row(
|
||||
spacing: 8.h,
|
||||
children: [
|
||||
Radio<String>(
|
||||
value: value,
|
||||
groupValue: groupValue,
|
||||
activeColor: AppColors.errorColor,
|
||||
onChanged: (_) => onTap(),
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
(useUpperCase ? value.toUpperCase() : value.toCamelCase)
|
||||
.toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1)
|
||||
.expanded,
|
||||
],
|
||||
).onPress(onTap),
|
||||
);
|
||||
}
|
||||
|
||||
void _showSelectionBottomSheet({
|
||||
required BuildContext context,
|
||||
required String title,
|
||||
required List<String> items,
|
||||
required String selectedValue,
|
||||
required Function(String) onSelected,
|
||||
bool useUpperCase = false,
|
||||
}) {
|
||||
final dialogService = getIt.get<DialogService>();
|
||||
|
||||
dialogService.showFamilyBottomSheetWithoutHWithChild(
|
||||
label: title.needTranslation,
|
||||
message: "",
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h),
|
||||
decoration: BoxDecoration(color: AppColors.whiteColor, borderRadius: BorderRadius.circular(20.r)),
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
return _buildSelectionRow(
|
||||
value: item,
|
||||
groupValue: selectedValue,
|
||||
useUpperCase: useUpperCase,
|
||||
onTap: () {
|
||||
onSelected(item);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) => Divider(height: 1, color: AppColors.dividerColor),
|
||||
),
|
||||
),
|
||||
onOkPressed: () {},
|
||||
);
|
||||
}
|
||||
|
||||
void _showHistoryDurationBottomsheet(BuildContext context, WaterMonitorViewModel viewModel) {
|
||||
_showSelectionBottomSheet(
|
||||
context: context,
|
||||
title: "Select Duration".needTranslation,
|
||||
items: viewModel.durationFilters,
|
||||
selectedValue: viewModel.selectedDurationFilter,
|
||||
onSelected: viewModel.setFilterDuration,
|
||||
);
|
||||
}
|
||||
|
||||
/// Handle reminder button tap (Set or Cancel)
|
||||
Future<void> _handleReminderButtonTap(WaterMonitorViewModel viewModel) async {
|
||||
if (viewModel.isWaterReminderEnabled) {
|
||||
// Cancel reminders
|
||||
_showCancelReminderConfirmation(viewModel);
|
||||
} else {
|
||||
// Set reminders
|
||||
await _setReminders(viewModel);
|
||||
}
|
||||
}
|
||||
|
||||
/// Show confirmation bottom sheet before cancelling reminders
|
||||
void _showCancelReminderConfirmation(WaterMonitorViewModel viewModel) {
|
||||
showCommonBottomSheetWithoutHeight(
|
||||
title: 'Notice'.needTranslation,
|
||||
context,
|
||||
child: Utils.getWarningWidget(
|
||||
loadingText: "Are you sure you want to cancel all water reminders?".needTranslation,
|
||||
isShowActionButtons: true,
|
||||
onCancelTap: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
onConfirmTap: () async {
|
||||
Navigator.pop(context);
|
||||
await _cancelReminders(viewModel);
|
||||
},
|
||||
),
|
||||
callBackFunc: () {},
|
||||
isFullScreen: false,
|
||||
isCloseButtonVisible: true,
|
||||
isDismissible: true,
|
||||
);
|
||||
}
|
||||
|
||||
/// Set water reminders
|
||||
Future<void> _setReminders(WaterMonitorViewModel viewModel) async {
|
||||
// Schedule reminders
|
||||
final success = await viewModel.scheduleWaterReminders();
|
||||
|
||||
if (success) {
|
||||
final times = await viewModel.getScheduledReminderTimes();
|
||||
_showReminderScheduledDialog(times);
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancel water reminders
|
||||
Future<void> _cancelReminders(WaterMonitorViewModel viewModel) async {
|
||||
final success = await viewModel.cancelWaterReminders();
|
||||
}
|
||||
|
||||
/// Show bottom sheet with scheduled reminder times
|
||||
void _showReminderScheduledDialog(List<DateTime> times) {
|
||||
showCommonBottomSheetWithoutHeight(
|
||||
title: 'Reminders Set!'.needTranslation,
|
||||
context,
|
||||
isCloseButtonVisible: false,
|
||||
isDismissible: false,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: 16.w, left: 16.w, right: 16.w),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Utils.getSuccessWidget(loadingText: 'Daily water reminders scheduled at:'.needTranslation),
|
||||
SizedBox(height: 16.h),
|
||||
Wrap(
|
||||
spacing: 8.w,
|
||||
runSpacing: 8.h,
|
||||
children: times
|
||||
.map(
|
||||
(time) => AppCustomChipWidget(
|
||||
icon: AppAssets.bell,
|
||||
iconColor: AppColors.quickLoginColor,
|
||||
richText: _formatTime(time).toText14(),
|
||||
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 8.h),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
|
||||
SizedBox(height: 24.h),
|
||||
|
||||
// OK button
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CustomButton(
|
||||
height: 56.h,
|
||||
text: 'OK'.needTranslation,
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
textColor: AppColors.whiteColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
callBackFunc: () {},
|
||||
);
|
||||
}
|
||||
|
||||
/// Format DateTime to readable time string
|
||||
String _formatTime(DateTime time) {
|
||||
final hour = time.hour;
|
||||
final minute = time.minute;
|
||||
final hour12 = hour > 12 ? hour - 12 : (hour == 0 ? 12 : hour);
|
||||
final period = hour >= 12 ? 'PM' : 'AM';
|
||||
return '${hour12.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')} $period';
|
||||
}
|
||||
|
||||
/// Format history date from /Date(milliseconds+0300)/ format
|
||||
String _formatHistoryDate(String dateString) {
|
||||
try {
|
||||
// Parse the /Date(milliseconds+0300)/ format
|
||||
final regex = RegExp(r'\/Date\((\d+)');
|
||||
final match = regex.firstMatch(dateString);
|
||||
if (match != null) {
|
||||
final milliseconds = int.tryParse(match.group(1)!);
|
||||
if (milliseconds != null) {
|
||||
final dateTime = DateTime.fromMillisecondsSinceEpoch(milliseconds);
|
||||
return _formatTime(dateTime);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
return dateString;
|
||||
}
|
||||
return dateString;
|
||||
}
|
||||
|
||||
/// Parse history date from /Date(milliseconds+0300)/ format to DateTime
|
||||
DateTime _parseHistoryDate(String dateString) {
|
||||
try {
|
||||
final regex = RegExp(r'\/Date\((\d+)');
|
||||
final match = regex.firstMatch(dateString);
|
||||
if (match != null) {
|
||||
final milliseconds = int.tryParse(match.group(1)!);
|
||||
if (milliseconds != null) {
|
||||
return DateTime.fromMillisecondsSinceEpoch(milliseconds);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Return current time as fallback
|
||||
}
|
||||
return DateTime.now();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.bgScaffoldColor,
|
||||
body: CollapsingListView(
|
||||
title: "Water Consumption".needTranslation,
|
||||
bottomChild: Consumer<WaterMonitorViewModel>(
|
||||
builder: (context, viewModel, child) {
|
||||
return Container(
|
||||
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
|
||||
color: AppColors.whiteColor,
|
||||
borderRadius: 24.r,
|
||||
hasShadow: true,
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(24.w),
|
||||
child: CustomButton(
|
||||
text: viewModel.isWaterReminderEnabled ? "Cancel Reminders".needTranslation : "Set Reminder".needTranslation,
|
||||
textColor: viewModel.isWaterReminderEnabled ? AppColors.errorColor : AppColors.successColor,
|
||||
backgroundColor: viewModel.isWaterReminderEnabled ? AppColors.errorColor.withValues(alpha: 0.1) : AppColors.successLightBgColor,
|
||||
onPressed: () => _handleReminderButtonTap(viewModel),
|
||||
icon: viewModel.isWaterReminderEnabled ? null : AppAssets.bell,
|
||||
iconColor: viewModel.isWaterReminderEnabled ? AppColors.errorColor : AppColors.successColor,
|
||||
borderRadius: 12.r,
|
||||
borderColor: AppColors.transparent,
|
||||
padding: EdgeInsets.symmetric(vertical: 14.h),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _refreshData,
|
||||
color: AppColors.blueColor,
|
||||
backgroundColor: AppColors.whiteColor,
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: 16.h),
|
||||
const WaterIntakeSummaryWidget(),
|
||||
SizedBox(height: 16.h),
|
||||
_buildHistoryGraphOrList(),
|
||||
SizedBox(height: 16.h),
|
||||
const HydrationTipsWidget(),
|
||||
SizedBox(height: 16.h),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,359 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hmg_patient_app_new/core/app_assets.dart';
|
||||
import 'package:hmg_patient_app_new/core/app_export.dart';
|
||||
import 'package:hmg_patient_app_new/core/dependencies.dart';
|
||||
import 'package:hmg_patient_app_new/core/utils/utils.dart';
|
||||
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
|
||||
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
|
||||
import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart';
|
||||
import 'package:hmg_patient_app_new/services/dialog_service.dart';
|
||||
import 'package:hmg_patient_app_new/theme/colors.dart';
|
||||
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
|
||||
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class WaterMonitorSettingsScreen extends StatefulWidget {
|
||||
const WaterMonitorSettingsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<WaterMonitorSettingsScreen> createState() => _WaterMonitorSettingsScreenState();
|
||||
}
|
||||
|
||||
class _WaterMonitorSettingsScreenState extends State<WaterMonitorSettingsScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.read<WaterMonitorViewModel>().initialize();
|
||||
});
|
||||
}
|
||||
|
||||
void _showSnackbar(String text) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(text),
|
||||
backgroundColor: AppColors.errorColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Reusable method to build selection row widget
|
||||
Widget _buildSelectionRow({
|
||||
required String value,
|
||||
required String groupValue,
|
||||
required VoidCallback onTap,
|
||||
bool useUpperCase = false,
|
||||
}) {
|
||||
return SizedBox(
|
||||
height: 70.h,
|
||||
child: Row(
|
||||
spacing: 8.h,
|
||||
children: [
|
||||
Radio<String>(
|
||||
value: value,
|
||||
groupValue: groupValue,
|
||||
activeColor: AppColors.errorColor,
|
||||
onChanged: (_) => onTap(),
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
(useUpperCase ? value.toUpperCase() : value.toCamelCase)
|
||||
.toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1)
|
||||
.expanded,
|
||||
],
|
||||
).onPress(onTap),
|
||||
);
|
||||
}
|
||||
|
||||
// Reusable method to show selection bottom sheet
|
||||
void _showSelectionBottomSheet({
|
||||
required BuildContext context,
|
||||
required String title,
|
||||
required List<String> items,
|
||||
required String selectedValue,
|
||||
required Function(String) onSelected,
|
||||
bool useUpperCase = false,
|
||||
}) {
|
||||
final dialogService = getIt.get<DialogService>();
|
||||
|
||||
dialogService.showFamilyBottomSheetWithoutHWithChild(
|
||||
label: title.needTranslation,
|
||||
message: "",
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)),
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
return _buildSelectionRow(
|
||||
value: item,
|
||||
groupValue: selectedValue,
|
||||
useUpperCase: useUpperCase,
|
||||
onTap: () {
|
||||
onSelected(item);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) => Divider(height: 1, color: AppColors.dividerColor),
|
||||
),
|
||||
),
|
||||
onOkPressed: () {},
|
||||
);
|
||||
}
|
||||
|
||||
void _showGenderSelectionBottomsheet(BuildContext context, WaterMonitorViewModel viewModel) {
|
||||
_showSelectionBottomSheet(
|
||||
context: context,
|
||||
title: "Select Gender".needTranslation,
|
||||
items: viewModel.genderOptions,
|
||||
selectedValue: viewModel.selectedGender,
|
||||
onSelected: viewModel.setGender,
|
||||
);
|
||||
}
|
||||
|
||||
void _showHeightUnitSelectionBottomSheet(BuildContext context, WaterMonitorViewModel viewModel) {
|
||||
_showSelectionBottomSheet(
|
||||
context: context,
|
||||
title: "Select Unit".needTranslation,
|
||||
items: viewModel.heightUnits,
|
||||
selectedValue: viewModel.selectedHeightUnit,
|
||||
onSelected: viewModel.setHeightUnit,
|
||||
useUpperCase: true,
|
||||
);
|
||||
}
|
||||
|
||||
void _showWeightUnitSelectionBottomsheet(BuildContext context, WaterMonitorViewModel viewModel) {
|
||||
_showSelectionBottomSheet(
|
||||
context: context,
|
||||
title: "Select Unit".needTranslation,
|
||||
items: viewModel.weightUnits,
|
||||
selectedValue: viewModel.selectedWeightUnit,
|
||||
onSelected: viewModel.setWeightUnit,
|
||||
useUpperCase: true,
|
||||
);
|
||||
}
|
||||
|
||||
void _showActivityLevelSelectionBottomsheet(BuildContext context, WaterMonitorViewModel viewModel) {
|
||||
_showSelectionBottomSheet(
|
||||
context: context,
|
||||
title: "Select Activity Level".needTranslation,
|
||||
items: viewModel.activityLevels,
|
||||
selectedValue: viewModel.selectedActivityLevel,
|
||||
onSelected: viewModel.setActivityLevel,
|
||||
);
|
||||
}
|
||||
|
||||
void _showNumberOfRemindersSelectionBottomsheet(BuildContext context, WaterMonitorViewModel viewModel) {
|
||||
_showSelectionBottomSheet(
|
||||
context: context,
|
||||
title: "Select Number of Reminders".needTranslation,
|
||||
items: viewModel.reminderOptions,
|
||||
selectedValue: viewModel.selectedNumberOfReminders,
|
||||
onSelected: viewModel.setNumberOfReminders,
|
||||
);
|
||||
}
|
||||
|
||||
// Reusable method to build text field
|
||||
Widget _buildTextField(TextEditingController controller, String hintText) {
|
||||
return TextField(
|
||||
controller: controller,
|
||||
keyboardType: TextInputType.number,
|
||||
maxLines: 1,
|
||||
cursorHeight: 14.h,
|
||||
textAlignVertical: TextAlignVertical.center,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
isCollapsed: true,
|
||||
hintText: hintText,
|
||||
hintStyle: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 14.f,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.textColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Reusable method to build settings row
|
||||
Widget _buildSettingsRow({
|
||||
required String icon,
|
||||
required String label,
|
||||
String? value,
|
||||
Widget? inputField,
|
||||
String? unit,
|
||||
VoidCallback? onUnitTap,
|
||||
VoidCallback? onRowTap,
|
||||
bool showDivider = true,
|
||||
}) {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
height: 40.w,
|
||||
width: 40.w,
|
||||
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
|
||||
color: AppColors.greyColor,
|
||||
borderRadius: 10.r,
|
||||
hasShadow: false,
|
||||
),
|
||||
child: Center(child: Utils.buildSvgWithAssets(icon: icon, height: 22.w, width: 22.w)),
|
||||
),
|
||||
SizedBox(width: 12.w),
|
||||
Expanded(
|
||||
flex: unit != null ? 3 : 1,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
label.toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor),
|
||||
if (inputField != null)
|
||||
Container(
|
||||
height: 20.w,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: inputField,
|
||||
)
|
||||
else if (value != null)
|
||||
value.toCamelCase.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (unit != null) ...[
|
||||
Container(
|
||||
width: 1.w,
|
||||
height: 30.w,
|
||||
color: AppColors.dividerColor,
|
||||
).paddingOnly(right: 10.w),
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
"Unit".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor),
|
||||
unit.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor),
|
||||
],
|
||||
),
|
||||
SizedBox(width: 12.w),
|
||||
Utils.buildSvgWithAssets(icon: AppAssets.arrow_down)
|
||||
],
|
||||
).onPress(onUnitTap ?? () {}),
|
||||
),
|
||||
] else if (onRowTap != null) ...[
|
||||
Utils.buildSvgWithAssets(icon: AppAssets.arrow_down),
|
||||
SizedBox(width: 4.w),
|
||||
],
|
||||
],
|
||||
).paddingSymmetrical(0.w, 16.w).onPress(onRowTap ?? () {}),
|
||||
if (showDivider) Divider(height: 1, color: AppColors.dividerColor),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final viewModel = context.watch<WaterMonitorViewModel>();
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.bgScaffoldColor,
|
||||
body: CollapsingListView(
|
||||
title: "H20 Settings".needTranslation,
|
||||
bottomChild: Container(
|
||||
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
|
||||
color: AppColors.whiteColor,
|
||||
borderRadius: 24.r,
|
||||
hasShadow: true,
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(24.w),
|
||||
child: CustomButton(
|
||||
text: "Save".needTranslation,
|
||||
onPressed: () async {
|
||||
final success = await viewModel.saveSettings();
|
||||
if (!success && viewModel.validationError != null) {
|
||||
_showSnackBar(context, viewModel.validationError!);
|
||||
} else if (success) {
|
||||
_showSnackBar(context, "Settings saved successfully");
|
||||
}
|
||||
},
|
||||
borderRadius: 12.r,
|
||||
padding: EdgeInsets.symmetric(vertical: 14.h),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 24.w, vertical: 24.h),
|
||||
padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h),
|
||||
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildSettingsRow(
|
||||
icon: AppAssets.profileIcon,
|
||||
label: "Your Name".needTranslation,
|
||||
inputField: _buildTextField(viewModel.nameController, 'Guest'),
|
||||
),
|
||||
_buildSettingsRow(
|
||||
icon: AppAssets.genderIcon,
|
||||
label: "Select Gender".needTranslation,
|
||||
value: viewModel.selectedGender,
|
||||
onRowTap: () => _showGenderSelectionBottomsheet(context, viewModel),
|
||||
),
|
||||
_buildSettingsRow(
|
||||
icon: AppAssets.calendarGrey,
|
||||
label: "Age (11-120) yrs".needTranslation,
|
||||
inputField: _buildTextField(viewModel.ageController, '20'),
|
||||
),
|
||||
_buildSettingsRow(
|
||||
icon: AppAssets.heightIcon,
|
||||
label: "Height".needTranslation,
|
||||
inputField: _buildTextField(viewModel.heightController, '175'),
|
||||
unit: viewModel.selectedHeightUnit,
|
||||
onUnitTap: () => _showHeightUnitSelectionBottomSheet(context, viewModel),
|
||||
),
|
||||
_buildSettingsRow(
|
||||
icon: AppAssets.weightScaleIcon,
|
||||
label: "Weight".needTranslation,
|
||||
inputField: _buildTextField(viewModel.weightController, '75'),
|
||||
unit: viewModel.selectedWeightUnit,
|
||||
onUnitTap: () => _showWeightUnitSelectionBottomsheet(context, viewModel),
|
||||
),
|
||||
_buildSettingsRow(
|
||||
icon: AppAssets.dumbellIcon,
|
||||
label: "Activity Level".needTranslation,
|
||||
value: viewModel.selectedActivityLevel,
|
||||
onRowTap: () => _showActivityLevelSelectionBottomsheet(context, viewModel),
|
||||
),
|
||||
_buildSettingsRow(
|
||||
icon: AppAssets.notificationIconGrey,
|
||||
label: "Number of reminders in a day".needTranslation,
|
||||
value: viewModel.selectedNumberOfReminders,
|
||||
onRowTap: () => _showNumberOfRemindersSelectionBottomsheet(context, viewModel),
|
||||
showDivider: false,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Show snackbar for validation errors and success messages
|
||||
void _showSnackBar(BuildContext context, String message) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
duration: const Duration(seconds: 3),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
backgroundColor: message.contains('successfully')
|
||||
? Colors.green
|
||||
: AppColors.errorColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
// Add this class at the bottom of your file (outside the main class)
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class BottleShapeClipper extends CustomClipper<Path> {
|
||||
@override
|
||||
Path getClip(Size size) {
|
||||
final path = Path();
|
||||
|
||||
// Create rounded rectangle matching the bottle body shape
|
||||
// The bottle has rounded corners with radius ~30-40 based on SVG
|
||||
final borderRadius = size.width * 0.25; // 25% of width for rounded corners
|
||||
|
||||
path.addRRect(
|
||||
RRect.fromRectAndRadius(
|
||||
Rect.fromLTWH(0, 0, size.width, size.height),
|
||||
Radius.circular(borderRadius),
|
||||
),
|
||||
);
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldReclip(covariant CustomClipper<Path> oldClipper) => false;
|
||||
}
|
||||
@ -0,0 +1,324 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hmg_patient_app_new/core/app_assets.dart';
|
||||
import 'package:hmg_patient_app_new/core/app_export.dart';
|
||||
import 'package:hmg_patient_app_new/core/utils/utils.dart';
|
||||
import 'package:hmg_patient_app_new/extensions/route_extensions.dart';
|
||||
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
|
||||
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
|
||||
import 'package:hmg_patient_app_new/features/water_monitor/models/water_cup_model.dart';
|
||||
import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart';
|
||||
import 'package:hmg_patient_app_new/theme/colors.dart';
|
||||
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
|
||||
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
/// Bottom sheet to switch between existing cups or add new custom cup
|
||||
void showSwitchCupBottomSheet(BuildContext context) {
|
||||
return showCommonBottomSheetWithoutHeight(
|
||||
context,
|
||||
titleWidget: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
"Switch Cups".toText20(weight: FontWeight.w600),
|
||||
"Select your preferred cup size".toText16(weight: FontWeight.w500, color: AppColors.greyTextColor),
|
||||
],
|
||||
),
|
||||
child: SwitchCupBottomSheet(),
|
||||
callBackFunc: () {},
|
||||
);
|
||||
}
|
||||
|
||||
class SwitchCupBottomSheet extends StatelessWidget {
|
||||
const SwitchCupBottomSheet({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final viewModel = context.watch<WaterMonitorViewModel>();
|
||||
final selectedId = viewModel.selectedCup?.id;
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 4,
|
||||
crossAxisSpacing: 16.w,
|
||||
childAspectRatio: 0.85,
|
||||
),
|
||||
itemCount: viewModel.cups.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == viewModel.cups.length) {
|
||||
return _buildAddCupItem(context);
|
||||
}
|
||||
|
||||
final cup = viewModel.cups[index];
|
||||
final isSelected = selectedId == cup.id;
|
||||
|
||||
return _buildCupItem(
|
||||
cup: cup,
|
||||
isSelected: isSelected,
|
||||
onTap: () {
|
||||
viewModel.selectCup(cup.id);
|
||||
context.pop();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCupItem({required WaterCupModel cup, required bool isSelected, required VoidCallback onTap}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Container(
|
||||
width: 60.w,
|
||||
height: 60.w,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? AppColors.primaryRedColor.withOpacity(0.08) : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
border: Border.all(
|
||||
color: isSelected ? AppColors.primaryRedColor : AppColors.bgScaffoldColor,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Utils.buildSvgWithAssets(
|
||||
icon: cup.iconPath,
|
||||
height: 30.h,
|
||||
width: 42.w,
|
||||
iconColor: isSelected ? AppColors.primaryRedColor : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Red badge for custom cups (delete)
|
||||
if (!cup.isDefault)
|
||||
Positioned(
|
||||
top: -6.h,
|
||||
right: -6.w,
|
||||
child: Builder(builder: (ctx) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
// call viewmodel remove
|
||||
final vm = ctx.read<WaterMonitorViewModel>();
|
||||
vm.removeCup(cup.id);
|
||||
},
|
||||
child: Container(
|
||||
color: AppColors.whiteColor,
|
||||
child: Utils.buildSvgWithAssets(icon: AppAssets.minimizeIcon, height: 20.w, width: 20.w),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 2.h),
|
||||
'${cup.capacityMl}ml'.toText10(weight: FontWeight.w500),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAddCupItem(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
showCustomizeCupBottomSheet(context);
|
||||
},
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 60.w,
|
||||
height: 60.w,
|
||||
child: Center(child: Utils.buildSvgWithAssets(icon: AppAssets.cupAdd, height: 30.h, width: 42.w)),
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
'Add'.needTranslation.toText10(weight: FontWeight.w500),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Bottom sheet to customize cup capacity with slider
|
||||
void showCustomizeCupBottomSheet(BuildContext context, {WaterCupModel? cupToEdit}) {
|
||||
return showCommonBottomSheetWithoutHeight(
|
||||
context,
|
||||
titleWidget: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
"Customize your drink cup".needTranslation.toText20(weight: FontWeight.w600),
|
||||
],
|
||||
),
|
||||
child: CustomizeCupBottomSheet(cupToEdit: cupToEdit),
|
||||
callBackFunc: () {},
|
||||
);
|
||||
}
|
||||
|
||||
class CustomizeCupBottomSheet extends StatefulWidget {
|
||||
final WaterCupModel? cupToEdit;
|
||||
|
||||
const CustomizeCupBottomSheet({super.key, this.cupToEdit});
|
||||
|
||||
@override
|
||||
State<CustomizeCupBottomSheet> createState() => _CustomizeCupBottomSheetState();
|
||||
}
|
||||
|
||||
class _CustomizeCupBottomSheetState extends State<CustomizeCupBottomSheet> {
|
||||
static const int minCapacity = 50;
|
||||
static const int maxCapacity = 500;
|
||||
|
||||
late double _currentCapacity;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_currentCapacity = (widget.cupToEdit?.capacityMl ?? 150).toDouble();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Cup icon with fill level visualization
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 60.w,
|
||||
height: 80.h,
|
||||
child: Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
// Cup image with dynamic fill
|
||||
SizedBox(
|
||||
width: 60.w,
|
||||
height: 80.h,
|
||||
child: Center(
|
||||
child: Utils.buildSvgWithAssets(
|
||||
icon: AppAssets.cupEmpty,
|
||||
width: 60.w,
|
||||
height: 80.h,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
ClipRect(
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
heightFactor: (_currentCapacity / maxCapacity).clamp(0.0, 1.0),
|
||||
child: Utils.buildSvgWithAssets(
|
||||
icon: AppAssets.cupFilled,
|
||||
width: 60.w,
|
||||
height: 80.h,
|
||||
fit: BoxFit.contain,
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(width: 12.w),
|
||||
|
||||
// Slider and labels
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Current value
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
'${_currentCapacity.round()}'.toText32(isBold: true),
|
||||
SizedBox(width: 4.w),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(bottom: 4.h),
|
||||
child: Text(
|
||||
'ml',
|
||||
style: TextStyle(
|
||||
fontSize: 16.f,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.greyTextColor,
|
||||
height: 1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
).paddingOnly(left: 12.w),
|
||||
|
||||
SizedBox(height: 16.h),
|
||||
|
||||
// Slider
|
||||
SliderTheme(
|
||||
data: SliderTheme.of(context).copyWith(
|
||||
activeTrackColor: AppColors.primaryRedColor,
|
||||
inactiveTrackColor: AppColors.primaryRedColor.withOpacity(0.2),
|
||||
thumbColor: AppColors.primaryRedColor,
|
||||
overlayColor: AppColors.primaryRedColor.withOpacity(0.2),
|
||||
trackHeight: 4.h,
|
||||
thumbShape: RoundSliderThumbShape(enabledThumbRadius: 10.w),
|
||||
),
|
||||
child: Slider(
|
||||
value: _currentCapacity,
|
||||
min: minCapacity.toDouble(),
|
||||
max: maxCapacity.toDouble(),
|
||||
divisions: (maxCapacity - minCapacity) ~/ 10,
|
||||
onChanged: (value) => setState(() => _currentCapacity = value),
|
||||
),
|
||||
),
|
||||
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: 'Max: $maxCapacity ml'.toText14(
|
||||
color: AppColors.greyTextColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
SizedBox(height: 24.h),
|
||||
CustomButton(
|
||||
text: 'Select'.needTranslation,
|
||||
onPressed: () {
|
||||
final newCup = WaterCupModel(
|
||||
id: widget.cupToEdit?.id ?? Uuid().v4(),
|
||||
name: '${_currentCapacity.round()}ml',
|
||||
capacityMl: _currentCapacity.round(),
|
||||
iconPath: AppAssets.cupEmpty,
|
||||
isDefault: false,
|
||||
);
|
||||
|
||||
final viewModel = context.read<WaterMonitorViewModel>();
|
||||
if (widget.cupToEdit != null) {
|
||||
viewModel.updateCup(newCup);
|
||||
} else {
|
||||
viewModel.addCup(newCup);
|
||||
}
|
||||
viewModel.selectCup(newCup.id);
|
||||
|
||||
Navigator.pop(context);
|
||||
},
|
||||
backgroundColor: AppColors.primaryRedColor,
|
||||
textColor: AppColors.whiteColor,
|
||||
fontSize: 16.f,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hmg_patient_app_new/core/app_assets.dart';
|
||||
import 'package:hmg_patient_app_new/core/app_export.dart';
|
||||
import 'package:hmg_patient_app_new/core/utils/utils.dart';
|
||||
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
|
||||
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
|
||||
import 'package:hmg_patient_app_new/theme/colors.dart';
|
||||
|
||||
class HydrationTipsWidget extends StatelessWidget {
|
||||
const HydrationTipsWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 24.w),
|
||||
padding: EdgeInsets.all(16.h),
|
||||
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
|
||||
color: AppColors.whiteColor,
|
||||
borderRadius: 24.r,
|
||||
hasShadow: true,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Utils.buildSvgWithAssets(
|
||||
icon: AppAssets.bulb_icon,
|
||||
width: 24.w,
|
||||
height: 24.h,
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
"Tips to stay hydrated".needTranslation.toText16(isBold: true),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 8.h),
|
||||
" • ${"Drink before you feel thirsty"}".needTranslation.toText12(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.textColorLight,
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
" • ${"Keep a refillable bottle next to you"}".needTranslation.toText12(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.textColorLight,
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
" • ${"Track your daily intake to stay motivated"}".needTranslation.toText12(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.textColorLight,
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
" • ${"Choose sparkling water instead of soda"}".needTranslation.toText12(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.textColorLight,
|
||||
),
|
||||
SizedBox(height: 8.h),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,174 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hmg_patient_app_new/core/app_assets.dart';
|
||||
import 'package:hmg_patient_app_new/core/app_export.dart';
|
||||
import 'package:hmg_patient_app_new/core/utils/utils.dart';
|
||||
import 'package:hmg_patient_app_new/extensions/route_extensions.dart';
|
||||
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
|
||||
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
|
||||
import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart';
|
||||
import 'package:hmg_patient_app_new/presentation/water_monitor/widgets/cup_bottomsheet_widgets.dart';
|
||||
import 'package:hmg_patient_app_new/theme/colors.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class WaterActionButtonsWidget extends StatelessWidget {
|
||||
const WaterActionButtonsWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<WaterMonitorViewModel>(builder: (context, vm, _) {
|
||||
final cupAmount = vm.selectedCupCapacityMl;
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
if (cupAmount > 0) {
|
||||
await vm.undoUserActivity();
|
||||
}
|
||||
},
|
||||
child: Utils.buildSvgWithAssets(
|
||||
icon: AppAssets.minimizeIcon,
|
||||
height: 20.h,
|
||||
width: 20.h,
|
||||
iconColor: AppColors.textColor,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 4.w),
|
||||
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
|
||||
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
|
||||
color: AppColors.blueColor,
|
||||
borderRadius: 99.r,
|
||||
hasShadow: true,
|
||||
),
|
||||
child: (cupAmount > 0 ? "+ $cupAmount ml" : "+ 0ml").toText12(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.whiteColor,
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
if (cupAmount > 0) {
|
||||
await vm.insertUserActivity(quantityIntake: cupAmount);
|
||||
}
|
||||
},
|
||||
child: Utils.buildSvgWithAssets(
|
||||
icon: AppAssets.addIconDark,
|
||||
height: 20.h,
|
||||
width: 20.h,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 8.h),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildActionButton(
|
||||
context: context,
|
||||
onTap: () => showSwitchCupBottomSheet(context),
|
||||
overlayWidget: AppAssets.refreshIcon,
|
||||
title: "Switch Cup".needTranslation,
|
||||
icon: Utils.buildSvgWithAssets(icon: AppAssets.glassIcon, height: 24.w, width: 24.w),
|
||||
),
|
||||
_buildActionButton(
|
||||
context: context,
|
||||
onTap: () async {
|
||||
final success = await vm.scheduleTestNotification();
|
||||
if (success) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Test notification will appear in 5 seconds!'.needTranslation),
|
||||
backgroundColor: AppColors.blueColor,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
margin: EdgeInsets.all(16.w),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Failed to schedule test notification'.needTranslation),
|
||||
backgroundColor: AppColors.errorColor,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
margin: EdgeInsets.all(16.w),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
title: "Plain Water".needTranslation,
|
||||
icon: Utils.buildSvgWithAssets(icon: AppAssets.glassIcon, height: 24.w, width: 24.w),
|
||||
),
|
||||
_buildActionButton(
|
||||
context: context,
|
||||
onTap: () => context.navigateWithName(AppRoutes.waterMonitorSettingsScreen),
|
||||
title: "Settings".needTranslation,
|
||||
icon: Icon(
|
||||
Icons.settings,
|
||||
color: AppColors.blueColor,
|
||||
size: 24.w,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildActionButton({
|
||||
required BuildContext context,
|
||||
String? overlayWidget,
|
||||
required String title,
|
||||
required Widget icon,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Column(
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
Container(
|
||||
height: 46.w,
|
||||
width: 46.w,
|
||||
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
|
||||
color: AppColors.blueColor.withValues(alpha: 0.14),
|
||||
borderRadius: 12.r,
|
||||
hasShadow: true,
|
||||
),
|
||||
child: Center(child: icon),
|
||||
),
|
||||
if (overlayWidget != null) ...[
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(2.w),
|
||||
height: 16.w,
|
||||
width: 16.w,
|
||||
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
|
||||
color: AppColors.blueColor,
|
||||
borderRadius: 100.r,
|
||||
hasShadow: true,
|
||||
),
|
||||
child: Center(
|
||||
child: Utils.buildSvgWithAssets(
|
||||
icon: AppAssets.refreshIcon,
|
||||
iconColor: AppColors.whiteColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
title.toText10(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,112 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hmg_patient_app_new/core/app_assets.dart';
|
||||
import 'package:hmg_patient_app_new/core/app_export.dart';
|
||||
import 'package:hmg_patient_app_new/core/utils/utils.dart';
|
||||
import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart';
|
||||
import 'package:hmg_patient_app_new/presentation/water_monitor/widgets/bottle_shape_clipper.dart';
|
||||
import 'package:hmg_patient_app_new/presentation/water_monitor/widgets/water_consumption_progress_widget.dart';
|
||||
import 'package:hmg_patient_app_new/theme/colors.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class WaterBottleWidget extends StatelessWidget {
|
||||
const WaterBottleWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<WaterMonitorViewModel>(
|
||||
builder: (context, vm, _) {
|
||||
final progressPercent = (vm.progress * 100).clamp(0.0, 100.0);
|
||||
|
||||
// SVG aspect ratio
|
||||
const svgAspectRatio = 315.0 / 143.0; // ~2.2
|
||||
|
||||
// Responsive bottle sizing with device-specific constraints
|
||||
double bottleWidth;
|
||||
if (isTablet) {
|
||||
bottleWidth = math.min(SizeUtils.width * 0.15, 180.0);
|
||||
} else if (isFoldable) {
|
||||
bottleWidth = math.min(100.w, 160.0);
|
||||
} else {
|
||||
bottleWidth = math.min(120.w, 140.0);
|
||||
}
|
||||
|
||||
final bottleHeight = bottleWidth * svgAspectRatio;
|
||||
|
||||
// Fillable area percentages
|
||||
final fillableHeightPercent = 0.7;
|
||||
const fillableWidthPercent = 0.8;
|
||||
|
||||
final fillableHeight = bottleHeight * fillableHeightPercent;
|
||||
final fillableWidth = bottleWidth * fillableWidthPercent;
|
||||
|
||||
// Device-specific positioning offsets
|
||||
final double leftOffset = isTablet ? 4.w : 8.w;
|
||||
final double bottomOffset = isTablet ? -65.h : -78.h;
|
||||
|
||||
return SizedBox(
|
||||
height: bottleHeight,
|
||||
width: bottleWidth,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Bottle SVG outline
|
||||
Center(
|
||||
child: Utils.buildSvgWithAssets(
|
||||
icon: AppAssets.waterBottle,
|
||||
height: bottleHeight,
|
||||
width: bottleWidth,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
|
||||
// Wave and bubbles clipped to bottle shape
|
||||
Positioned.fill(
|
||||
left: leftOffset,
|
||||
bottom: bottomOffset,
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: fillableWidth,
|
||||
height: fillableHeight,
|
||||
child: ClipPath(
|
||||
clipper: BottleShapeClipper(),
|
||||
child: Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
// Animated wave
|
||||
Positioned(
|
||||
child: WaterConsumptionProgressWidget(
|
||||
progress: progressPercent,
|
||||
size: math.min(fillableWidth, fillableHeight),
|
||||
containerWidth: fillableWidth,
|
||||
containerHeight: fillableHeight,
|
||||
waveDuration: const Duration(milliseconds: 3000),
|
||||
waveColor: AppColors.blueColor,
|
||||
),
|
||||
),
|
||||
|
||||
// Bubbles (only show if progress > 10%)
|
||||
if (progressPercent > 10)
|
||||
Positioned(
|
||||
bottom: fillableHeight * 0.12,
|
||||
child: Utils.buildSvgWithAssets(
|
||||
icon: AppAssets.waterBottleOuterBubbles,
|
||||
height: isTablet ? math.min(45.0, fillableHeight * 0.2) : math.min(55.0, fillableHeight * 0.22),
|
||||
width: fillableWidth * 0.65,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,78 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hmg_patient_app_new/presentation/water_monitor/widgets/water_splash_progress_widget.dart';
|
||||
import 'package:hmg_patient_app_new/theme/colors.dart';
|
||||
|
||||
class WaterConsumptionProgressWidget extends StatefulWidget {
|
||||
/// progress: 0.0 - 100.0
|
||||
final double progress;
|
||||
final double size;
|
||||
final Color? waveColor;
|
||||
final double? containerWidth;
|
||||
final double? containerHeight;
|
||||
final Duration? waveDuration;
|
||||
|
||||
const WaterConsumptionProgressWidget({super.key, required this.progress, this.size = 100, this.waveColor, this.containerWidth, this.containerHeight, this.waveDuration});
|
||||
|
||||
@override
|
||||
State<WaterConsumptionProgressWidget> createState() => _WaterConsumptionProgressWidgetState();
|
||||
}
|
||||
|
||||
class _WaterConsumptionProgressWidgetState extends State<WaterConsumptionProgressWidget> with SingleTickerProviderStateMixin {
|
||||
late AnimationController _progressController;
|
||||
late Animation<double> _progressAnimation;
|
||||
double _lastTarget = 0.0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_lastTarget = widget.progress;
|
||||
_progressController = AnimationController(vsync: this, duration: const Duration(milliseconds: 1200));
|
||||
_progressAnimation = Tween<double>(begin: 0, end: widget.progress).animate(CurvedAnimation(parent: _progressController, curve: Curves.easeInOut));
|
||||
_progressController.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant WaterConsumptionProgressWidget oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if ((widget.progress - _lastTarget).abs() > 0.01) {
|
||||
// animate from current value to new target
|
||||
final begin = _progressAnimation.value;
|
||||
_progressAnimation =
|
||||
Tween<double>(begin: begin, end: widget.progress).animate(CurvedAnimation(parent: _progressController, curve: Curves.easeInOut));
|
||||
_progressController
|
||||
..reset()
|
||||
..forward();
|
||||
_lastTarget = widget.progress;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_progressController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// default to app blue color if none provided
|
||||
final blue = widget.waveColor ?? AppColors.blueColor;
|
||||
return AnimatedBuilder(
|
||||
animation: _progressAnimation,
|
||||
builder: (context, child) {
|
||||
return WaterWaveProgress(
|
||||
progress: _progressAnimation.value,
|
||||
size: widget.size,
|
||||
showPercentage: false,
|
||||
useCircleClip: false,
|
||||
waveColor: blue,
|
||||
containerWidth: widget.containerWidth,
|
||||
containerHeight: widget.containerHeight,
|
||||
waveDuration: widget.waveDuration,
|
||||
backgroundColor: Colors.transparent,
|
||||
progressColor: Colors.transparent,
|
||||
textColor: Colors.transparent,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,116 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hmg_patient_app_new/core/app_export.dart';
|
||||
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
|
||||
import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart';
|
||||
import 'package:hmg_patient_app_new/presentation/water_monitor/widgets/water_action_buttons_widget.dart';
|
||||
import 'package:hmg_patient_app_new/presentation/water_monitor/widgets/water_bottle_widget.dart';
|
||||
import 'package:hmg_patient_app_new/theme/colors.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
|
||||
class WaterIntakeSummaryWidget extends StatelessWidget {
|
||||
const WaterIntakeSummaryWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.all(24.w),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
AppColors.blueGradientColorOne,
|
||||
AppColors.blueGradientColorTwo,
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: isTablet ? 2 : 3,
|
||||
child: Consumer<WaterMonitorViewModel>(builder: (context, vm, _) {
|
||||
if (vm.isLoading) {
|
||||
return _buildLoadingShimmer();
|
||||
}
|
||||
|
||||
final goalMl = vm.dailyGoalMl;
|
||||
final consumed = vm.totalConsumedMl;
|
||||
final remaining = (goalMl - consumed) > 0 ? (goalMl - consumed) : 0;
|
||||
final completedPercent = "${(vm.progress * 100).clamp(0.0, 100.0).toStringAsFixed(0)}%";
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
"Next Drink Time".needTranslation.toText18(weight: FontWeight.w600, color: AppColors.textColor),
|
||||
vm.nextDrinkTime.toText32(weight: FontWeight.w600, color: AppColors.blueColor),
|
||||
SizedBox(height: 12.h),
|
||||
_buildStatusColumn(title: "Your Goal".needTranslation, subTitle: "${goalMl}ml"),
|
||||
SizedBox(height: 8.h),
|
||||
_buildStatusColumn(title: "Remaining".needTranslation, subTitle: "${remaining}ml"),
|
||||
SizedBox(height: 8.h),
|
||||
_buildStatusColumn(title: "Completed".needTranslation, subTitle: completedPercent, subTitleColor: AppColors.successColor),
|
||||
SizedBox(height: 8.h),
|
||||
_buildStatusColumn(
|
||||
title: "Hydration Status".needTranslation,
|
||||
subTitle: vm.hydrationStatus,
|
||||
subTitleColor: vm.hydrationStatusColor,
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
SizedBox(width: isTablet ? 32 : 16.w),
|
||||
Expanded(
|
||||
flex: isTablet ? 1 : 2,
|
||||
child: const WaterBottleWidget(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const WaterActionButtonsWidget(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusColumn({required String title, required String subTitle, Color? subTitleColor}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
"$title: ".toText16(weight: FontWeight.w500, color: AppColors.textColor),
|
||||
subTitle.toText12(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: subTitleColor ?? AppColors.greyTextColor,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingShimmer() {
|
||||
return ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
padding: EdgeInsets.all(0.w),
|
||||
itemCount: 4,
|
||||
separatorBuilder: (_, __) => SizedBox(height: 12.h),
|
||||
itemBuilder: (context, index) {
|
||||
return Shimmer.fromColors(
|
||||
baseColor: AppColors.shimmerBaseColor,
|
||||
highlightColor: AppColors.shimmerHighlightColor,
|
||||
child: Container(
|
||||
height: 40.h,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.whiteColor,
|
||||
borderRadius: BorderRadius.circular(10.r),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,351 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hmg_patient_app_new/theme/colors.dart';
|
||||
|
||||
/// Spherical water wave progress bar with animated ripple effect
|
||||
/// Based on https://github.com/meeziest/spherical_water_wavy_progress_bar
|
||||
///
|
||||
/// Usage:
|
||||
/// WaterWaveProgress(
|
||||
/// progress: 75.0,
|
||||
/// size: 200.w,
|
||||
/// showPercentage: true,
|
||||
/// )
|
||||
class WaterWaveProgress extends StatefulWidget {
|
||||
final double progress; // 0.0 to 100.0
|
||||
final double size;
|
||||
final bool showPercentage;
|
||||
final bool useCircleClip;
|
||||
final Color? waveColor;
|
||||
final Color? backgroundColor;
|
||||
final Color? progressColor;
|
||||
final Color? textColor;
|
||||
final Duration? waveDuration;
|
||||
|
||||
// When drawing inside a non-square clip (like bottle area), provide the actual available
|
||||
// width and height so the painter can compute vertical fill correctly.
|
||||
final double? containerWidth;
|
||||
final double? containerHeight;
|
||||
|
||||
const WaterWaveProgress({
|
||||
super.key,
|
||||
required this.progress,
|
||||
this.size = 200,
|
||||
this.showPercentage = true,
|
||||
this.useCircleClip = true,
|
||||
this.waveColor,
|
||||
this.backgroundColor,
|
||||
this.progressColor,
|
||||
this.textColor,
|
||||
this.waveDuration,
|
||||
this.containerWidth,
|
||||
this.containerHeight,
|
||||
});
|
||||
|
||||
@override
|
||||
State<WaterWaveProgress> createState() => _WaterWaveProgressState();
|
||||
}
|
||||
|
||||
class _WaterWaveProgressState extends State<WaterWaveProgress> with SingleTickerProviderStateMixin {
|
||||
late AnimationController _waveController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_waveController = AnimationController(
|
||||
vsync: this,
|
||||
duration: widget.waveDuration ?? const Duration(milliseconds: 2000),
|
||||
)..repeat();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_waveController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final radius = widget.size / 2;
|
||||
|
||||
// When container dimensions are provided (non-square bottle area), use them so
|
||||
// the CustomPaint canvas matches the available clip rect. Otherwise fall back
|
||||
// to the square size value.
|
||||
final paintWidth = widget.containerWidth ?? widget.size;
|
||||
final paintHeight = widget.containerHeight ?? widget.size;
|
||||
|
||||
return SizedBox(
|
||||
width: paintWidth,
|
||||
height: paintHeight,
|
||||
child: CustomPaint(
|
||||
painter: _WavePainter(
|
||||
progress: widget.progress,
|
||||
waveAnimation: _waveController,
|
||||
// For circle-based painters we still provide circleRadius; when using
|
||||
// rectangular painting the circleRadius is unused.
|
||||
circleRadius: radius,
|
||||
waveColor: widget.waveColor ?? AppColors.primaryRedColor,
|
||||
backgroundColor: widget.backgroundColor ?? AppColors.bgScaffoldColor,
|
||||
useCircleClip: widget.useCircleClip,
|
||||
containerWidth: widget.containerWidth,
|
||||
containerHeight: widget.containerHeight,
|
||||
),
|
||||
// Only draw the circular progress/percentage when explicitly requested.
|
||||
foregroundPainter: widget.showPercentage
|
||||
? _ProgressPainter(
|
||||
progress: widget.progress,
|
||||
circleRadius: radius,
|
||||
progressColor: widget.progressColor ?? AppColors.primaryRedColor,
|
||||
textColor: widget.textColor ?? AppColors.textColor,
|
||||
showPercentage: widget.showPercentage,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Paints the animated water waves inside a circular clip
|
||||
class _WavePainter extends CustomPainter {
|
||||
final double progress;
|
||||
final Animation<double> waveAnimation;
|
||||
final double circleRadius;
|
||||
final Color waveColor;
|
||||
final Color backgroundColor;
|
||||
final bool useCircleClip;
|
||||
final double? containerWidth;
|
||||
final double? containerHeight;
|
||||
|
||||
_WavePainter({
|
||||
required this.progress,
|
||||
required this.waveAnimation,
|
||||
required this.circleRadius,
|
||||
required this.waveColor,
|
||||
required this.backgroundColor,
|
||||
this.useCircleClip = true,
|
||||
this.containerWidth,
|
||||
this.containerHeight,
|
||||
}) : super(repaint: waveAnimation);
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final center = Offset(size.width / 2, size.height / 2);
|
||||
canvas.translate(center.dx, center.dy);
|
||||
|
||||
if (useCircleClip) {
|
||||
// Clip to circle
|
||||
canvas.clipPath(
|
||||
Path()
|
||||
..addOval(
|
||||
Rect.fromCircle(
|
||||
center: Offset.zero,
|
||||
radius: circleRadius,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Fill background circle if specified
|
||||
if (backgroundColor != Colors.transparent) {
|
||||
canvas.drawCircle(
|
||||
Offset.zero,
|
||||
circleRadius,
|
||||
Paint()..color = backgroundColor,
|
||||
);
|
||||
}
|
||||
|
||||
// Draw two sine waves clipped to circle for spherical style
|
||||
_drawSineWave(canvas, waveColor.withAlpha((0.5 * 255).round()), shift: 0);
|
||||
_drawSineWave(canvas, waveColor, shift: circleRadius / 2, mirror: true);
|
||||
} else {
|
||||
// No circular clipping: draw waves over the full rectangular area. Use provided container
|
||||
// dimensions if available; otherwise fall back to square bounds derived from circleRadius.
|
||||
final w = containerWidth ?? (circleRadius * 2);
|
||||
final h = containerHeight ?? (circleRadius * 2);
|
||||
|
||||
// use local transforms to center at origin and compute waves relative to the provided rect
|
||||
canvas.save();
|
||||
canvas.translate(-w / 2, -h / 2); // move origin to top-left of the rect
|
||||
|
||||
_drawSineWaveRect(canvas, waveColor.withAlpha((0.5 * 255).round()), width: w, height: h, shift: 0);
|
||||
_drawSineWaveRect(canvas, waveColor, width: w, height: h, shift: w / 4, mirror: true);
|
||||
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
void _drawSineWaveRect(Canvas canvas, Color color, {required double width, required double height, double shift = 0.0, bool mirror = false}) {
|
||||
if (mirror) {
|
||||
canvas.save();
|
||||
canvas.translate(width, 0);
|
||||
canvas.scale(-1, 1);
|
||||
}
|
||||
|
||||
final amplitude = height * 0.04; // smaller amplitude for rectangular waves
|
||||
final angularVelocity = pi / (width / 2);
|
||||
final delta = Curves.easeInOut.transform(progress / 100);
|
||||
|
||||
final offsetX = 2 * width * waveAnimation.value + shift;
|
||||
final offsetY = height * (1.0 - delta);
|
||||
|
||||
final path = Path();
|
||||
for (double x = 0; x <= width; x += 1) {
|
||||
final y = amplitude * sin(angularVelocity * (x + offsetX));
|
||||
if (x == 0) {
|
||||
path.moveTo(x, y + offsetY);
|
||||
} else {
|
||||
path.lineTo(x, y + offsetY);
|
||||
}
|
||||
}
|
||||
|
||||
path.lineTo(width, height);
|
||||
path.lineTo(0, height);
|
||||
path.close();
|
||||
|
||||
final wavePaint = Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.fill
|
||||
..isAntiAlias = true;
|
||||
|
||||
canvas.drawPath(path, wavePaint);
|
||||
|
||||
if (mirror) canvas.restore();
|
||||
}
|
||||
|
||||
void _drawSineWave(Canvas canvas, Color color, {double shift = 0.0, bool mirror = false}) {
|
||||
if (mirror) {
|
||||
canvas.save();
|
||||
canvas.transform(Matrix4.rotationY(pi).storage);
|
||||
}
|
||||
|
||||
// original circular/spherical style: compute bounds based on circleRadius
|
||||
final startX = -circleRadius;
|
||||
final endX = circleRadius;
|
||||
final startY = circleRadius;
|
||||
final endY = -circleRadius;
|
||||
|
||||
final amplitude = circleRadius * 0.15;
|
||||
final angularVelocity = pi / circleRadius;
|
||||
final delta = Curves.easeInOut.transform(progress / 100);
|
||||
|
||||
final offsetX = 2 * circleRadius * waveAnimation.value + shift;
|
||||
final offsetY = startY + (endY - startY - amplitude) * delta;
|
||||
|
||||
final wavePaint = Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.fill
|
||||
..isAntiAlias = true;
|
||||
|
||||
final path = Path();
|
||||
|
||||
for (double x = startX; x <= endX; x++) {
|
||||
// Sine wave function: y = A * sin(ωx + φ)
|
||||
final y = amplitude * sin(angularVelocity * (x + offsetX));
|
||||
if (x == startX) {
|
||||
path.moveTo(x, y + offsetY);
|
||||
} else {
|
||||
path.lineTo(x, y + offsetY);
|
||||
}
|
||||
}
|
||||
|
||||
path.lineTo(endX, startY);
|
||||
path.lineTo(startX, startY);
|
||||
path.close();
|
||||
|
||||
canvas.drawPath(path, wavePaint);
|
||||
|
||||
if (mirror) canvas.restore();
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _WavePainter oldDelegate) => oldDelegate.progress != progress;
|
||||
}
|
||||
|
||||
/// Paints the circular progress arc and percentage text
|
||||
class _ProgressPainter extends CustomPainter {
|
||||
final double progress;
|
||||
final double circleRadius;
|
||||
final Color progressColor;
|
||||
final Color textColor;
|
||||
final bool showPercentage;
|
||||
|
||||
_ProgressPainter({
|
||||
required this.progress,
|
||||
required this.circleRadius,
|
||||
required this.progressColor,
|
||||
required this.textColor,
|
||||
required this.showPercentage,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final center = Offset(size.width / 2, size.height / 2);
|
||||
canvas.translate(center.dx, center.dy);
|
||||
|
||||
_drawCircleProgress(canvas);
|
||||
if (showPercentage) {
|
||||
_drawProgressText(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
void _drawCircleProgress(Canvas canvas) {
|
||||
final strokeWidth = circleRadius * 0.077;
|
||||
|
||||
// Background circle with shadow
|
||||
final bgPaint = Paint()
|
||||
..color = progressColor.withAlpha((0.2 * 255).round())
|
||||
..strokeWidth = strokeWidth
|
||||
..style = PaintingStyle.stroke
|
||||
..isAntiAlias = true;
|
||||
|
||||
final shadowPaint = Paint()
|
||||
..color = Colors.black.withAlpha((0.1 * 255).round())
|
||||
..strokeWidth = strokeWidth
|
||||
..style = PaintingStyle.stroke
|
||||
..maskFilter = const MaskFilter.blur(BlurStyle.outer, 10);
|
||||
|
||||
canvas.drawCircle(Offset.zero, circleRadius, bgPaint);
|
||||
canvas.drawCircle(Offset.zero, circleRadius, shadowPaint);
|
||||
|
||||
// Progress arc
|
||||
final progressPaint = Paint()
|
||||
..color = progressColor
|
||||
..strokeWidth = strokeWidth
|
||||
..strokeCap = StrokeCap.round
|
||||
..style = PaintingStyle.stroke
|
||||
..isAntiAlias = true;
|
||||
|
||||
canvas.drawArc(
|
||||
Rect.fromCircle(center: Offset.zero, radius: circleRadius),
|
||||
-0.5 * pi,
|
||||
2 * pi * (progress / 100),
|
||||
false,
|
||||
progressPaint,
|
||||
);
|
||||
}
|
||||
|
||||
void _drawProgressText(Canvas canvas) {
|
||||
final textSpan = TextSpan(
|
||||
text: "${progress.toInt()}%",
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: circleRadius * 0.35,
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1.0,
|
||||
),
|
||||
);
|
||||
|
||||
final textPainter = TextPainter(
|
||||
text: textSpan,
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout();
|
||||
|
||||
textPainter.paint(
|
||||
canvas,
|
||||
Offset(-textPainter.width / 2, -textPainter.height / 2),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _ProgressPainter oldDelegate) => oldDelegate.progress != progress;
|
||||
}
|
||||
@ -0,0 +1,373 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:hmg_patient_app_new/services/logger_service.dart';
|
||||
import 'package:timezone/data/latest_all.dart' as tz;
|
||||
import 'package:timezone/timezone.dart' as tz show TZDateTime, local, setLocalLocation, getLocation;
|
||||
|
||||
/// Abstract class defining the notification service interface
|
||||
abstract class NotificationService {
|
||||
/// Initialize the notification service
|
||||
Future<void> initialize({Function(String payload)? onNotificationClick});
|
||||
|
||||
/// Request notification permissions (mainly for iOS)
|
||||
Future<bool> requestPermissions();
|
||||
|
||||
/// Show an immediate notification
|
||||
Future<void> showNotification({
|
||||
required String title,
|
||||
required String body,
|
||||
String? payload,
|
||||
});
|
||||
|
||||
/// Schedule a notification at a specific date and time
|
||||
Future<void> scheduleNotification({
|
||||
required int id,
|
||||
required String title,
|
||||
required String body,
|
||||
required DateTime scheduledDate,
|
||||
String? payload,
|
||||
});
|
||||
|
||||
/// Schedule daily notifications at specific times
|
||||
Future<void> scheduleDailyNotifications({
|
||||
required List<DateTime> times,
|
||||
required String title,
|
||||
required String body,
|
||||
String? payload,
|
||||
});
|
||||
|
||||
/// Schedule water reminder notifications
|
||||
Future<void> scheduleWaterReminders({
|
||||
required List<DateTime> reminderTimes,
|
||||
required String title,
|
||||
required String body,
|
||||
});
|
||||
|
||||
/// Cancel a specific notification by id
|
||||
Future<void> cancelNotification(int id);
|
||||
|
||||
/// Cancel all scheduled notifications
|
||||
Future<void> cancelAllNotifications();
|
||||
|
||||
/// Get list of pending notifications
|
||||
Future<List<PendingNotificationRequest>> getPendingNotifications();
|
||||
}
|
||||
|
||||
/// Implementation of NotificationService following the project architecture
|
||||
class NotificationServiceImp implements NotificationService {
|
||||
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin;
|
||||
final LoggerService loggerService;
|
||||
|
||||
NotificationServiceImp({required this.flutterLocalNotificationsPlugin, required this.loggerService});
|
||||
|
||||
// Channel IDs for different notification types
|
||||
static const String _waterReminderChannelId = 'water_reminder_channel';
|
||||
static const String _waterReminderChannelName = 'Water Reminders';
|
||||
static const String _waterReminderChannelDescription = 'Daily water intake reminders';
|
||||
|
||||
static const String _generalChannelId = 'hmg_general_channel';
|
||||
static const String _generalChannelName = 'HMG Notifications';
|
||||
static const String _generalChannelDescription = 'General notifications from HMG';
|
||||
|
||||
Function(String payload)? _onNotificationClick;
|
||||
|
||||
@override
|
||||
Future<void> initialize({Function(String payload)? onNotificationClick}) async {
|
||||
try {
|
||||
// Initialize timezone database
|
||||
tz.initializeTimeZones();
|
||||
|
||||
// Set local timezone (you can also use a specific timezone if needed)
|
||||
// For example: tz.setLocalLocation(tz.getLocation('Asia/Riyadh'));
|
||||
final locationName = DateTime.now().timeZoneName;
|
||||
try {
|
||||
tz.setLocalLocation(tz.getLocation(locationName));
|
||||
} catch (e) {
|
||||
// Fallback to UTC if specific timezone not found
|
||||
loggerService.logInfo('Could not set timezone $locationName, using UTC');
|
||||
tz.setLocalLocation(tz.getLocation('UTC'));
|
||||
}
|
||||
|
||||
_onNotificationClick = onNotificationClick;
|
||||
|
||||
const androidSettings = AndroidInitializationSettings('app_icon');
|
||||
const iosSettings = DarwinInitializationSettings(
|
||||
requestAlertPermission: true,
|
||||
requestBadgePermission: true,
|
||||
requestSoundPermission: true,
|
||||
);
|
||||
|
||||
const initializationSettings = InitializationSettings(
|
||||
android: androidSettings,
|
||||
iOS: iosSettings,
|
||||
);
|
||||
|
||||
await flutterLocalNotificationsPlugin.initialize(
|
||||
initializationSettings,
|
||||
onDidReceiveNotificationResponse: _handleNotificationResponse,
|
||||
);
|
||||
|
||||
loggerService.logInfo('NotificationService initialized successfully');
|
||||
} catch (ex) {
|
||||
loggerService.logError('Failed to initialize NotificationService: $ex');
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle notification tap
|
||||
void _handleNotificationResponse(NotificationResponse response) {
|
||||
try {
|
||||
if (response.payload != null && _onNotificationClick != null) {
|
||||
_onNotificationClick!(response.payload!);
|
||||
}
|
||||
loggerService.logInfo('Notification tapped: ${response.payload}');
|
||||
} catch (ex) {
|
||||
loggerService.logError('Error handling notification response: $ex');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> requestPermissions() async {
|
||||
try {
|
||||
// Request permissions for iOS
|
||||
final result =
|
||||
await flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>()?.requestPermissions(
|
||||
alert: true,
|
||||
badge: true,
|
||||
sound: true,
|
||||
);
|
||||
|
||||
// For Android 13+, permissions are requested at runtime
|
||||
final androidResult = await flutterLocalNotificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
|
||||
?.requestNotificationsPermission();
|
||||
|
||||
loggerService.logInfo('Notification permissions: iOS=${result ?? true}, Android=${androidResult ?? true}');
|
||||
return result ?? androidResult ?? true;
|
||||
} catch (ex) {
|
||||
loggerService.logError('Error requesting notification permissions: $ex');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> showNotification({
|
||||
required String title,
|
||||
required String body,
|
||||
String? payload,
|
||||
}) async {
|
||||
try {
|
||||
final androidDetails = AndroidNotificationDetails(
|
||||
_generalChannelId,
|
||||
_generalChannelName,
|
||||
channelDescription: _generalChannelDescription,
|
||||
importance: Importance.high,
|
||||
priority: Priority.high,
|
||||
vibrationPattern: _getVibrationPattern(),
|
||||
);
|
||||
|
||||
const iosDetails = DarwinNotificationDetails();
|
||||
|
||||
final notificationDetails = NotificationDetails(
|
||||
android: androidDetails,
|
||||
iOS: iosDetails,
|
||||
);
|
||||
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
title,
|
||||
body,
|
||||
notificationDetails,
|
||||
payload: payload,
|
||||
);
|
||||
|
||||
loggerService.logInfo('Notification shown: $title');
|
||||
} catch (ex) {
|
||||
loggerService.logError('Error showing notification: $ex');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> scheduleNotification({
|
||||
required int id,
|
||||
required String title,
|
||||
required String body,
|
||||
required DateTime scheduledDate,
|
||||
String? payload,
|
||||
}) async {
|
||||
try {
|
||||
final androidDetails = AndroidNotificationDetails(
|
||||
_generalChannelId,
|
||||
_generalChannelName,
|
||||
channelDescription: _generalChannelDescription,
|
||||
importance: Importance.high,
|
||||
priority: Priority.high,
|
||||
vibrationPattern: _getVibrationPattern(),
|
||||
);
|
||||
|
||||
const iosDetails = DarwinNotificationDetails();
|
||||
|
||||
final notificationDetails = NotificationDetails(
|
||||
android: androidDetails,
|
||||
iOS: iosDetails,
|
||||
);
|
||||
|
||||
await flutterLocalNotificationsPlugin.zonedSchedule(
|
||||
id,
|
||||
title,
|
||||
body,
|
||||
tz.TZDateTime.from(scheduledDate, tz.local),
|
||||
notificationDetails,
|
||||
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
|
||||
payload: payload,
|
||||
);
|
||||
|
||||
loggerService.logInfo('Notification scheduled for: $scheduledDate');
|
||||
} catch (ex) {
|
||||
loggerService.logError('Error scheduling notification: $ex');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> scheduleDailyNotifications({
|
||||
required List<DateTime> times,
|
||||
required String title,
|
||||
required String body,
|
||||
String? payload,
|
||||
}) async {
|
||||
try {
|
||||
for (int i = 0; i < times.length; i++) {
|
||||
final time = times[i];
|
||||
await scheduleNotification(
|
||||
id: i + 1000,
|
||||
// Offset ID to avoid conflicts
|
||||
title: title,
|
||||
body: body,
|
||||
scheduledDate: time,
|
||||
payload: payload,
|
||||
);
|
||||
}
|
||||
|
||||
loggerService.logInfo('Scheduled ${times.length} daily notifications');
|
||||
} catch (ex) {
|
||||
loggerService.logError('Error scheduling daily notifications: $ex');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> scheduleWaterReminders({
|
||||
required List<DateTime> reminderTimes,
|
||||
required String title,
|
||||
required String body,
|
||||
}) async {
|
||||
try {
|
||||
// Cancel existing water reminders first
|
||||
await _cancelWaterReminders();
|
||||
|
||||
final androidDetails = AndroidNotificationDetails(
|
||||
_waterReminderChannelId,
|
||||
_waterReminderChannelName,
|
||||
channelDescription: _waterReminderChannelDescription,
|
||||
importance: Importance.high,
|
||||
priority: Priority.high,
|
||||
vibrationPattern: _getVibrationPattern(),
|
||||
icon: 'app_icon',
|
||||
styleInformation: const BigTextStyleInformation(''),
|
||||
);
|
||||
|
||||
const iosDetails = DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: true,
|
||||
);
|
||||
|
||||
final notificationDetails = NotificationDetails(
|
||||
android: androidDetails,
|
||||
iOS: iosDetails,
|
||||
);
|
||||
|
||||
for (int i = 0; i < reminderTimes.length; i++) {
|
||||
final reminderTime = reminderTimes[i];
|
||||
final notificationId = 5000 + i; // Use 5000+ range for water reminders
|
||||
|
||||
// Schedule for today if time hasn't passed, otherwise schedule for tomorrow
|
||||
DateTime scheduledDate = reminderTime;
|
||||
if (scheduledDate.isBefore(DateTime.now())) {
|
||||
scheduledDate = scheduledDate.add(const Duration(days: 1));
|
||||
}
|
||||
|
||||
await flutterLocalNotificationsPlugin.zonedSchedule(
|
||||
notificationId,
|
||||
title,
|
||||
body,
|
||||
tz.TZDateTime.from(scheduledDate, tz.local),
|
||||
notificationDetails,
|
||||
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
|
||||
matchDateTimeComponents: DateTimeComponents.time, // Repeat daily at the same time
|
||||
payload: 'water_reminder_$i',
|
||||
);
|
||||
}
|
||||
|
||||
loggerService.logInfo('Scheduled ${reminderTimes.length} water reminders');
|
||||
} catch (ex) {
|
||||
loggerService.logError('Error scheduling water reminders: $ex');
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancel all water reminders (IDs 5000-5999)
|
||||
Future<void> _cancelWaterReminders() async {
|
||||
try {
|
||||
final pendingNotifications = await getPendingNotifications();
|
||||
for (final notification in pendingNotifications) {
|
||||
if (notification.id >= 5000 && notification.id < 6000) {
|
||||
await cancelNotification(notification.id);
|
||||
}
|
||||
}
|
||||
loggerService.logInfo('Cancelled all water reminders');
|
||||
} catch (ex) {
|
||||
loggerService.logError('Error cancelling water reminders: $ex');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> cancelNotification(int id) async {
|
||||
try {
|
||||
await flutterLocalNotificationsPlugin.cancel(id);
|
||||
loggerService.logInfo('Cancelled notification with ID: $id');
|
||||
} catch (ex) {
|
||||
loggerService.logError('Error cancelling notification: $ex');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> cancelAllNotifications() async {
|
||||
try {
|
||||
await flutterLocalNotificationsPlugin.cancelAll();
|
||||
loggerService.logInfo('Cancelled all notifications');
|
||||
} catch (ex) {
|
||||
loggerService.logError('Error cancelling all notifications: $ex');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<PendingNotificationRequest>> getPendingNotifications() async {
|
||||
try {
|
||||
final pending = await flutterLocalNotificationsPlugin.pendingNotificationRequests();
|
||||
loggerService.logInfo('Found ${pending.length} pending notifications');
|
||||
return pending;
|
||||
} catch (ex) {
|
||||
loggerService.logError('Error getting pending notifications: $ex');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Get vibration pattern for notifications
|
||||
Int64List _getVibrationPattern() {
|
||||
final vibrationPattern = Int64List(4);
|
||||
vibrationPattern[0] = 0;
|
||||
vibrationPattern[1] = 500;
|
||||
vibrationPattern[2] = 500;
|
||||
vibrationPattern[3] = 500;
|
||||
return vibrationPattern;
|
||||
}
|
||||
}
|
||||