This commit is contained in:
Riccardo Usardi committed 2025-07-18 22:24:01 +02:00
commit e833bcaec4
224 files changed
+11812

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 765 KiB

+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--Generator: Apple Native CoreSVG 175.5-->
<!DOCTYPE svg
PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="17.0605" height="18.4082">
<g>
<rect height="18.4082" opacity="0" width="17.0605" x="0" y="0"/>
<path d="M2.39258 18.4082L14.668 18.4082C16.2891 18.4082 17.0605 17.9199 17.0605 16.8457C17.0605 14.2871 13.8281 10.5859 8.53516 10.5859C3.23242 10.5859 0 14.2871 0 16.8457C0 17.9199 0.771484 18.4082 2.39258 18.4082ZM1.93359 16.9336C1.67969 16.9336 1.57227 16.8652 1.57227 16.6602C1.57227 15.0586 4.05273 12.0605 8.53516 12.0605C13.0078 12.0605 15.4883 15.0586 15.4883 16.6602C15.4883 16.8652 15.3906 16.9336 15.1367 16.9336ZM8.53516 9.375C10.8594 9.375 12.7539 7.30469 12.7539 4.78516C12.7539 2.28516 10.8691 0.322266 8.53516 0.322266C6.2207 0.322266 4.31641 2.32422 4.31641 4.80469C4.32617 7.31445 6.21094 9.375 8.53516 9.375ZM8.53516 7.90039C7.10938 7.90039 5.88867 6.5332 5.88867 4.80469C5.88867 3.10547 7.08984 1.79688 8.53516 1.79688C9.99023 1.79688 11.1816 3.08594 11.1816 4.78516C11.1816 6.51367 9.9707 7.90039 8.53516 7.90039Z" fill="#ffffff" fill-opacity="0.85"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" id="dashboard-apps"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 5a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V5zM4 15a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-4zM4 5a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V5zm8 10a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-6a1 1 0 0 1-1-1v-4z"></path></svg>

After

Width:  |  Height:  |  Size: 480 B

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
<circle cx="9" cy="9" r="4" fill="#494c4e" fill-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 282 B

+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--Generator: Apple Native CoreSVG 175.5-->
<!DOCTYPE svg
PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="26.4746" height="16.6113">
<g>
<rect height="16.6113" opacity="0" width="26.4746" x="0" y="0"/>
<path d="M13.2422 16.6113C21.0645 16.6113 26.4746 10.2832 26.4746 8.31055C26.4746 6.32812 21.0547 0.00976562 13.2422 0.00976562C5.51758 0.00976562 0 6.32812 0 8.31055C0 10.2832 5.51758 16.6113 13.2422 16.6113ZM13.2422 13.75C10.1953 13.75 7.7832 11.2793 7.77344 8.31055C7.76367 5.26367 10.1953 2.87109 13.2422 2.87109C16.2598 2.87109 18.7012 5.26367 18.7012 8.31055C18.7012 11.2793 16.2598 13.75 13.2422 13.75ZM13.2422 10.2734C14.3262 10.2734 15.2246 9.38477 15.2246 8.31055C15.2246 7.22656 14.3262 6.33789 13.2422 6.33789C12.1484 6.33789 11.25 7.22656 11.25 8.31055C11.25 9.38477 12.1484 10.2734 13.2422 10.2734Z" fill="#ffffff" fill-opacity="0.85"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--Generator: Apple Native CoreSVG 175.5-->
<!DOCTYPE svg
PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="26.4746" height="18.2812">
<g>
<rect height="18.2812" opacity="0" width="26.4746" x="0" y="0"/>
<path d="M13.2422 17.4463C15.0488 17.4463 16.7285 17.1143 18.2031 16.5674L15.6445 14.0088C14.9219 14.3799 14.1016 14.585 13.2422 14.585C10.1953 14.585 7.7832 12.1143 7.77344 9.14551C7.77344 8.2666 7.97852 7.43652 8.33984 6.7041L5 3.34473C1.92383 5.26855 0 7.97363 0 9.14551C0 11.1182 5.51758 17.4463 13.2422 17.4463ZM21.7578 14.7803C24.6094 12.8955 26.4746 10.3076 26.4746 9.14551C26.4746 7.16309 21.0547 0.844727 13.2422 0.844727C11.582 0.844727 10.0195 1.12793 8.60352 1.59668L11.1426 4.13574C11.7871 3.8623 12.4902 3.70605 13.2422 3.70605C16.2598 3.70605 18.7012 6.09863 18.7012 9.14551C18.7012 9.8877 18.5352 10.6006 18.2422 11.2256ZM10.127 8.9209C10.127 10.6396 11.543 12.0264 13.252 12.0264C13.3887 12.0264 13.5156 12.0166 13.6523 12.0068L10.1465 8.50098C10.1367 8.6377 10.127 8.78418 10.127 8.9209ZM16.3477 8.94043C16.3477 7.23145 14.9707 5.84473 13.2422 5.84473C13.1152 5.84473 12.9883 5.85449 12.8711 5.86426L16.3281 9.32129C16.3379 9.2041 16.3477 9.06738 16.3477 8.94043ZM20.8398 17.6025C21.1328 17.9053 21.6016 17.9248 21.9043 17.6025C22.2266 17.2803 22.1973 16.8408 21.9043 16.5381L5.57617 0.219727C5.2832-0.0732422 4.80469-0.0732422 4.50195 0.219727C4.21875 0.50293 4.21875 1.00098 4.50195 1.28418Z" fill="#ffffff" fill-opacity="0.85"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--Generator: Apple Native CoreSVG 175.5-->
<!DOCTYPE svg
PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="11.6553" height="11.0815">
<g>
<rect height="11.0815" opacity="0" width="11.6553" x="0" y="0"/>
<path d="M7.08496 9.46899L4.56543 9.46899L4.56543 6.24146C4.56543 6.01685 4.7168 5.87036 4.94141 5.87036L6.71387 5.87036C6.93848 5.87036 7.08496 6.01685 7.08496 6.24146ZM1.52344 9.17603C1.52344 9.85474 1.93359 10.2551 2.62207 10.2551L9.04297 10.2551C9.73145 10.2551 10.1367 9.85474 10.1367 9.17603L10.1367 5.30884L6.12305 1.94458C5.9375 1.78833 5.71777 1.79321 5.53711 1.94458L1.52344 5.30884ZM0 4.82056C0 5.02075 0.15625 5.21118 0.419922 5.21118C0.556641 5.21118 0.668945 5.13794 0.771484 5.05493L5.65918 0.953369C5.7666 0.855713 5.89844 0.855713 6.00586 0.953369L10.8936 5.05493C10.9912 5.13794 11.1035 5.21118 11.2402 5.21118C11.4697 5.21118 11.6553 5.06958 11.6553 4.83521C11.6553 4.68872 11.6064 4.59106 11.5039 4.50317L6.4209 0.230713C6.05957-0.0769043 5.61035-0.0769043 5.24414 0.230713L0.15625 4.50317C0.0488281 4.59106 0 4.70825 0 4.82056ZM8.91113 2.61841L10.1367 3.65356L10.1367 1.39771C10.1367 1.18286 10 1.04614 9.78516 1.04614L9.2627 1.04614C9.05273 1.04614 8.91113 1.18286 8.91113 1.39771Z" fill="#ffffff" fill-opacity="0.85"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+5
View File
@@ -0,0 +1,5 @@
<svg width="2421" height="1364" viewBox="0 0 2421 1364" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.4614 620.447L657.461 1241.05C659.404 1242.94 660.5 1245.53 660.5 1248.23V1354C660.5 1359.52 656.023 1364 650.5 1364H10.5C4.97715 1364 0.5 1359.52 0.5 1354V1244V627.626C0.5 618.791 11.1187 614.297 17.4614 620.447Z" fill="#64748B"/>
<path d="M2403.04 620.447L1763.04 1241.05C1761.1 1242.94 1760 1245.53 1760 1248.23V1354C1760 1359.52 1764.48 1364 1770 1364H2410C2415.52 1364 2420 1359.52 2420 1354V1244V627.626C2420 618.791 2409.38 614.297 2403.04 620.447Z" fill="#64748B"/>
<path d="M0 145.758V10C0 4.47715 4.47715 0 10 0H565.555C568.177 0 570.695 1.03027 572.565 2.86882L1203.49 623.108C1207.38 626.933 1213.62 626.933 1217.51 623.108L1848.43 2.86881C1850.3 1.03026 1852.82 0 1855.45 0H2411C2416.52 0 2421 4.47715 2421 10V145.758C2421 148.469 2419.9 151.064 2417.95 152.948L1217.45 1313.28C1213.57 1317.03 1207.43 1317.03 1203.55 1313.28L3.05027 152.948C1.10086 151.064 0 148.469 0 145.758Z" fill="#64748B"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--Generator: Apple Native CoreSVG 175.5-->
<!DOCTYPE svg
PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="19.9219" height="19.9316">
<g>
<rect height="19.9316" opacity="0" width="19.9219" x="0" y="0"/>
<path d="M9.96094 19.9219C15.4102 19.9219 19.9219 15.4004 19.9219 9.96094C19.9219 4.51172 15.4004 0 9.95117 0C4.51172 0 0 4.51172 0 9.96094C0 15.4004 4.52148 19.9219 9.96094 19.9219ZM9.96094 18.2617C5.35156 18.2617 1.66992 14.5703 1.66992 9.96094C1.66992 5.35156 5.3418 1.66016 9.95117 1.66016C14.5605 1.66016 18.2617 5.35156 18.2617 9.96094C18.2617 14.5703 14.5703 18.2617 9.96094 18.2617ZM6.12305 10.791L13.7891 10.791C14.3359 10.791 14.707 10.498 14.707 9.98047C14.707 9.45312 14.3555 9.15039 13.7891 9.15039L6.12305 9.15039C5.55664 9.15039 5.19531 9.45312 5.19531 9.98047C5.19531 10.498 5.57617 10.791 6.12305 10.791Z" fill="#ffffff" fill-opacity="0.85"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800px" height="800px" viewBox="0 0 20 20" fill="none">
<path fill="#000000" fill-rule="evenodd" d="M19 4a1 1 0 01-1 1H2a1 1 0 010-2h16a1 1 0 011 1zm0 6a1 1 0 01-1 1H2a1 1 0 110-2h16a1 1 0 011 1zm-1 7a1 1 0 100-2H2a1 1 0 100 2h16z"/>
</svg>

After

Width:  |  Height:  |  Size: 288 B

+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--Generator: Apple Native CoreSVG 175.5-->
<!DOCTYPE svg
PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="12.4707" height="24.2449">
<g>
<rect height="24.2449" opacity="0" width="12.4707" x="0" y="0"/>
<path d="M6.24023 0.144902C2.76367 0.144902 0 2.90857 0 6.36561C0 8.97303 1.5625 11.2777 4.00391 12.2054L4.00391 21.8441C4.00391 22.0883 4.08203 22.2738 4.25781 22.4594L5.88867 24.1C6.05469 24.266 6.38672 24.3148 6.5918 24.1097L9.6875 21.014C9.89258 20.7992 9.88281 20.516 9.6875 20.3109L7.76367 18.4164L10.4004 15.7894C10.5859 15.5941 10.5859 15.3109 10.3809 15.0961L7.77344 12.4691C10.791 11.2679 12.4707 9.04139 12.4707 6.36561C12.4707 2.92811 9.6875 0.144902 6.24023 0.144902ZM6.24023 5.86756C5.33203 5.86756 4.60938 5.13514 4.60938 4.2367C4.60938 3.3285 5.32227 2.60584 6.24023 2.60584C7.13867 2.60584 7.87109 3.3285 7.87109 4.2367C7.87109 5.13514 7.13867 5.86756 6.24023 5.86756Z" fill="#ffffff" fill-opacity="0.85"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--Generator: Apple Native CoreSVG 175.5-->
<!DOCTYPE svg
PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="19.9219" height="19.9316">
<g>
<rect height="19.9316" opacity="0" width="19.9219" x="0" y="0"/>
<path d="M9.96094 19.9219C15.4102 19.9219 19.9219 15.4004 19.9219 9.96094C19.9219 4.51172 15.4004 0 9.95117 0C4.51172 0 0 4.51172 0 9.96094C0 15.4004 4.52148 19.9219 9.96094 19.9219ZM9.96094 18.2617C5.35156 18.2617 1.66992 14.5703 1.66992 9.96094C1.66992 5.35156 5.3418 1.66016 9.95117 1.66016C14.5605 1.66016 18.2617 5.35156 18.2617 9.96094C18.2617 14.5703 14.5703 18.2617 9.96094 18.2617ZM16.6406 16.3965L16.6113 16.2891C16.1328 14.8535 13.5547 13.2812 9.96094 13.2812C6.37695 13.2812 3.79883 14.8535 3.31055 16.2793L3.28125 16.3965C5.03906 18.1348 8.05664 19.1504 9.96094 19.1504C11.875 19.1504 14.8633 18.1445 16.6406 16.3965ZM9.96094 11.6211C11.8457 11.6406 13.3105 10.0391 13.3105 7.93945C13.3105 5.9668 11.8359 4.33594 9.96094 4.33594C8.08594 4.33594 6.60156 5.9668 6.61133 7.93945C6.62109 10.0391 8.08594 11.6016 9.96094 11.6211Z" fill="#ffffff" fill-opacity="0.85"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--Generator: Apple Native CoreSVG 175.5-->
<!DOCTYPE svg
PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="21.9922" height="21.6794">
<g>
<rect height="21.6794" opacity="0" width="21.9922" x="0" y="0"/>
<path d="M10.9961 20.3465C11.25 20.3465 11.4941 20.327 11.7578 20.3075L12.3145 21.3719C12.4316 21.5965 12.6367 21.704 12.9102 21.6747C13.1543 21.6258 13.3203 21.4403 13.3594 21.1864L13.5254 20.0047C14.0137 19.868 14.4922 19.6825 14.9512 19.4872L15.8301 20.2782C16.0156 20.454 16.25 20.4833 16.4941 20.3563C16.709 20.2294 16.7969 20.0047 16.748 19.7411L16.5039 18.579C16.9141 18.2958 17.3145 17.9735 17.6758 17.6122L18.7598 18.0614C19.0039 18.1688 19.2383 18.1102 19.4238 17.8954C19.5898 17.7098 19.5996 17.4657 19.4629 17.2411L18.8281 16.2254C19.1211 15.8251 19.3555 15.3758 19.5801 14.9071L20.7812 14.9657C21.0352 14.9852 21.2402 14.8387 21.3281 14.6044C21.4062 14.3602 21.3379 14.1258 21.1328 13.9696L20.1953 13.2274C20.3223 12.7489 20.4199 12.2508 20.4688 11.7235L21.5918 11.3622C21.8457 11.284 21.9922 11.0985 21.9922 10.8348C21.9922 10.5712 21.8457 10.3856 21.5918 10.2977L20.4688 9.94615C20.4199 9.41881 20.3223 8.93053 20.1953 8.44225L21.1328 7.70006C21.3281 7.54381 21.4062 7.3192 21.3281 7.07506C21.2402 6.84068 21.0352 6.6942 20.7812 6.71373L19.5801 6.75279C19.3555 6.28404 19.1211 5.85435 18.8281 5.43443L19.4629 4.41881C19.5996 4.21373 19.5801 3.96959 19.4238 3.78404C19.2383 3.5692 19.0039 3.52037 18.7598 3.61803L17.6758 4.05748C17.3145 3.70592 16.9141 3.37389 16.5039 3.09068L16.748 1.93834C16.8066 1.6649 16.709 1.44029 16.4941 1.3231C16.25 1.18639 16.0156 1.21568 15.8301 1.40123L14.9512 2.18248C14.4922 1.9774 14.0137 1.81139 13.5254 1.6649L13.3594 0.493026C13.3203 0.23912 13.1445 0.0535732 12.9004 0.0047451C12.6367-0.0245518 12.4219 0.0828701 12.3145 0.297714L11.7578 1.36217C11.4941 1.34264 11.25 1.33287 10.9961 1.33287C10.7324 1.33287 10.498 1.34264 10.2344 1.36217L9.66797 0.297714C9.56055 0.0828701 9.3457-0.0245518 9.08203 0.0047451C8.83789 0.0535732 8.66211 0.23912 8.63281 0.493026L8.45703 1.6649C7.96875 1.81139 7.5 1.96764 7.03125 2.18248L6.15234 1.40123C5.9668 1.21568 5.73242 1.19615 5.48828 1.3231C5.27344 1.44029 5.17578 1.6649 5.23438 1.93834L5.47852 3.09068C5.07812 3.37389 4.66797 3.70592 4.30664 4.05748L3.22266 3.61803C2.97852 3.52037 2.75391 3.5692 2.56836 3.78404C2.40234 3.96959 2.38281 4.21373 2.51953 4.41881L3.1543 5.43443C2.87109 5.85435 2.62695 6.28404 2.40234 6.75279L1.21094 6.71373C0.957031 6.6942 0.742188 6.84068 0.654297 7.07506C0.576172 7.3192 0.654297 7.54381 0.849609 7.70006L1.78711 8.44225C1.66016 8.93053 1.5625 9.41881 1.5332 9.93639L0.390625 10.2977C0.136719 10.3856 0 10.5712 0 10.8348C0 11.0985 0.136719 11.284 0.390625 11.3622L1.5332 11.7235C1.5625 12.2508 1.66016 12.7489 1.78711 13.2274L0.849609 13.9696C0.654297 14.1161 0.585938 14.3504 0.654297 14.6044C0.742188 14.8387 0.957031 14.9852 1.21094 14.9657L2.40234 14.9071C2.62695 15.3758 2.87109 15.8251 3.1543 16.2254L2.51953 17.2411C2.38281 17.4657 2.40234 17.7098 2.56836 17.8954C2.75391 18.1102 2.97852 18.1688 3.22266 18.0614L4.30664 17.6122C4.66797 17.9735 5.07812 18.2958 5.47852 18.579L5.23438 19.7411C5.18555 20.0047 5.27344 20.2294 5.48828 20.3563C5.73242 20.4833 5.9668 20.454 6.15234 20.2782L7.03125 19.4872C7.5 19.6825 7.96875 19.868 8.45703 20.0047L8.63281 21.1864C8.66211 21.4403 8.83789 21.6258 9.08203 21.6747C9.3457 21.704 9.55078 21.5965 9.66797 21.3719L10.2344 20.3075C10.4883 20.327 10.7324 20.3465 10.9961 20.3465ZM10.9961 18.8719C6.52344 18.8719 3.06641 15.2977 3.06641 10.8446C3.06641 6.3817 6.52344 2.80748 10.9961 2.80748C15.4688 2.80748 18.9258 6.3817 18.9258 10.8446C18.9258 15.2977 15.4688 18.8719 10.9961 18.8719ZM9.28711 9.23326L10.3711 8.54967L7.06055 2.8856L5.9375 3.52037ZM13.2617 11.4598L19.8535 11.4598L19.8438 10.2001L13.2617 10.2001ZM10.3809 13.1493L9.30664 12.4462L5.83984 18.12L6.95312 18.7743ZM10.9766 13.4129C12.3828 13.4129 13.5254 12.2704 13.5254 10.8544C13.5254 9.43834 12.3828 8.29576 10.9766 8.29576C9.56055 8.29576 8.41797 9.43834 8.41797 10.8544C8.41797 12.2704 9.56055 13.4129 10.9766 13.4129ZM10.9766 12.036C10.3027 12.036 9.79492 11.5184 9.79492 10.8544C9.79492 10.1903 10.3027 9.67271 10.9766 9.67271C11.6406 9.67271 12.1484 10.1903 12.1484 10.8544C12.1484 11.5184 11.6406 12.036 10.9766 12.036Z" fill="#ffffff" fill-opacity="0.85"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

+59
View File
@@ -0,0 +1,59 @@
class Address {
final int id;
final String country;
final String postalCode;
final String county;
final String province;
final String city;
final String street;
final String houseNumber;
final double latitude;
final double longitude;
final bool active;
Address({
required this.id,
required this.country,
required this.postalCode,
required this.county,
required this.province,
required this.city,
required this.street,
required this.houseNumber,
required this.latitude,
required this.longitude,
required this.active,
});
Map<String, dynamic> toJson() {
return {
'id': id,
'country': country,
'postalCode': postalCode,
'county': county,
'province': province,
'city': city,
'street': street,
'houseNumber': houseNumber,
'latitude': latitude,
'longitude': longitude,
'active': active,
};
}
factory Address.fromJson(Map<String, dynamic> json) {
return Address(
id: json['id'] as int,
country: json['country'] as String,
postalCode: json['postalCode'] as String,
county: json['county'] as String,
province: json['province'] as String,
city: json['city'] as String,
street: json['street'] as String,
houseNumber: json['houseNumber'] as String,
latitude: (json['latitude'] as num).toDouble(),
longitude: (json['longitude'] as num).toDouble(),
active: json['active'] as bool,
);
}
}
+49
View File
@@ -0,0 +1,49 @@
import 'contact.type.dart';
class Contact {
final int id;
final ContactType contactType;
final String value;
final int priority;
final String notes;
final DateTime creationDate;
final DateTime? updateDate;
final bool active;
Contact({
required this.id,
required this.contactType,
required this.value,
required this.priority,
required this.notes,
required this.creationDate,
this.updateDate,
required this.active,
});
Map<String, dynamic> toJson() {
return {
'id': id,
'contactType': contactType.toJson(),
'value': value,
'priority': priority,
'notes': notes,
'creationDate': creationDate.toIso8601String(),
'updateDate': updateDate?.toIso8601String(),
'active': active,
};
}
factory Contact.fromJson(Map<String, dynamic> json) {
return Contact(
id: json['id'] as int,
contactType: ContactType.fromJson(json['contactType'] as Map<String, dynamic>),
value: json['value'] as String,
priority: json['priority'] as int,
notes: json['notes'] as String,
creationDate: DateTime.parse(json['creationDate'] as String),
updateDate: json['updateDate'] != null ? DateTime.parse(json['updateDate'] as String) : null,
active: json['active'] as bool,
);
}
}
+31
View File
@@ -0,0 +1,31 @@
class ContactType {
final int id;
final String type;
final String description;
final bool active;
ContactType({
required this.id,
required this.type,
required this.description,
required this.active,
});
Map<String, dynamic> toJson() {
return {
'id': id,
'type': type,
'description': description,
'active': active,
};
}
factory ContactType.fromJson(Map<String, dynamic> json) {
return ContactType(
id: json['id'] as int,
type: json['type'] as String,
description: json['description'] as String,
active: json['active'] as bool,
);
}
}
+25
View File
@@ -0,0 +1,25 @@
class Coordinates {
final double latitude;
final double longitude;
Coordinates({
required this.latitude,
required this.longitude,
});
// ---
Map<String, dynamic> toJson() {
return {
'latitude': latitude,
'longitude': longitude,
};
}
factory Coordinates.fromJson(Map<String, dynamic> json) {
return Coordinates(
latitude: (json['latitude'] as num).toDouble(),
longitude: (json['longitude'] as num).toDouble(),
);
}
}
+74
View File
@@ -0,0 +1,74 @@
// Importa le classi annidate
import 'contact.dart';
import 'vineyard.dart';
import 'address.dart';
class Owner {
final int id;
final String? name;
final String? surname;
final String? companyName;
final bool isActive;
final DateTime creationDate;
final String note;
final List<Contact>? contacts;
final List<Vineyard>? vineyards;
final List<Address>? addresses;
// Costruttore con parametri nominati e 'required'
Owner({
required this.id,
this.name,
this.surname,
this.companyName,
required this.isActive,
required this.creationDate,
required this.note,
this.contacts,
this.vineyards,
this.addresses,
});
// ---
/// Converte l'oggetto Owner in una mappa per la serializzazione JSON.
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'surname': surname,
'companyName': companyName,
'isActive': isActive,
'creationDate': creationDate.toIso8601String(),
'note': note,
'contacts': contacts?.map((contact) => contact.toJson()).toList(),
'vineyards': vineyards?.map((vineyard) => vineyard.toJson()).toList(),
'addresses': addresses?.map((address) => address.toJson()).toList(),
};
}
/// Crea un oggetto Owner da una mappa (es. da una risposta JSON).
factory Owner.fromJson(Map<String, dynamic> json) {
try{
return Owner(
id: json['id'] as int,
name: json['name'] as String,
surname: json['surname'] as String,
companyName: json['companyName'] as String,
isActive: json['isActive'] as bool,
creationDate: DateTime.parse(json['creationDate'] as String),
note: json['note'] as String,
// Mappa ogni elemento JSON in un oggetto Contact
contacts: (json['contacts'] as List).map((i) => Contact.fromJson(i as Map<String, dynamic>)).toList(),
// Mappa ogni elemento JSON in un oggetto Vineyard
vineyards: (json['vineyards'] as List).map((i) => Vineyard.fromJson(i as Map<String, dynamic>)).toList(),
// Mappa ogni elemento JSON in un oggetto Address
addresses: (json['addresses'] as List).map((i) => Address.fromJson(i as Map<String, dynamic>)).toList(),
);
}
catch (e) {
print('Errore durante la deserializzazione dell\'oggetto Owner: $e');
return Owner(id: 0, isActive: false, creationDate: DateTime.now(), note: ''); // Restituisce un oggetto Owner vuoto in caso di errore
}
}
}
+35
View File
@@ -0,0 +1,35 @@
class PlantVariety {
final int id;
final String type;
final String quality;
final String name;
final String colour;
PlantVariety({
required this.id,
required this.type,
required this.quality,
required this.name,
required this.colour,
});
Map<String, dynamic> toJson() {
return {
'id': id,
'type': type,
'quality': quality,
'name': name,
'colour': colour,
};
}
factory PlantVariety.fromJson(Map<String, dynamic> json) {
return PlantVariety(
id: json['id'] as int,
type: json['type'] as String,
quality: json['quality'] as String,
name: json['name'] as String,
colour: json['colour'] as String,
);
}
}
+33
View File
@@ -0,0 +1,33 @@
class Role {
// Proprietà della classe
final int id;
final String name;
final String description;
// Costruttore con parametri nominati e 'required'
Role({
required this.id,
required this.name,
required this.description,
});
// ---
/// Converte l'oggetto Role in una mappa per la serializzazione JSON.
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'description': description,
};
}
/// Crea un oggetto Role da una mappa (es. da una risposta JSON).
factory Role.fromJson(Map<String, dynamic> json) {
return Role(
id: json['id'] as int,
name: json['name'] as String,
description: json['description'] as String,
);
}
}
+51
View File
@@ -0,0 +1,51 @@
// Assumiamo che la classe VinePlant sia stata definita in un file 'vine_plant.dart'
import 'vine.plant.dart';
class Row {
final int id;
final int vineyardId;
final int rowNum;
final double length;
final List<VinePlant>? vinePlants;
// Costruttore con parametri nominati.
// 'vinePlants' è opzionale perché è nullable.
Row({
required this.id,
required this.vineyardId,
required this.rowNum,
required this.length,
this.vinePlants,
});
// ---
/// Converte l'oggetto Row in una mappa per la serializzazione JSON.
Map<String, dynamic> toJson() {
return {
'id': id,
'vineyardId': vineyardId,
'rowNum': rowNum,
'length': length,
// Se la lista non è null, la converte in una lista di mappe
'vinePlants': vinePlants?.map((plant) => plant.toJson()).toList(),
};
}
/// Crea un oggetto Row da una mappa (es. da una risposta JSON).
factory Row.fromJson(Map<String, dynamic> json) {
// La lista viene creata solo se esiste e non è null nella mappa JSON
final List<dynamic>? plantsJson = json['vinePlants'];
final List<VinePlant>? plantsList = plantsJson != null
? plantsJson.map((json) => VinePlant.fromJson(json as Map<String, dynamic>)).toList()
: null;
return Row(
id: json['id'] as int,
vineyardId: json['vineyardId'] as int,
rowNum: json['rowNum'] as int,
length: (json['length'] as num).toDouble(),
vinePlants: plantsList,
);
}
}
+61
View File
@@ -0,0 +1,61 @@
// Importa le classi annidate
import 'role.dart';
import 'owner.dart';
class User {
final int? id;
final String? username;
final String? password;
final Role? role;
final bool? active;
final List<Owner>? owners;
// Costruttore con parametri nominati e 'required'
User({
this.id,
this.username,
this.password,
this.role,
this.active,
this.owners,
});
// ---
/// Converte l'oggetto User in una mappa per la serializzazione JSON.
Map<String, dynamic> toJson() {
try{
return {
'id': id,
'username': username,
'password': password,
'role': role?.toJson(), // Chiama il metodo toJson() della classe Role
'active': active,
'owners': owners?.map((owner) => owner.toJson()).toList(), // Converte la lista in una lista di mappe JSON
};
} catch (e) {
print('Errore durante la serializzazione dell\'oggetto User: $e');
return {};
}
}
/// Crea un oggetto User da una mappa (es. da una risposta JSON).
factory User.fromJson(Map<String, dynamic> json) {
final Map<String, dynamic> userJson = json['user'] as Map<String, dynamic>;
try{
return User(
id: userJson['id'] as int,
username: userJson['username'] as String?,
password: userJson['password'] as String?,
role: Role.fromJson(userJson['role'] as Map<String, dynamic>), // Crea un oggetto Role dalla mappa annidata
active: json['active'] as bool?,
owners: (userJson['owners'] as List)
.map((i) => Owner.fromJson(i as Map<String, dynamic>))
.toList(), // Mappa ogni elemento JSON in un oggetto Owner
);
} catch (e) {
print('Errore durante la deserializzazione dell\'oggetto User: $e');
return User(); // Restituisce un oggetto User vuoto in caso di errore
}
}
}
+48
View File
@@ -0,0 +1,48 @@
// Assumiamo che la classe PlantVariety sia stata definita in un file 'plant_variety.dart'
import 'plant.variety.dart';
class VinePlant {
// Proprietà della classe
final int id;
final int rowId;
final int plantNumberInRow;
final int varietyId;
final PlantVariety variety;
final DateTime plantingDate;
// Costruttore con parametri nominati
VinePlant({
required this.id,
required this.rowId,
required this.plantNumberInRow,
required this.varietyId,
required this.variety,
required this.plantingDate,
});
// ---
/// Converte l'oggetto VinePlant in una mappa per la serializzazione JSON.
Map<String, dynamic> toJson() {
return {
'id': id,
'rowId': rowId,
'plantNumberInRow': plantNumberInRow,
'varietyId': varietyId,
'variety': variety.toJson(),
'plantingDate': plantingDate.toIso8601String(),
};
}
/// Crea un oggetto VinePlant da una mappa (es. da una risposta JSON).
factory VinePlant.fromJson(Map<String, dynamic> json) {
return VinePlant(
id: json['id'] as int,
rowId: json['rowId'] as int,
plantNumberInRow: json['plantNumberInRow'] as int,
varietyId: json['varietyId'] as int,
variety: PlantVariety.fromJson(json['variety'] as Map<String, dynamic>),
plantingDate: DateTime.parse(json['plantingDate'] as String),
);
}
}
+87
View File
@@ -0,0 +1,87 @@
// Assumiamo che le classi Coordinates e Row siano definite
// in file separati
import 'coordinates.dart';
import 'row.dart';
class Vineyard {
final int id;
final String name;
final String location;
final double hectares;
final Coordinates? coordinates;
final DateTime creationDate;
final List<Row>? rows;
final Map<int, Coordinates>? area;
// Costruttore con parametri nominati.
// 'rows' è opzionale perché è nullable.
Vineyard({
required this.id,
required this.name,
required this.location,
required this.hectares,
this.coordinates,
required this.creationDate,
this.rows,
this.area,
});
// ---
/// Converte l'oggetto Vineyard in una mappa per la serializzazione JSON.
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'location': location,
'hectares': hectares,
'coordinates': coordinates?.toJson(),
'creationDate': creationDate.toIso8601String(),
// Se 'rows' non è null, lo converte in una lista di mappe JSON
'rows': rows?.map((row) => row.toJson()).toList(),
// Converte la mappa 'area'
'area': area?.map((key, value) => MapEntry(key.toString(), value.toJson())),
};
}
/// Crea un oggetto Vineyard da una mappa (es. da una risposta JSON).
factory Vineyard.fromJson(Map<String, dynamic> json) {
try{
// Gestione della lista di 'rows'
final List<dynamic>? rowsJson = json['rows'];
final List<Row>? rowsList = rowsJson?.map((json) => Row.fromJson(json as Map<String, dynamic>)).toList();
// Gestione della mappa 'area'
final Map<dynamic, dynamic> areaJson = json['area'] as Map<dynamic, dynamic>;
final Map<int, Coordinates> areaMap = areaJson.map(
(key, value) => MapEntry(
int.parse(key as String), // Converte la chiave da String a int
Coordinates.fromJson(value as Map<String, dynamic>),
),
);
return Vineyard(
id: json['id'] as int,
name: json['name'] as String,
location: json['location'] as String,
hectares: (json['hectares'] as num).toDouble(),
coordinates: null,//Coordinates.fromJson(json['coordinates'] as Map<String, dynamic>),
creationDate: DateTime.parse(json['creationDate'] as String),
rows: rowsList,
area: areaMap,
);
} catch (e) {
print('Errore durante la deserializzazione dell\'oggetto Vineyard: $e');
return Vineyard(
id: 0,
name: '',
location: '',
hectares: 0.0,
coordinates: null,
creationDate: DateTime.now(),
rows: [],
area: {},
); // Restituisce un oggetto Vineyard vuoto in caso di errore
}
}
}
+52
View File
@@ -0,0 +1,52 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:get/get_navigation/src/root/get_material_app.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:u_vineyard/template/src/classes/constents.class.dart';
import 'package:u_vineyard/template/src/classes/functions.class.dart';
import 'package:u_vineyard/template/src/classes/routes.class.dart';
import 'package:u_vineyard/template/src/platform/platform_functions.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
PlatformFunctions.init();
await initializeProviders();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return GetMaterialApp(
title: 'Dashboard',
debugShowCheckedModeBanner: false,
getPages: appRoutes,
initialRoute: appRoutes.first.name,
scrollBehavior: const MaterialScrollBehavior().copyWith(
dragDevices: {
PointerDeviceKind.mouse,
PointerDeviceKind.touch,
PointerDeviceKind.stylus,
PointerDeviceKind.unknown
},
),
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: primaryColor).copyWith(
surfaceTint: Colors.transparent,
),
textSelectionTheme: TextSelectionThemeData(
cursorColor: primaryColor,
selectionColor: primaryColor.withOpacity(.3),
),
useMaterial3: true,
textTheme: GoogleFonts.poppinsTextTheme(),
),
);
}
}
+287
View File
@@ -0,0 +1,287 @@
// import '../../../widgets/responsive_widgets/responsive.widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:u_vineyard/services/api.service.dart';
import 'package:u_vineyard/template/src/classes/enums.class.dart';
import 'package:u_vineyard/template/src/classes/functions.class.dart';
import 'package:u_vineyard/template/src/providers/alert.provider.dart';
import 'package:u_vineyard/template/src/providers/auth.provider.dart';
import 'package:u_vineyard/template/src/widgets/responsive_widgets/responsive.widget.dart';
import 'package:u_vineyard/template/src/widgets/simple_loader.widget.dart';
// import '../../../widgets/simple_loader.widget.dart';
class LoginPage extends StatefulWidget {
static const route = '/login';
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final TextEditingController emailController = TextEditingController();
final TextEditingController passwordController = TextEditingController();
bool isVisible = false;
bool isLoading = false;
String? error;
@override
void dispose() {
emailController.dispose();
passwordController.dispose();
super.dispose();
}
update(VoidCallback func) {
if (mounted) setState(func);
}
bool validate() {
final email = emailController.text;
final pass = passwordController.text;
if(email != "admin"){
if (email.isEmpty) {
return !setError('The email address is required');
}
if (!email.isEmail) {
return !setError('The email address is inviled');
}
if (pass.isEmpty) {
return !setError('The password is required');
}
}
return true;
}
login() async {
setError('');
update(() => isLoading = true);
final state = validate();
if (state) {
TextInput.finishAutofillContext();
await Future.delayed(const Duration(seconds: 1), () async {
ApiService.login(
emailController.text,
passwordController.text,
).then((value) {
if (value == '200') {
Get.find<AuthProvider>().toggleAuth(true);
Get.offAllNamed('/');
} else {
setError('Login failed: $value');
}
}).catchError((e) {
setError('Login failed: $e');
});
Get.offAllNamed('/');
});
initializeProviders();
}
update(() => isLoading = false);
}
togglePass() {
if (mounted) setState(() => isVisible = !isVisible);
}
bool setError(String error) {
final alerter = Get.find<AlertProvider>();
alerter.setAlert(
error,
type: AlertType.danger,
dismissable: false,
);
if (mounted) {
setState(() {});
}
if (error.isEmpty) return false;
return true;
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: ResponsiveWidget(
builder: (cnx, screen) => Container(
width: double.infinity,
height: double.infinity,
decoration: !screen.isMobile
? BoxDecoration(
image: DecorationImage(
image: AssetImage(imgUrl('login.jpg')),
fit: BoxFit.cover,
),
)
: null,
child: Center(
child: SingleChildScrollView(
physics:
screen.isBig ? const NeverScrollableScrollPhysics() : null,
child: Container(
width: !screen.isMobile ? 550 : null,
decoration: BoxDecoration(
color: !screen.isMobile ? Colors.white : null,
borderRadius: const BorderRadius.all(Radius.circular(20)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 50),
Text(
'Welcome Back',
style:
Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.w600,
color: const Color(0xff0f0f0f),
),
),
const SizedBox(height: 50),
GetBuilder<AlertProvider>(
init: AlertProvider(),
builder: (alerter) {
return Padding(
padding: const EdgeInsets.only(
bottom: 20,
left: 20,
right: 20,
),
child: alerter.alert ?? const SizedBox(),
);
},
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 30),
child: AutofillGroup(
child: Column(
children: [
TextField(
controller: emailController,
autofocus: true,
autofillHints: const [AutofillHints.email],
decoration: InputDecoration(
labelText: 'Email',
icon: SizedBox(
width: 20,
child: svg(
'profile',
color: Colors.grey.shade800,
),
),
filled: true,
fillColor: const Color(0xFFf0f1f5),
border: const OutlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFf0f1f5),
),
),
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFf0f1f5),
),
),
),
onChanged: (value) => setError(''),
onEditingComplete:
TextInput.finishAutofillContext,
textInputAction: TextInputAction.next,
keyboardType: TextInputType.emailAddress,
onSubmitted: (_) => login(),
),
const SizedBox(height: 20),
TextField(
autofillHints: const [AutofillHints.password],
controller: passwordController,
autofocus: false,
obscureText: !isVisible,
decoration: InputDecoration(
labelText: 'Password',
icon: SizedBox(
width: 20,
child: svg(
'password',
color: Colors.grey.shade800,
),
),
suffixIcon: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10),
child: IconButton(
onPressed: togglePass,
icon: isVisible
? svg('eye.slash.fill',
size: 16,
color: Colors.grey.shade800)
: svg('eye.fill',
size: 15,
color: Colors.grey.shade800),
),
),
filled: true,
fillColor: const Color(0xFFf0f1f5),
border: const OutlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFf0f1f5),
),
),
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFf0f1f5),
),
),
),
onChanged: (value) => setError(''),
onEditingComplete:
TextInput.finishAutofillContext,
textInputAction: TextInputAction.go,
keyboardType: TextInputType.visiblePassword,
onSubmitted: (_) => login(),
),
const SizedBox(height: 50),
SizedBox(
width: 300,
height: 50,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(5),
),
),
),
onPressed: isLoading ? null : login,
child: AnimatedCrossFade(
crossFadeState: !isLoading
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
duration: const Duration(milliseconds: 300),
alignment: Alignment.center,
firstChild: const Text('Login'),
secondChild: const Center(
child: SimpleLoader(
strokeWidth: 3,
size: 20,
),
),
),
),
)
],
),
),
),
const SizedBox(height: 50, width: 200),
],
),
),
),
),
),
),
);
}
}
+250
View File
@@ -0,0 +1,250 @@
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import 'package:u_vineyard/services/polygon.area.calculator.service.dart';
import 'package:u_vineyard/template/src/widgets/card.widget.dart';
import 'package:u_vineyard/template/src/widgets/responsive_widgets/repesponsive_grid_item.widget.dart';
import 'package:u_vineyard/template/src/widgets/responsive_widgets/responsive_grid.widget.dart';
import 'package:u_vineyard/widgets/map.widget.dart';
class NewVineyardScreen extends StatefulWidget {
const NewVineyardScreen({super.key});
@override
State<NewVineyardScreen> createState() => _NewVineyardScreenState(); // Correzione: nome della classe State
}
class _NewVineyardScreenState extends State<NewVineyardScreen> { // Correzione: nome della classe State
// Aggiungi un TextEditingController per il TextFormField
final TextEditingController _nomeVignaController = TextEditingController();
final TextEditingController _superficieController = TextEditingController();
List<LatLng> _polygonPoints = [];
double _calculatedArea = 0.0; // Area calcolata, inizialmente impostata a 0
final TextEditingController _localitaController = TextEditingController();
@override
void dispose() {
_nomeVignaController.dispose(); // Libera il controller quando il widget viene eliminato
_superficieController.dispose(); // Libera il controller quando il widget viene eliminato
_localitaController.dispose(); // Libera il controller quando il widget viene eliminato
super.dispose();
}
void _handleMapTap(TapPosition tapPosition, LatLng latLng) {
setState(() {
_polygonPoints.add(latLng);
_calculatedArea = PolygonAreaCalculator.calculateAreaInHectares(_polygonPoints);
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Punto aggiunto: ${latLng.latitude.toStringAsFixed(4)}, ${latLng.longitude.toStringAsFixed(4)}'),
duration: const Duration(milliseconds: 800),
),
);
}
void _clearPolygon() {
setState(() {
_polygonPoints.clear();
_calculatedArea = 0.0;
_superficieController.clear();
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Poligono e superficie resettati.'),
duration: Duration(milliseconds: 800),
),
);
}
void _showAreaConfirmationDialog() {
if (_calculatedArea > 0) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Conferma Superficie'),
content: Text(
'L\'area calcolata del poligono è di ${_calculatedArea.toStringAsFixed(2)} ettari.\n\nVuoi utilizzare questo valore o inserirlo manualmente?'),
actions: <Widget>[
TextButton(
child: const Text('Inserisci Manualmente'),
onPressed: () {
Navigator.of(context).pop();
_superficieController.clear();
},
),
FilledButton(
child: const Text('Usa Valore Calcolato'),
onPressed: () {
setState(() {
_superficieController.text = _calculatedArea.toStringAsFixed(2);
});
Navigator.of(context).pop();
},
),
],
);
},
);
}
}
void _submitForm() {
//if (_formKey.currentState!.validate()) {
if (true) {
if (_polygonPoints.length < 3) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Disegna almeno 3 punti per il poligono della vigna.')),
);
return;
}
print('--- Dati Vigna ---');
print('Nome Vigna: ${_nomeVignaController.text}');
print('Località: ${_localitaController.text}');
print('Superficie (ha): ${_superficieController.text}');
print('Coordinate Poligono:');
_polygonPoints.forEach((point) {
print(' Lat: ${point.latitude}, Lng: ${point.longitude}');
});
print('-----------------');
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Dati inviati (simulato)! Controlla la console.')),
);
}
}
@override
Widget build(BuildContext context) {
return ResponsiveGrid(
childrenBuilder: (context, screen) {
return [
ResponsiveGridItem(
crossAxisCellCount: screen.setAxis(12, 12, 12, 12),
child: Container(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CardWidget(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Aggiungi Nuova Vigna',
style: Theme.of(context).textTheme.titleLarge,
),
),
),
const SizedBox(height: 16.0), // Spazio dopo il CardWidget
// *** INIZIO DELLA CORREZIONE ***
Row(
children: [
Expanded( // <--- AVVOLGI IL TextFormField IN UN Expanded
child: TextFormField(
controller: _nomeVignaController, // Collega il controller
decoration: const InputDecoration(
labelText: 'Nome Vigna',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.grass),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Inserisci il nome della vigna';
}
return null;
},
),
),
],
),
const SizedBox(height: 12),
TextFormField(
controller: _localitaController,
decoration: const InputDecoration(
labelText: 'Località',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.location_on),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Inserisci la località';
}
return null;
},
),
const SizedBox(height: 12),
TextFormField(
//controller: superficieController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: 'Superficie (ettari)',
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.area_chart),
suffixIcon: _calculatedArea > 0
? IconButton(
icon: const Icon(Icons.auto_mode, color: Colors.green),
onPressed: _showAreaConfirmationDialog, // Mostra il dialogo di conferma
tooltip: 'Usa area calcolata',
)
: null,
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Inserisci la superficie';
}
if (double.tryParse(value) == null) {
return 'Inserisci un numero valido';
}
return null;
},
),
// *** FINE DELLA CORREZIONE ***
// Add your form or content here
Row(
children: [
Expanded(
child: VignaMapWidget(
polygonPoints: _polygonPoints,
onMapTap: _handleMapTap,
onClearPolygon: _clearPolygon,
calculatedArea: _calculatedArea,
),
),
],
),
Row(
children: [
Expanded(
child: SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: _submitForm,
icon: const Icon(Icons.send),
label: const Text(
'Invia Dati Vigna',
style: TextStyle(fontSize: 18),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green[800],
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
),
),
],
),
],
),
),
),
];
},
);
}
}
+71
View File
@@ -0,0 +1,71 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'auth.service.dart';
class ApiService {
static const String _baseUrl = 'http://192.168.0.9:5000'; // cambia qui
// Token memorizzato in memoria (per sessione)
static String? _token;
static String? get token => _token;
static String get baseUrl => _baseUrl;
// Login con Basic Auth e salvataggio token
static Future<String> login(String username, String password) async {
final url = Uri.parse('$_baseUrl/User/login');
// Header con Basic Auth
String basicAuth =
'Basic ${base64Encode(utf8.encode('$username:$password'))}';
final response = await http.get(
url,
headers: {
'Authorization': basicAuth,
'Accept': 'application/json',
},
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
_token = data['access_token']; // salva il token in memoria
print('Token ricevuto: $_token'); // stampa in console
AuthService.instance.saveToken(_token!);
return '${response.statusCode}';
} else {
throw Exception(
'Login fallito: ${response.statusCode} - ${response.body}');
}
}
static Future<dynamic> checkLogin() async {
final token = AuthService.instance.token;
print('Token corrente: $token'); // stampa in console
if (token == null) {
print('Token non disponibile. Effettua il login prima.'); // stampa in console
throw Exception('Token non disponibile. Effettua il login prima.');
}
final url = Uri.parse('$_baseUrl/Test/cklogin');
final response = await http.get(
url,
headers: {
'Authorization': 'Bearer $token',
'Accept': 'application/json',
},
);
if (response.statusCode == 200) {
final object = jsonDecode(response.body);
print('Risposta cklogin: $object'); // stampa in console
return object;
} else {
throw Exception(
'Errore checkLogin: ${response.statusCode} - ${response.body}');
}
}
}
+42
View File
@@ -0,0 +1,42 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:u_vineyard/services/auth.service.dart';
import '../entities/user.dart';
import 'api.service.dart';
class UserService {
static final String _baseUrl = ApiService.baseUrl;
Future<User?> userProfile(int userId) async {
String token = AuthService.instance.token ?? '';
if (token.isEmpty) {
print('Token non disponibile. Effettua il login prima.');
return null; // Gestisci il caso in cui il token non sia disponibile
}
try {
final url = Uri.parse('$_baseUrl/Test/cklogin');
final response = await http.get(
url,
headers: {
'Authorization': 'Bearer $token',
'Accept': 'application/json',
},
);
if (response.statusCode == 200) {
// Decodifica la risposta JSON
final Map<String, dynamic> jsonMap = json.decode(response.body);
// Crea e restituisce un oggetto User usando il costruttore fromJson
return User.fromJson(jsonMap);
} else {
// Gestisci le risposte con stato diverso da 200 (es. 404, 500)
print('Errore: La richiesta non è andata a buon fine. Status code: ${response.statusCode}');
return null;
}
} catch (e) {
// Gestisci eventuali eccezioni (es. nessuna connessione, URL errato)
print('Errore durante il recupero dei dati: $e');
return null;
}
}
}
+125
View File
@@ -0,0 +1,125 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:local_auth/local_auth.dart';
import 'package:shared_preferences/shared_preferences.dart';
class AuthService {
static final AuthService _instance = AuthService._internal();
static AuthService get instance => _instance;
AuthService._internal();
final _secureStorage = const FlutterSecureStorage(); // Per piattaforme mobili
// Non inizializzare SharedPreferences qui direttamente, perché è async
String? _token;
static const String _tokenKey = 'AuthToken'; // Chiave comune per storage
String? get token => _token;
// Questa funzione ora serve solo per la logica specifica mobile (es. biometrica)
// Non usarla per decidere dove salvare il token, usa kIsWeb direttamente
Future<bool> _isMobileOrDesktopPlatform() async {
if (kIsWeb) return false;
// Android, iOS, Windows, Linux, macOS
//final deviceInfo = DeviceInfoPlugin();
if (defaultTargetPlatform == TargetPlatform.android ||
defaultTargetPlatform == TargetPlatform.iOS ||
defaultTargetPlatform == TargetPlatform.windows ||
defaultTargetPlatform == TargetPlatform.linux ||
defaultTargetPlatform == TargetPlatform.macOS) {
return true;
}
return false;
}
// Nuova funzione per salvare il token
Future<void> saveToken(String token) async {
_token = token; // Aggiorna il token in memoria
print('AuthToken: Tentativo di salvataggio token: $token');
if (kIsWeb) {
// Per Web: usa SharedPreferences (che su web usa localStorage)
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_tokenKey, token);
print('AuthToken: Token salvato in SharedPreferences (Web).');
} else if (await _isMobileOrDesktopPlatform()) {
// Per Mobile/Desktop: usa FlutterSecureStorage
await _secureStorage.write(key: _tokenKey, value: token);
print('AuthToken: Token salvato in FlutterSecureStorage (Mobile/Desktop).');
}
}
// Nuova funzione per caricare il token (senza biometria)
Future<String?> _loadTokenFromStorage() async {
if (kIsWeb) {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(_tokenKey);
} else if (await _isMobileOrDesktopPlatform()) {
return _secureStorage.read(key: _tokenKey);
}
return null; // Caso non supportato
}
Future<bool> loadTokenWithBiometric() async {
// Prima, prova sempre a caricare il token dallo storage generale
// Questo è il token che verra' utilizzato se la biometria fallisce o non è disponibile.
_token = await _loadTokenFromStorage();
if (_token != null && !await _isMobileOrDesktopPlatform()) {
// Se siamo su web e il token è stato caricato, l'autenticazione è implicita (no biometria)
print('AuthToken: Token caricato da storage (Web), biometria non applicabile.');
return true;
}
// Se non è una piattaforma mobile/desktop supportata per la biometria, o se il token non c'è,
// o se la biometria non è disponibile/non supportata, gestisci di conseguenza.
if (!await _isMobileOrDesktopPlatform()) {
print('AuthToken: Biometria non disponibile su questa piattaforma.');
return false;
}
try {
final auth = LocalAuthentication();
final canAuth = await auth.canCheckBiometrics || await auth.isDeviceSupported();
if (!canAuth) {
print('AuthToken: Dispositivo non supporta biometria.');
// Se la biometria non è supportata, ma un token esiste, consideralo loggato
return _token != null;
}
final authenticated = await auth.authenticate(
localizedReason: 'Autenticati per accedere',
options: const AuthenticationOptions(
biometricOnly: true,
stickyAuth: true,
),
);
if (authenticated) {
final stored = await _secureStorage.read(key: _tokenKey); // Rileggi da secure storage
_token = stored; // Aggiorna il token in memoria con quello autenticato biometricamente
print('AuthToken: Biometria riuscita, token da storage sicuro: $_token');
return stored != null;
} else {
print('AuthToken: Autenticazione biometrica fallita.');
_token = null; // Se la biometria fallisce, resetta il token in memoria
}
} catch (e) {
debugPrint('AuthToken: Errore autenticazione biometrica: $e');
_token = null; // In caso di errore, resetta il token
}
return false;
}
Future<void> clear() async {
_token = null;
print('AuthToken: Tentativo di cancellazione token.');
if (kIsWeb) {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_tokenKey);
print('AuthToken: Token cancellato da SharedPreferences (Web).');
} else if (await _isMobileOrDesktopPlatform()) {
await _secureStorage.delete(key: _tokenKey);
print('AuthToken: Token cancellato da FlutterSecureStorage (Mobile/Desktop).');
}
}
}
@@ -0,0 +1,40 @@
// utils/polygon_area_calculator.dart
import 'package:latlong2/latlong.dart';
import 'dart:math';
class PolygonAreaCalculatorService {
// Costante per il raggio medio della Terra in metri (WGS84 ellipsoid semi-major axis)
static const double _earthRadius = 6378137.0;
/// Calcola l'area di un poligono su una sfera (Terra) in ettari.
/// Implementa una versione della formula di Shoelace per coordinate geodetiche.
/// Restituisce 0.0 se i punti sono meno di 3.
static double calculateAreaInHectares(List<LatLng> points) {
if (points.length < 3) return 0.0;
double area = 0.0;
// La formula di Shoelace richiede che il poligono sia chiuso (primo punto = ultimo punto)
List<LatLng> closedPoints = List.from(points);
if (closedPoints.first != closedPoints.last) {
closedPoints.add(closedPoints.first);
}
for (int i = 0; i < closedPoints.length - 1; i++) {
LatLng p1 = closedPoints[i];
LatLng p2 = closedPoints[i + 1];
// Conversione delle latitudini e longitudini da gradi a radianti
double lat1Rad = p1.latitude * pi / 180.0;
double lon1Rad = p1.longitude * pi / 180.0;
double lat2Rad = p2.latitude * pi / 180.0;
double lon2Rad = p2.longitude * pi / 180.0;
// Formula per l'area di un segmento sferico
area += (lon2Rad - lon1Rad) * (2 + sin(lat1Rad) + sin(lat2Rad));
}
area = (area * _earthRadius * _earthRadius / 2.0).abs();
// Converti da metri quadrati a ettari (1 ettaro = 10000 m^2)
return area / 10000.0;
}
}
@@ -0,0 +1,40 @@
// utils/polygon_area_calculator.dart
import 'package:latlong2/latlong.dart';
import 'dart:math';
class PolygonAreaCalculator {
// Costante per il raggio medio della Terra in metri (WGS84 ellipsoid semi-major axis)
static const double _earthRadius = 6378137.0;
/// Calcola l'area di un poligono su una sfera (Terra) in ettari.
/// Implementa una versione della formula di Shoelace per coordinate geodetiche.
/// Restituisce 0.0 se i punti sono meno di 3.
static double calculateAreaInHectares(List<LatLng> points) {
if (points.length < 3) return 0.0;
double area = 0.0;
// La formula di Shoelace richiede che il poligono sia chiuso (primo punto = ultimo punto)
List<LatLng> closedPoints = List.from(points);
if (closedPoints.first != closedPoints.last) {
closedPoints.add(closedPoints.first);
}
for (int i = 0; i < closedPoints.length - 1; i++) {
LatLng p1 = closedPoints[i];
LatLng p2 = closedPoints[i + 1];
// Conversione delle latitudini e longitudini da gradi a radianti
double lat1Rad = p1.latitude * pi / 180.0;
double lon1Rad = p1.longitude * pi / 180.0;
double lat2Rad = p2.latitude * pi / 180.0;
double lon2Rad = p2.longitude * pi / 180.0;
// Formula per l'area di un segmento sferico
area += (lon2Rad - lon1Rad) * (2 + sin(lat1Rad) + sin(lat2Rad));
}
area = (area * _earthRadius * _earthRadius / 2.0).abs();
// Converti da metri quadrati a ettari (1 ettaro = 10000 m^2)
return area / 10000.0;
}
}
+90
View File
@@ -0,0 +1,90 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'src/models/dashboard_models/dashboard_card.model.dart';
final demoUsersList = List.generate(
20,
(i) => UsersModel.fromMap({
'id': i.toString(),
'avatar':
'https://cdn.dribbble.com/users/1500210/avatars/normal/ebcc769d2f155e72aa024bdafadc28cc.jpg?1629718724&resize=80x80',
'name': 'user $i',
'dateOfBirth': DateTime.now().toLocal(),
'gender': i.isEven ? "male" : "female",
'email': 'user$i@codestein.dev',
'phone': (Random.secure().nextInt(9999999) + 1999999).toString(),
'status': i.isEven,
}),
);
class UsersModel {
final String id;
final String avatar;
final String name;
final DateTime dateOfBirth;
final String gender;
final String email;
final String phone;
bool status;
UsersModel({
required this.id,
required this.avatar,
required this.name,
required this.dateOfBirth,
required this.gender,
required this.email,
required this.phone,
this.status = false,
});
factory UsersModel.fromMap(Map<String, dynamic> map) {
return UsersModel(
id: map['id'],
avatar: map['avatar'],
name: map['name'],
dateOfBirth: map['dateOfBirth'],
gender: map['gender'],
email: map['email'],
phone: map['phone'],
status: map['status'] ?? false,
);
}
}
// final dashboardCardModelValues = [
// DashboardCardModel.fromJson({
// 'name': 'Projects',
// 'icon': Icons.wine_bar,
// 'value': 327,
// 'persentage': 32.1,
// 'persentageValue': '2.1',
// 'notes': '8k social visitors',
// }),
// DashboardCardModel.fromJson({
// 'name': 'Stock Qty',
// 'icon': Icons.add_chart_rounded,
// 'value': 275,
// 'persentage': 64.1,
// 'persentageValue': '3.1',
// 'notes': 'Total 424,567 deliveries',
// }),
// DashboardCardModel.fromJson({
// 'name': 'C APEX',
// 'icon': Icons.apple_rounded,
// 'value': 648,
// 'persentage': 72.1,
// 'persentageValue': '12.1',
// 'notes': 'Counted in Millions',
// }),
// DashboardCardModel.fromJson({
// 'name': 'Saving',
// 'icon': Icons.data_saver_off_rounded,
// 'value': 18478,
// 'persentage': 24.1,
// 'persentageValue': '0.1',
// 'notes': 'Reports by states and ganders',
// }),
// ];
+50
View File
@@ -0,0 +1,50 @@
import 'src/classes/constents.class.dart';
import 'src/platform/platform_functions.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:google_fonts/google_fonts.dart';
import 'src/classes/functions.class.dart';
import 'src/classes/routes.class.dart';
// void main() async {
// WidgetsFlutterBinding.ensureInitialized();
// PlatformFunctions.init();
// await initializeProviders();
// runApp(const MyApp());
// }
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return GetMaterialApp(
title: 'Dashboard',
debugShowCheckedModeBanner: false,
getPages: appRoutes,
initialRoute: appRoutes.first.name,
scrollBehavior: const MaterialScrollBehavior().copyWith(
dragDevices: {
PointerDeviceKind.mouse,
PointerDeviceKind.touch,
PointerDeviceKind.stylus,
PointerDeviceKind.unknown
},
),
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: primaryColor).copyWith(
surfaceTint: Colors.transparent,
),
textSelectionTheme: TextSelectionThemeData(
cursorColor: primaryColor,
selectionColor: primaryColor.withOpacity(.3),
),
useMaterial3: true,
textTheme: GoogleFonts.poppinsTextTheme(),
),
);
}
}
@@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
const Color primaryColor = Colors.blueAccent;
const Color navbarColor = Colors.white;
const Color drawerColor = Colors.white;
const bool toastAutoDissmiss = true;
const Duration toastDuration = Duration(milliseconds: 200);
const Duration toastDissmissDuration = Duration(seconds: 4);
// this is the maximum width the main container can take as width => in web called (fixed, fluid)
// it can be double.infinity or fixed double like 1600.0
const double maxContainerWidth = 1600;
// if true a BreadCampWidget will be showen at the top of the main container
// you can add or edit this widget as you like, you will find it in the /src/widgets/app_widgets.
const bool showBreadCamp = false;
const List<BoxShadow> cardShadow = [
BoxShadow(
color: Color(0x1F6E9AE6),
offset: Offset(0, 12),
blurRadius: 20,
spreadRadius: .05,
),
];
@@ -0,0 +1,9 @@
enum AlertType { primary, success, danger, warning, info }
enum ToastType { primary, success, danger, warning, info }
enum ScreenSizeType { desktop, tablet, mobile }
enum DissmissType { auto, click, swipe }
enum DrawerItemType { link, menu }
@@ -0,0 +1,165 @@
import 'dart:math';
import 'constents.class.dart';
import 'enums.class.dart';
import 'routes.class.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:get/get.dart';
import '../providers/alert.provider.dart';
import '../providers/auth.provider.dart';
import '../providers/routes.provider.dart';
import '../providers/search.provider.dart';
import '../providers/toast.provider.dart';
String imgUrl(String url) {
if (kIsWeb) return 'images/$url';
return 'images/$url';
}
Widget svg(String name, {double size = 20, Color? color}) {
try {
final String assetName = 'images/svgs/$name.svg';
return SvgPicture.asset(
assetName,
width: size,
height: size,
colorFilter: ColorFilter.mode(color ?? primaryColor, BlendMode.srcIn),
);
} catch (e) {
//
}
return const SizedBox();
}
alert(
String message, {
AlertType type = AlertType.primary,
Duration autoDismissAfter = Duration.zero,
}) {
Get.find<AlertProvider>().setAlert(
message,
type: type,
autoDismissAfter: autoDismissAfter,
);
}
toast(
String? message, {
ToastType type = ToastType.primary,
VoidCallback? callback,
}) {
Get.find<ToastProvider>().addToast(message, type: type, callback: callback);
}
void showModel(String type) {
final r = Random.secure().nextInt(5);
switch (r) {
case 0:
alert(AlertType.primary.toString(), type: AlertType.primary);
break;
case 1:
alert(AlertType.success.toString(), type: AlertType.success);
break;
case 2:
alert(AlertType.warning.toString(), type: AlertType.warning);
break;
case 3:
alert(AlertType.danger.toString(), type: AlertType.danger);
break;
case 4:
alert(AlertType.info.toString(), type: AlertType.info);
break;
default:
alert(AlertType.primary.toString(), type: AlertType.primary);
}
// Get.defaultDialog(content: SelectableText(type));
}
Future<void> initializeProviders() async {
Get.put(AuthProvider(), permanent: true);
Get.put(AlertProvider(), permanent: true);
Get.put(RoutesProvider());
Get.put(SearchProvider());
Get.put(ToastProvider());
}
Future<void> removeProviders() async {
await Get.delete<RoutesProvider>(force: true);
await Get.delete<SearchProvider>(force: true);
await Get.delete<ToastProvider>(force: true);
}
closeDrawerMenu(String item) {
for (var child in drawerMenu) {
if (child.type == DrawerItemType.menu) {
child.isOpen = false;
for (var subChild in child.children) {
if (subChild.route == item) {
child.isOpen = true;
} else if (subChild.type == DrawerItemType.menu) {
subChild.isOpen = false;
for (var subsubChild in subChild.children) {
if (subsubChild.route == item) {
child.isOpen = true;
subChild.isOpen = true;
}
}
}
}
}
}
}
int menuIndex(String item) {
for (var child in drawerMenu) {
if (child.type == DrawerItemType.menu) {
for (var subChild in child.children) {
if (subChild.type == DrawerItemType.menu) {
for (var subsubChild in subChild.children) {
if (subsubChild.route == item) {
return drawerMenu.indexOf(child);
}
}
} else {
if (subChild.route == item) {
return drawerMenu.indexOf(child);
}
}
}
} else {
if (child.route == item) {
return drawerMenu.indexOf(child);
}
}
}
return 0;
}
void focusSearch() {
Get.find<SearchProvider>().focus();
}
String formatDateTime(DateTime value) {
int hours = value.hour;
String letters = 'AM';
if (hours > 12) {
hours -= 12;
letters = "PM";
}
String convertedDateTime = value.year.toString();
convertedDateTime += "-${value.month.toString().padLeft(2, '0')}";
convertedDateTime += "-${value.day.toString().padLeft(2, '0')}";
convertedDateTime += " ${hours.toString().padLeft(2, '0')}";
convertedDateTime += "-${value.minute.toString().padLeft(2, '0')} $letters";
return convertedDateTime;
}
String formatDate(DateTime value) {
String convertedDateTime = value.year.toString();
convertedDateTime += "-${value.month.toString().padLeft(2, '0')}";
convertedDateTime += "-${value.day.toString().padLeft(2, '0')}";
return convertedDateTime;
}
+157
View File
@@ -0,0 +1,157 @@
import 'package:u_vineyard/pages/login.page.dart';
import 'package:u_vineyard/pages/new.vineyard.page.dart';
import '../models/app_models/drawer_item.model.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../middlewares/auth.middleware.dart';
import '../screens/dashboard/dashboard.screen.dart';
import '../screens/main/main.screen.dart';
List<GetPage<dynamic>> appRoutes = [
GetPage(
title: 'Dashboard',
name: MainScreen.route,
page: () => const MainScreen(),
middlewares: [AuthMiddleware()],
),
GetPage(
title: 'Login',
name: LoginPage.route,
page: () => const LoginPage(),
middlewares: [GuestMiddleware()],
)
];
Map<String, Widget Function()> internalRoutes = {
'/dashboard': () => const DashboadScreen(),
'/vineyards/new-vineyard': () => const NewVineyardScreen(), //const SizedBox(child: Text('1')),
'/vineyards/list': () => const SizedBox(child: Text('2')),
'/vineyards/edit': () => const SizedBox(child: Text('3')),
'/settings/account': () => const SizedBox(child: Text('5')),
'/menu2/1': () => const SizedBox(child: Text('menu 2-1')),
'/menu2/2': () => const SizedBox(child: Text('menu 2-2')),
'/menu2/3': () => const SizedBox(child: Text('menu 2-3')),
'/menu2/4': () => const SizedBox(child: Text('menu 2-4')),
'/menu2/menu3/1': () => const SizedBox(child: Text('menu 2-3-1')),
'/menu2/menu3/2': () => const SizedBox(child: Text('menu 2-3-2')),
'/menu2/menu3/3': () => const SizedBox(child: Text('menu 2-3-3')),
'/menu2/menu3/4': () => const SizedBox(child: Text('menu 2-3-4')),
};
final List<Map<String, dynamic>> drawerMenuMap = [
{
'type': 'link',
'name': 'Dashboard',
'icon': 'dashboard',
'route': '/dashboard',
},
{
'type': 'menu',
'name': 'Vigne',
'icon': 'menu',
'route': '/vineyards',
'children': [
{
'type': 'link',
'name': 'Aggiungi Vigna',
'icon': 'dot',
'route': '/new-vineyard',
},
{
'type': 'link',
'name': 'Lista Vigne',
'icon': 'dot',
'route': '/list',
},
{
'type': 'link',
'name': 'Modifica Vigna',
'icon': 'dot',
'route': '/edit',
},
],
},
{
'type': 'menu',
'name': 'Menu2',
'icon': 'menu',
'route': '/menu2',
'children': [
{
'type': 'link',
'name': 'Menu2 Item 1',
'icon': 'dot',
'route': '/1',
},
{
'type': 'link',
'name': 'Menu2 Item 2',
'icon': 'dot',
'route': '/2',
},
{
'type': 'link',
'name': 'Menu2 Item 3',
'icon': 'dot',
'route': '/3',
},
{
'type': 'link',
'name': 'Menu2 Item 4',
'icon': 'dot',
'route': '/4',
},
{
'type': 'menu',
'name': 'Menu2 menu',
'icon': 'menu',
'route': '/menu3',
'children': [
{
'type': 'link',
'name': 'Menu3 Item 1',
'icon': 'dot',
'route': '/1',
},
{
'type': 'link',
'name': 'Menu3 Item 2',
'icon': 'dot',
'route': '/2',
},
{
'type': 'link',
'name': 'Menu3 Item 3',
'icon': 'dot',
'route': '/3',
},
{
'type': 'link',
'name': 'Menu3 Item 4',
'icon': 'dot',
'route': '/4',
},
],
},
],
},
{
'type': 'menu',
'name': 'Settings',
'icon': 'settings',
'route': '/settings',
'children': [
{
'type': 'link',
'name': 'Account',
'icon': 'account',
'route': '/account',
},
],
},
];
final List<DrawerItem> drawerMenu =
drawerMenuMap.map(DrawerItem.fromJson).toList();
@@ -0,0 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
const escape = SingleActivator(LogicalKeyboardKey.escape);
const enter = SingleActivator(LogicalKeyboardKey.enter);
const controlF = SingleActivator(LogicalKeyboardKey.keyF, control: true);
@@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../providers/auth.provider.dart';
class AuthMiddleware extends GetMiddleware {
final authProvider = Get.find<AuthProvider>();
@override
RouteSettings? redirect(String? route) {
return authProvider.isAuth || route == '/login'
? null
: const RouteSettings(name: '/login');
}
}
class GuestMiddleware extends GetMiddleware {
final authProvider = Get.find<AuthProvider>();
@override
RouteSettings? redirect(String? route) {
return authProvider.isAuth && route != '/'
? const RouteSettings(name: '/')
: null;
}
}
@@ -0,0 +1,42 @@
// ignore_for_file: hash_and_equals
import '../../classes/enums.class.dart';
class DrawerItem {
bool isOpen;
final DrawerItemType type;
final String name;
final String icon;
final String route;
final List<DrawerItem> children;
DrawerItem({
this.isOpen = false,
this.type = DrawerItemType.link,
required this.name,
required this.icon,
required this.route,
this.children = const [],
});
factory DrawerItem.fromJson(Map<String, dynamic> json, {String? parent}) {
if (parent is String) json['route'] = parent + json['route'];
final List<Map<String, dynamic>> childrenMap = json['children'] ?? [];
final List<DrawerItem> children = [];
for (var child in childrenMap) {
children.add(DrawerItem.fromJson(child, parent: json['route']));
}
return DrawerItem(
type: children.isEmpty ? DrawerItemType.link : DrawerItemType.menu,
name: json['name'],
icon: json['icon'] ?? '',
route: json['route'],
children: children,
);
}
@override
bool operator ==(Object other) {
return other is DrawerItem && other.name == name && other.route == route;
}
}
@@ -0,0 +1,7 @@
class UserModel {
final String name;
final String email;
final String image;
UserModel(this.name, this.email, this.image);
}
@@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
class DashboardCardModel {
final String name;
final IconData icon;
final int value;
double? persentage;
String? persentageValue;
String? notes;
DashboardCardModel({
required this.name,
required this.icon,
required this.value,
this.persentage,
this.persentageValue,
this.notes,
});
factory DashboardCardModel.fromJson(json) {
return DashboardCardModel(
name: json['name'],
icon: json['icon'],
value: json['value'],
persentage: json['persentage'],
persentageValue: json['persentageValue'],
notes: json['notes'],
);
}
}
@@ -0,0 +1,17 @@
import 'package:syncfusion_flutter_datagrid/datagrid.dart';
class GridColumnModel {
final String name;
final double width;
final bool allowFiltering;
final bool allowSorting;
final ColumnWidthMode columnWidthMode;
GridColumnModel({
required this.name,
this.width = double.nan,
this.allowFiltering = true,
this.allowSorting = true,
this.columnWidthMode = ColumnWidthMode.none,
});
}
@@ -0,0 +1,3 @@
class GeneralSearchModel {
GeneralSearchModel();
}
@@ -0,0 +1,24 @@
import 'package:flutter/services.dart';
import '../classes/functions.class.dart';
class PlatformFunctions {
static void init() {
try {
keyListener();
} catch (e) {
//
}
}
static keyListener() {
RawKeyboard.instance.addListener((event) {
final isKeyDown = event is RawKeyDownEvent;
if (isKeyDown &&
event.logicalKey.keyId == 102 &&
event.isControlPressed) {
focusSearch();
}
});
}
}
@@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../classes/enums.class.dart';
import '../widgets/notifications_widgets/alert.widget.dart';
class AlertProvider extends GetxController {
final Rx<Alert?> _alert = Rx<Alert?>(null);
Widget? get alert => _alert.value;
setAlert(
String message, {
AlertType type = AlertType.info,
Duration autoDismissAfter = Duration.zero,
bool dismissable = true,
}) {
if (message.isEmpty) {
_alert.value = null;
} else {
_alert.value = Alert(
message,
type: type,
dismissible: dismissable,
autoDismissAfter: autoDismissAfter,
onDismiss: () {
_alert.value = null;
update();
},
);
}
update();
}
}
@@ -0,0 +1,50 @@
import 'package:u_vineyard/entities/user.dart';
import 'package:u_vineyard/pages/login.page.dart';
import 'package:u_vineyard/services/api.user.service.dart';
import 'package:u_vineyard/services/auth.service.dart';
import 'package:get/get.dart';
import '../classes/functions.class.dart';
class AuthProvider extends GetxController {
final Rx<User?> _profile = Rx<User?>(null);
final RxBool _isAuth = RxBool(AuthService.instance.token != null);
bool get isAuth => _isAuth.value;
User? get profile => _profile.value;
@override
void onInit() {
super.onInit();
// Chiama la funzione per recuperare il profilo all'inizializzazione del controller
fetchProfileData();
}
Future<void> toggleAuth(bool value) async {
_isAuth.value = value;
if(!value) {
await removeProviders();
} else {
// Se l'utente si autentica, recupera i dati del profilo
await fetchProfileData();
}
update();
}
Future<void> fetchProfileData() async {
final userService = UserService();
// Assumiamo che l'ID utente sia 1
final fetchedUser = await userService.userProfile(1);
// Aggiorna la variabile reattiva
_profile.value = fetchedUser;
}
void logout() async {
_isAuth.value = false;
AuthService.instance.clear();
update();
await removeProviders();
Get.offAllNamed(LoginPage.route);
}
}
@@ -0,0 +1,103 @@
import '../providers/auth.provider.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../classes/functions.class.dart';
import '../classes/routes.class.dart';
import '../models/app_models/drawer_item.model.dart';
import '../screens/errors/404.screen.dart';
class Routes {
static void to(Widget Function() screen, {dynamic arguments}) {
Get.find<RoutesProvider>().to(screen, arguments: arguments);
}
static void toNamed(
String newRoute, {
duplicate = false,
dynamic arguments,
}) {
Get.find<RoutesProvider>().toNamed(
newRoute,
arguments: arguments,
duplicate: duplicate,
);
closeDrawerMenu(newRoute);
}
}
class RoutesProvider extends GetxController {
GlobalKey<ScaffoldState>? mainKey = GlobalKey();
final RxInt _selectedIndex = RxInt(0);
final RxString _current = RxString('/dashboard');
final Rx<Widget Function()?> _currentWidget = Rx<Widget Function()?>(null);
final Rx<dynamic> _arguments = Rx<dynamic>(null);
int get currentIndex => _selectedIndex.value;
String get current => _current.value;
dynamic get arguments => _arguments.value;
bool get isDrawerOpen => mainKey?.currentState?.isDrawerOpen ?? false;
Widget Function() get currentWidget => getWidget();
DrawerItem get currentMenu => drawerMenu[currentIndex];
set currentWidget(Widget Function()? v) => _currentWidget.value = v;
set currentIndex(int v) => _selectedIndex.value = v;
Widget Function() getWidget() {
final auth = Get.find<AuthProvider>();
if (!auth.isAuth) {
auth.logout();
return () => const Error404Screen();
}
return _currentWidget.value != null
? _currentWidget.value!
: (internalRoutes[current] ?? () => const Error404Screen());
}
void toNamed(String newRoute, {duplicate = false, dynamic arguments}) {
if (newRoute.startsWith('/')) {
if (duplicate) {
_arguments.value = arguments;
_current.value = newRoute;
_currentWidget.value = null;
update();
} else if (newRoute != _current.value) {
_arguments.value = arguments;
_current.value = newRoute;
_currentWidget.value = null;
update();
}
}
try {
currentIndex = menuIndex(newRoute);
update();
} catch (e) {
//
}
}
to(Widget Function() screen, {dynamic arguments}) {
_current.value = '-';
_arguments.value = arguments;
currentWidget = screen;
update();
}
void toggleDrawer() {
if (mainKey?.currentState?.isDrawerOpen ?? false) {
mainKey?.currentState?.openDrawer();
} else {
mainKey?.currentState?.closeDrawer();
}
}
void closeDrawer() {
try {
mainKey?.currentState?.closeDrawer();
} catch (e) {
//
}
}
}
@@ -0,0 +1,71 @@
import '../classes/enums.class.dart';
import '../models/general_search.model.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../classes/functions.class.dart';
import '../screens/search/general_search.screen.dart';
import 'routes.provider.dart';
class SearchProvider extends GetxController {
final searchController = TextEditingController();
final node = FocusNode();
final RxBool _isLoading = RxBool(false);
bool get isLoading => _isLoading.value;
@override
void dispose() {
searchController.dispose();
node.dispose();
super.dispose();
}
void loading(bool value) {
_isLoading.value = value;
update();
}
search(String value) async {
if (searchController.text.isEmpty) return;
loading(true);
await Future.delayed(const Duration(seconds: 1), () {
if (value.toLowerCase() == 'test') {
Routes.to(() => const GeneralSerch(<GeneralSearchModel>[]));
searchController.clear();
} else {
toast(
'$value Not found',
type: ToastType.warning,
callback: () => print('clicked'),
);
focus();
}
});
loading(false);
}
void unfocus() {
node.unfocus();
update();
}
void clear() {
node.unfocus();
searchController.clear();
update();
}
void focus() {
try {
node.requestFocus();
searchController.selection = TextSelection(
baseOffset: 0,
extentOffset: searchController.text.length,
);
} catch (e) {
//
}
}
}
@@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../classes/constents.class.dart';
import '../classes/enums.class.dart';
import '../widgets/notifications_widgets/toast.widget.dart';
class ToastProvider extends GetxController {
late GlobalKey<AnimatedListState> listKey;
@override
onInit() {
super.onInit();
setKey(GlobalKey<AnimatedListState>());
}
setKey(GlobalKey<AnimatedListState> key) {
listKey = key;
}
final Rx<List<ToastModel>> _toasts = Rx<List<ToastModel>>([]);
List<ToastModel> get toasts => _toasts.value;
addToast(String? message,
{ToastType type = ToastType.primary, VoidCallback? callback}) {
final item = ToastModel(type: type, message: message, callback: callback);
_toasts.value.insert(0, item);
listKey.currentState?.insertItem(0, duration: toastDuration);
update();
}
removeToast(ToastModel item, DissmissType type) {
try {
final index = _toasts.value.indexOf(item);
_toasts.value.removeAt(index);
listKey.currentState?.removeItem(
index,
(__, animation) =>
type == DissmissType.swipe ? const SizedBox() : buildItem(item, animation),
duration: toastDuration,
);
update();
} catch (e) {
//
}
}
Widget buildItem(ToastModel item, Animation<double> animation) {
return SizeTransition(
sizeFactor: animation,
child: FadeTransition(
opacity: animation,
child: Toast(item, removeToast),
),
);
}
}
@@ -0,0 +1,275 @@
import '../../../providers/auth.provider.dart';
import '../../../widgets/responsive_widgets/responsive.widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import '../../../classes/enums.class.dart';
import '../../../classes/functions.class.dart';
import '../../../providers/alert.provider.dart';
import '../../../widgets/simple_loader.widget.dart';
class LoginScreenOld extends StatefulWidget {
static const route = '/login';
const LoginScreenOld({super.key});
@override
State<LoginScreenOld> createState() => _LoginScreenOldState();
}
class _LoginScreenOldState extends State<LoginScreenOld> {
final TextEditingController emailController = TextEditingController();
final TextEditingController passwordController = TextEditingController();
bool isVisible = false;
bool isLoading = false;
String? error;
@override
void dispose() {
emailController.dispose();
passwordController.dispose();
super.dispose();
}
update(VoidCallback func) {
if (mounted) setState(func);
}
bool validate() {
final email = emailController.text;
final pass = passwordController.text;
if (email.isEmpty) {
return !setError('The email address is required');
}
if (!email.isEmail) {
return !setError('The email address is inviled');
}
if (pass.isEmpty) {
return !setError('The password is required');
}
return true;
}
login() async {
setError('');
update(() => isLoading = true);
final state = validate();
if(Get.find<AuthProvider>().isAuth) {
Get.offAllNamed('/');
return;
}
if (state) {
TextInput.finishAutofillContext();
await Future.delayed(const Duration(seconds: 1), () async {
await Get.find<AuthProvider>().toggleAuth(true);
Get.offAllNamed('/');
});
initializeProviders();
}
update(() => isLoading = false);
}
togglePass() {
if (mounted) setState(() => isVisible = !isVisible);
}
bool setError(String error) {
final alerter = Get.find<AlertProvider>();
alerter.setAlert(
error,
type: AlertType.danger,
dismissable: false,
);
if (mounted) {
setState(() {});
}
if (error.isEmpty) return false;
return true;
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: ResponsiveWidget(
builder: (cnx, screen) => Container(
width: double.infinity,
height: double.infinity,
decoration: !screen.isMobile
? BoxDecoration(
image: DecorationImage(
image: AssetImage(imgUrl('login.jpg')),
fit: BoxFit.cover,
),
)
: null,
child: Center(
child: SingleChildScrollView(
physics:
screen.isBig ? const NeverScrollableScrollPhysics() : null,
child: Container(
width: !screen.isMobile ? 550 : null,
decoration: BoxDecoration(
color: !screen.isMobile ? Colors.white : null,
borderRadius: const BorderRadius.all(Radius.circular(20)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 50),
Text(
'Welcome Back',
style:
Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.w600,
color: const Color(0xff0f0f0f),
),
),
const SizedBox(height: 50),
GetBuilder<AlertProvider>(
init: AlertProvider(),
builder: (alerter) {
return Padding(
padding: const EdgeInsets.only(
bottom: 20,
left: 20,
right: 20,
),
child: alerter.alert ?? const SizedBox(),
);
},
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 30),
child: AutofillGroup(
child: Column(
children: [
TextField(
controller: emailController,
autofocus: true,
autofillHints: const [AutofillHints.email],
decoration: InputDecoration(
labelText: 'Email',
icon: SizedBox(
width: 20,
child: svg(
'profile',
color: Colors.grey.shade800,
),
),
filled: true,
fillColor: const Color(0xFFf0f1f5),
border: const OutlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFf0f1f5),
),
),
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFf0f1f5),
),
),
),
onChanged: (value) => setError(''),
onEditingComplete:
TextInput.finishAutofillContext,
textInputAction: TextInputAction.next,
keyboardType: TextInputType.emailAddress,
onSubmitted: (_) => login(),
),
const SizedBox(height: 20),
TextField(
autofillHints: const [AutofillHints.password],
controller: passwordController,
autofocus: false,
obscureText: !isVisible,
decoration: InputDecoration(
labelText: 'Password',
icon: SizedBox(
width: 20,
child: svg(
'password',
color: Colors.grey.shade800,
),
),
suffixIcon: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10),
child: IconButton(
onPressed: togglePass,
icon: isVisible
? svg('eye.slash.fill',
size: 16,
color: Colors.grey.shade800)
: svg('eye.fill',
size: 15,
color: Colors.grey.shade800),
),
),
filled: true,
fillColor: const Color(0xFFf0f1f5),
border: const OutlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFf0f1f5),
),
),
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFf0f1f5),
),
),
),
onChanged: (value) => setError(''),
onEditingComplete:
TextInput.finishAutofillContext,
textInputAction: TextInputAction.go,
keyboardType: TextInputType.visiblePassword,
onSubmitted: (_) => login(),
),
const SizedBox(height: 50),
SizedBox(
width: 300,
height: 50,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(5),
),
),
),
onPressed: isLoading ? null : login,
child: AnimatedCrossFade(
crossFadeState: !isLoading
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
duration: const Duration(milliseconds: 300),
alignment: Alignment.center,
firstChild: const Text('Login'),
secondChild: const Center(
child: SimpleLoader(
strokeWidth: 3,
size: 20,
),
),
),
),
)
],
),
),
),
const SizedBox(height: 50, width: 200),
],
),
),
),
),
),
),
);
}
}
@@ -0,0 +1,176 @@
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';
import 'package:u_vineyard/template/src/models/dashboard_models/dashboard_card.model.dart';
import 'package:u_vineyard/template/src/providers/auth.provider.dart';
import 'package:u_vineyard/widgets/map.widget.dart';
import 'package:flutter/material.dart';
import '../../widgets/data_tables_widgets/data_table_card.widget.dart';
import '../../widgets/menu_list.widget.dart';
import '../../widgets/responsive_widgets/responsive_grid.widget.dart';
import '../../widgets/responsive_widgets/repesponsive_grid_item.widget.dart';
import 'dashboard_widgets/dashboard_card.widget.dart';
class DashboadScreen extends StatefulWidget {
const DashboadScreen({super.key});
@override
State<DashboadScreen> createState() => _DashboadScreenState();
}
class _VineyardDetails {
var vineyards = Get.find<AuthProvider>().profile?.owners?.expand((owner) => owner.vineyards ?? []).toList() ?? [];
int get numberOfVineyards => vineyards.length;
double get totalArea => vineyards.fold(0.0, (sum, vineyard) => sum! + (vineyard.hectares ?? 0.0)) ?? 0.0;
}
class _DashboadScreenState extends State<DashboadScreen> {
final dashboardCardModelValues = [
DashboardCardModel.fromJson({
'name': 'Vigneti',
'icon': Icons.eco_rounded,
'value': _VineyardDetails().numberOfVineyards,
}),
DashboardCardModel.fromJson({
'name': 'Ettari',
'icon': Icons.grass_rounded,
'value': _VineyardDetails().totalArea.toInt(),
'persentage': 64.1,
'persentageValue': '3.1',
'notes': 'Total 424,567 deliveries',
}),
];
//double _calculatedArea = 0.0;
@override
Widget build(BuildContext context) {
return ResponsiveGrid(
childrenBuilder: (cnx, screen) {
return [
...dashboardCardModelValues
.map(
(item) => ResponsiveGridItem(
animationIndex: dashboardCardModelValues.indexOf(item),
crossAxisCellCount: screen.setAxis(6, 9, 6, 6),
child: DashboardStatisticCard(
item,
callback: () {},
isSmall: screen.isSmall,
actions: [
if (dashboardCardModelValues.indexOf(item) == 0)
MenuListItem(
callback: () {},
svgName: 'menu',
title: 'Lista',
),
MenuListItem(
callback: () {},
svgName: 'menu',
title: 'Aggiungi',
),
],
),
),
)
.toList(),
// ResponsiveGridItem(
// animationIndex: 4,
// crossAxisCellCount: screen.setAxis(6, 9, 6, 6),
// child: CardWidget(
// height: 420,
// child: Column(
// children: [
// Row(
// children: [
// FittedBox(
// fit: BoxFit.scaleDown,
// child: SelectableText(
// 'Shot per distance',
// style: Theme.of(context)
// .textTheme
// .headlineSmall
// ?.copyWith(fontWeight: FontWeight.w700),
// ),
// ),
// ],
// ),
// const Spacer(),
// const AnimationRadialChart(),
// ],
// ),
// ),
// ),
ResponsiveGridItem(
animationIndex: 7,
crossAxisCellCount: 24,
child: MainMapWidget(),
),
// ResponsiveGridItem(
// animationIndex: 5,
// crossAxisCellCount: screen.setAxis(6, 9, 6, 6),
// child: CardWidget(
// height: 420,
// child: Column(
// children: [
// Row(
// children: [
// FittedBox(
// fit: BoxFit.scaleDown,
// child: SelectableText(
// 'Revenue',
// style: Theme.of(context)
// .textTheme
// .headlineSmall
// ?.copyWith(fontWeight: FontWeight.w700),
// ),
// ),
// const Spacer(),
// const SelectableText('Last 7 days')
// ],
// ),
// const Spacer(),
// const AnimationSplineChart(),
// ],
// ),
// ),
// ),
// ResponsiveGridItem(
// animationIndex: 12,
// crossAxisCellCount: screen.setAxis(12, 18, 24, 24),
// child: CardWidget(
// height: 420,
// child: Column(
// children: [
// Row(
// children: [
// FittedBox(
// fit: BoxFit.scaleDown,
// child: SelectableText(
// 'Revenue',
// style: Theme.of(context)
// .textTheme
// .headlineSmall
// ?.copyWith(fontWeight: FontWeight.w700),
// ),
// ),
// const Spacer(),
// const SelectableText('Last 7 days VS period week')
// ],
// ),
// const Spacer(),
// const AnimationSplineChart(),
// ],
// ),
// ),
// ),
ResponsiveGridItem(
animationIndex: 7,
crossAxisCellCount: 24,
child: DataTableCard(screen.isBig, screen.is4K, screen.isMobile),
),
const SizedBox(height: 30),
];
},
);
}
}
@@ -0,0 +1,127 @@
import 'package:flutter/material.dart';
import '../../../models/dashboard_models/dashboard_card.model.dart';
import '../../../widgets/animated_text.widget.dart';
import '../../../widgets/card.widget.dart';
import '../../../widgets/menu_list.widget.dart';
import '../../../widgets/rounded_progress_indicator.widget.dart';
class DashboardStatisticCard extends StatelessWidget {
const DashboardStatisticCard(
this.item, {
super.key,
this.isSmall = false,
this.actions = const [],
this.callback,
});
final DashboardCardModel item;
final bool isSmall;
final List<Widget> actions;
final VoidCallback? callback;
@override
Widget build(BuildContext context) {
return Hero(
tag: item,
child: CardWidget(
callback: callback,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
item.icon,
size: 20,
color: Colors.green,
),
const Spacer(),
// Uncomment and fix the widget below if needed
// RoundedProgressIndicator(
// textValue: Text(
// '+${item.persentageValue}%',
// style: Theme.of(context)
// .textTheme
// .labelSmall
// ?.copyWith(fontWeight: FontWeight.w900),
// ),
// value: (item.persentage) / 100,
// backgroundColor: Colors.grey.shade200,
// size: 50,
// stroke: 6,
// ),
],
),
const SizedBox(height: 4),
SelectableText(
item.name,
style: Theme.of(
context,
).textTheme.labelLarge?.copyWith(
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AnimatedNumber(
number: item.value,
style: Theme.of(
context,
).textTheme.displayMedium?.copyWith(
color: Colors.grey.shade900,
fontWeight: FontWeight.w900,
),
),
const SizedBox(height: 5),
// SelectableText(
// item.notes ?? '',
// maxLines: 1,
// style: Theme.of(
// context,
// ).textTheme.labelLarge?.copyWith(
// color: Colors.grey.shade600,
// fontWeight: FontWeight.w500,
// overflow: TextOverflow.ellipsis,
// ),
// ),
],
),
),
actions.isNotEmpty
? MenuList(
actions: actions,
offset: const Offset(0, 5),
builder: (controller) => IconButton.filledTonal(
onPressed: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
icon: const Icon(Icons.more_vert_rounded),
),
)
: IconButton.filledTonal(
onPressed: callback,
icon: const Icon(
Icons.arrow_forward_ios_rounded,
size: 12,
),
),
],
)
],
),
),
);
}
}
@@ -0,0 +1,101 @@
import 'dart:math';
import '../../../classes/constents.class.dart';
/// Package import
import 'package:flutter/material.dart';
/// Chart import
import 'package:syncfusion_flutter_charts/charts.dart';
/// Renders the spline chart sample with dynamically updated data points.
class AnimationSplineChart extends StatefulWidget {
/// Renders the spline chart sample with dynamically updated data points.
const AnimationSplineChart({super.key});
@override
State<AnimationSplineChart> createState() => _AnimationSplineChartState();
}
class _AnimationSplineChartState extends State<AnimationSplineChart> {
List<_ChartData>? _chartData;
List<_ChartData>? _chartData2;
@override
Widget build(BuildContext context) {
_getChartData();
return _buildAnimationSplineChart();
}
/// get the spline chart sample with dynamically updated data points.
SfCartesianChart _buildAnimationSplineChart() {
return SfCartesianChart(
plotAreaBorderWidth: 0,
primaryXAxis:
NumericAxis(majorGridLines: const MajorGridLines(width: 0)),
primaryYAxis: NumericAxis(
majorTickLines: const MajorTickLines(color: Colors.transparent),
axisLine: const AxisLine(width: 0),
minimum: 0,
maximum: 100),
series: _getDefaultSplineSeries());
}
/// get the spline series sample with dynamically updated data points.
List<SplineSeries<_ChartData, num>> _getDefaultSplineSeries() {
return <SplineSeries<_ChartData, num>>[
SplineSeries<_ChartData, num>(
dataSource: _chartData!,
xValueMapper: (_ChartData sales, _) => sales.x,
yValueMapper: (_ChartData sales, _) => sales.y,
color: primaryColor,
width: 4,
markerSettings: const MarkerSettings(
isVisible: true,
borderWidth: 5,
),
),
SplineSeries<_ChartData, num>(
dataSource: _chartData2!,
xValueMapper: (_ChartData sales, _) => sales.x,
yValueMapper: (_ChartData sales, _) => sales.y,
color: Colors.purple,
width: 4,
markerSettings: const MarkerSettings(
isVisible: true,
borderWidth: 5,
),
),
];
}
@override
void dispose() {
_chartData!.clear();
_chartData2!.clear();
super.dispose();
}
/// get the random value
int _getRandomInt(int min, int max) {
final Random random = Random();
return min + random.nextInt(max - min);
}
//Get the random data points
void _getChartData() {
_chartData = <_ChartData>[];
_chartData2 = <_ChartData>[];
for (int i = 0; i < 5; i++) {
_chartData!.add(_ChartData(i, _getRandomInt(15, 85)));
_chartData2!.add(_ChartData(i, _getRandomInt(15, 85)));
}
}
}
class _ChartData {
_ChartData(this.x, this.y);
final int x;
final int y;
}
@@ -0,0 +1,139 @@
/// Package imports
import '../../../classes/constents.class.dart';
import 'package:flutter/material.dart';
/// Chart import
import 'package:syncfusion_flutter_charts/charts.dart';
/// Render the radial bar customization.
class AnimationRadialChart extends StatefulWidget {
/// Cretaes customised radial bar series.
const AnimationRadialChart({super.key});
@override
State<AnimationRadialChart> createState() => _AnimationRadialChartState();
}
class _AnimationRadialChartState extends State<AnimationRadialChart> {
TooltipBehavior? _tooltipBehavior;
List<_ChartData>? dataSources;
List<Color>? colors;
@override
void initState() {
_tooltipBehavior =
TooltipBehavior(enable: true, format: 'point.x : point.y%');
dataSources = <_ChartData>[
_ChartData(
x: 'Vehicle',
y: 62.70,
text: '10%',
pointColor: const Color.fromRGBO(69, 186, 161, 1.0)),
_ChartData(
x: 'Education',
y: 29.20,
text: '10%',
pointColor: const Color.fromRGBO(230, 135, 111, 1.0)),
_ChartData(
x: 'Home',
y: 85.20,
text: '100%',
pointColor: const Color.fromRGBO(145, 132, 202, 1.0)),
_ChartData(
x: 'Personal',
y: 45.70,
text: '100%',
pointColor: const Color.fromRGBO(235, 96, 143, 1.0))
];
colors = const <Color>[
Color.fromRGBO(69, 186, 161, 1.0),
Color.fromRGBO(230, 135, 111, 1.0),
Color.fromRGBO(145, 132, 202, 1.0),
Color.fromRGBO(235, 96, 143, 1.0)
];
super.initState();
}
@override
Widget build(BuildContext context) {
return _buildCustomizedRadialBarChart();
}
/// Return the circular chart with radial customization.
SfCircularChart _buildCustomizedRadialBarChart() {
return SfCircularChart(
title: ChartTitle(text: ''),
series: _getRadialBarCustomizedSeries(),
tooltipBehavior: _tooltipBehavior,
);
}
/// Returns radial bar which need to be customized.
List<RadialBarSeries<_ChartData, String>> _getRadialBarCustomizedSeries() {
return <RadialBarSeries<_ChartData, String>>[
RadialBarSeries<_ChartData, String>(
animationDuration: 0,
maximumValue: 100,
gap: '10%',
radius: '100%',
dataSource: <_ChartData>[
_ChartData(
x: 'Vehicle',
y: 62.70,
text: '100%',
pointColor: Colors.redAccent,
),
_ChartData(
x: 'Education',
y: 29.20,
text: '100%',
pointColor: Colors.orange,
),
_ChartData(
x: 'Home',
y: 85.20,
text: '100%',
pointColor: Colors.indigoAccent,
),
_ChartData(
x: 'Personal',
y: 45.70,
text: '100%',
pointColor: primaryColor,
)
],
cornerStyle: CornerStyle.bothCurve,
innerRadius: '50%',
xValueMapper: (_ChartData data, _) => data.x,
yValueMapper: (_ChartData data, _) => data.y,
pointRadiusMapper: (_ChartData data, _) => data.text,
/// Color mapper for each bar in radial bar series,
/// which is get from datasource.
pointColorMapper: (_ChartData data, _) => data.pointColor,
legendIconType: LegendIconType.circle,
),
];
}
@override
void dispose() {
dataSources!.clear();
super.dispose();
}
}
class _ChartData {
_ChartData({
required this.x,
required this.y,
required this.text,
required this.pointColor,
});
final String x;
final double y;
final String text;
final Color pointColor;
}
@@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../providers/auth.provider.dart';
class Error404Screen extends StatelessWidget {
const Error404Screen({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Center(
child: ElevatedButton(
onPressed: Get.find<AuthProvider>().logout,
child: const Text('logout'),
),
),
const Text('error NOT found'),
],
),
);
}
}
@@ -0,0 +1,83 @@
import '../../providers/alert.provider.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../classes/constents.class.dart';
import '../../providers/routes.provider.dart';
import '../../widgets/app_widgets/app_breadcamp.widget.dart';
import '../../widgets/app_widgets/app_drawer.widget.dart';
import '../../widgets/app_widgets/app_navbar.widget.dart';
import '../../widgets/notifications_widgets/toast.widget.dart';
import '../../widgets/responsive_widgets/responsive.widget.dart';
class MainScreen extends StatefulWidget {
static const route = '/';
const MainScreen({super.key});
@override
State<MainScreen> createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
@override
Widget build(BuildContext context) {
return ResponsiveWidget(
builder: (context, screen) => GetBuilder<RoutesProvider>(
init: RoutesProvider(),
builder: (router) {
return Scaffold(
key: router.mainKey,
appBar: const AppNarBar(),
drawer: !screen.isBig ? const AppDrawer() : null,
body: Stack(
children: [
Positioned.fill(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (screen.isBig) const AppDrawer(),
Expanded(
child: Column(
children: [
GetBuilder<AlertProvider>(
builder: (alerter) {
return Padding(
padding: EdgeInsets.symmetric(
vertical: 10,
horizontal:
(Get.width - 304) > maxContainerWidth
? (((Get.width - 304) -
maxContainerWidth) /
2) +
2
: 20,
),
child: alerter.alert ?? const SizedBox(),
);
},
),
if (showBreadCamp) const BreadCrumbWidget(),
Expanded(
child: router.currentWidget.call(),
),
],
),
),
],
),
),
const Positioned(
top: 0,
right: 10,
width: 400,
child: Toasts(),
),
],
),
);
},
),
);
}
}
@@ -0,0 +1,15 @@
import 'package:flutter/material.dart';
import '../../models/general_search.model.dart';
class GeneralSerch extends StatelessWidget {
static const route = '/search';
const GeneralSerch(this.result, {super.key});
final List<GeneralSearchModel> result;
@override
Widget build(BuildContext context) {
return Text(result.toString());
}
}
@@ -0,0 +1,91 @@
import '../providers/auth.provider.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../providers/routes.provider.dart';
import 'menu_list.widget.dart';
class AccountWidget extends StatelessWidget {
const AccountWidget({
required this.desktop,
super.key,
});
final bool desktop;
@override
Widget build(BuildContext context) {
return MenuList(
actions: [
MenuListItem(
callback: () => Routes.toNamed('/settings/account'),
svgName: 'profile',
title: 'Profile',
),
MenuListItem(
callback: Get.find<AuthProvider>().logout,
svgName: 'logout',
title: 'Logout',
color: Colors.redAccent,
),
],
builder: (controller) => GestureDetector(
onTap: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
child: Container(
width: desktop ? 200 : null,
padding: const EdgeInsets.symmetric(horizontal: 7),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(30)),
color: desktop ? Colors.grey.shade100 : null,
),
child: GetBuilder<AuthProvider>(builder: (provider) {
return desktop
? Row(
children: [
const SizedBox(width: 7),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
provider.profile?.username ?? '',
style: Theme.of(context)
.textTheme
.labelMedium
?.copyWith(fontWeight: FontWeight.bold),
),
Text(
provider.profile!.role?.name ?? '',
style: Theme.of(context).textTheme.labelSmall,
),
],
),
),
const Icon(Icons.keyboard_arrow_down_rounded)
],
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.account_circle_rounded, size: 30),
const SizedBox(width: 10),
Text(
provider.profile?.username ?? '',
style: Theme.of(context).textTheme.labelMedium,
),
],
);
}),
),
),
);
}
}
@@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
class AnimatedNumber extends StatefulWidget {
const AnimatedNumber({
Key? key,
required this.number,
this.separator = 3,
this.style,
}) : super(key: key);
final int number;
final int separator;
final TextStyle? style;
@override
State<AnimatedNumber> createState() => _AnimatedNumberState();
}
class _AnimatedNumberState extends State<AnimatedNumber>
with SingleTickerProviderStateMixin {
late Animation<int> _animation;
late AnimationController _animationController;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
);
_animation = IntTween(begin: 0, end: widget.number).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.fastOutSlowIn,
));
_animationController.forward();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedTextValue(
animation: _animation,
separator: widget.separator,
style: widget.style,
);
}
}
class AnimatedTextValue extends AnimatedWidget {
final Animation<int> animation;
const AnimatedTextValue({
Key? key,
required this.animation,
required this.separator,
this.style,
}) : super(key: key, listenable: animation);
final int separator;
final TextStyle? style;
_separatValue(int value) {
if (separator > 0 && value.toString().length > separator) {
final String valueString = value.toString();
final int valueLength = valueString.length;
final int separatorIndex = valueLength - separator;
final String valueBeforeSeparator =
valueString.substring(0, separatorIndex);
final String valueAfterSeparator = valueString.substring(separatorIndex);
return '$valueBeforeSeparator,$valueAfterSeparator';
}
return value.toString();
}
@override
Widget build(BuildContext context) {
return SelectableText(
_separatValue(animation.value),
maxLines: 1,
style: style ??
const TextStyle(
fontSize: 26,
height: 1.4,
fontWeight: FontWeight.w900,
),
);
}
}
@@ -0,0 +1,86 @@
import 'package:flutter/material.dart';
import 'package:flutter_breadcrumb/flutter_breadcrumb.dart';
import 'package:get/get.dart';
import '../../classes/constents.class.dart';
import '../../providers/routes.provider.dart';
class BreadCrumbWidget extends StatelessWidget {
const BreadCrumbWidget({this.onBreadTap, super.key});
final void Function(String)? onBreadTap;
@override
Widget build(BuildContext context) {
return GetBuilder<RoutesProvider>(
builder: (router) {
final items = router.current.trim().replaceFirst('/', '').split('/');
return Visibility(
visible: items.isNotEmpty,
child: Container(
margin: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 10,
),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: const BorderRadius.all(
Radius.circular(10),
),
),
child: Row(
children: [
Expanded(
child: BreadCrumb(
items: items
.map(
(e) => BreadCrumbItem(
padding: const EdgeInsets.all(5),
borderRadius:
const BorderRadius.all(Radius.circular(5)),
onTap: onBreadTap == null
? null
: () {
final route = items
.getRange(0, items.indexOf(e) + 1)
.toList()
.join('/');
onBreadTap?.call('/$route');
},
content: Text(
e.capitalize.toString(),
style: Theme.of(context)
.textTheme
.labelMedium
?.copyWith(
fontWeight: FontWeight.w500,
color: onBreadTap == null
? Colors.grey.shade600
: primaryColor,
),
),
),
)
.toList(),
divider: Text(
' / ',
style: Theme.of(context)
.textTheme
.labelMedium
?.copyWith(color: Colors.grey),
),
overflow: const WrapOverflow(
keepLastDivider: false,
direction: Axis.horizontal,
),
),
),
],
),
),
);
},
);
}
}
@@ -0,0 +1,249 @@
import '../../classes/functions.class.dart';
import '../../providers/routes.provider.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../classes/constents.class.dart';
import '../../classes/enums.class.dart';
import '../../classes/routes.class.dart';
import '../../models/app_models/drawer_item.model.dart';
import 'app_logo.widget.dart';
import '../responsive_widgets/responsive.widget.dart';
class AppDrawer extends StatefulWidget {
const AppDrawer({super.key});
@override
State<AppDrawer> createState() => _AppDrawerState();
}
class _AppDrawerState extends State<AppDrawer> with TickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<double> _animation;
final provider = Get.find<RoutesProvider>();
bool isLoading = true;
@override
void initState() {
_controller = AnimationController(
duration: const Duration(milliseconds: 275),
vsync: this,
);
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.easeIn,
);
provider.addListener(toggle);
if (provider.currentMenu.children.isNotEmpty) {
_controller.forward();
}
super.initState();
}
final scrollController = ScrollController();
toggle() {
if (provider.currentMenu.children.isNotEmpty) {
_controller.forward();
}
if (provider.currentMenu.children.isEmpty) {
_controller.reverse();
}
}
@override
void dispose() {
provider.removeListener(toggle);
// _controller.dispose();
// TODO:: find a sloution to dispose _controller
scrollController.dispose();
super.dispose();
}
List<Widget> getMenu(DrawerItem item) {
if (item.type == DrawerItemType.menu) {
final children = item.children;
return children.map(getItem).toList();
}
return [];
}
Widget getItem(DrawerItem item, {String? parent, bool sub = true}) {
if (item.type == DrawerItemType.menu) {
final children = item.children;
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 1,
),
child: ExpansionTile(
key: GlobalKey(),
leading: svg(
item.icon,
size: sub ? 15 : 20,
color: item.isOpen ? Colors.blueGrey.shade800 : Colors.blueGrey,
),
title: Text(
item.name,
selectionColor: Colors.grey.shade900,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: item.isOpen ? FontWeight.w600 : FontWeight.w500,
fontSize: sub ? 14 : null,
),
),
// childrenPadding: const EdgeInsets.only(left: 10),
initiallyExpanded: item.isOpen,
backgroundColor: Colors.grey.withOpacity(.05),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
collapsedShape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
children: children.map((v) => getItem(v, sub: true)).toList(),
),
);
}
final selected = provider.current == item.route;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 3),
child: ListTile(
key: ValueKey(item),
title: Text(item.name),
dense: sub,
leading: svg(
item.icon,
size: sub ? 15 : 20,
color: selected ? Colors.blueGrey.shade800 : Colors.blueGrey,
),
selectedColor: Colors.grey.shade900,
hoverColor: Colors.grey.withOpacity(.1),
selected: selected,
selectedTileColor: Colors.grey.withOpacity(.2),
shape: ContinuousRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
style: ListTileStyle.drawer,
onTap: () {
Routes.toNamed(
item.route,
duplicate: true,
);
// closeDrawerMenu(item.route);
if (provider.isDrawerOpen) {
provider.closeDrawer();
}
},
),
);
}
@override
Widget build(BuildContext context) {
return ResponsiveWidget(builder: (context, screen) {
return GetBuilder<RoutesProvider>(builder: (provider) {
return Row(
children: [
SizedBox(
width: 92,
child: NavigationRail(
onDestinationSelected: (value) {
final item = drawerMenu[value];
if (item.type == DrawerItemType.link) {
Routes.toNamed(
item.route,
duplicate: true,
);
if (provider.isDrawerOpen) {
provider.closeDrawer();
}
}
provider.currentIndex = value;
provider.update();
// change(value);
},
useIndicator: true,
indicatorColor: Colors.white,
backgroundColor: primaryColor,
labelType: NavigationRailLabelType.all,
unselectedLabelTextStyle: Theme.of(context)
.textTheme
.labelMedium
?.copyWith(color: Colors.white70),
selectedLabelTextStyle: Theme.of(context)
.textTheme
.labelMedium
?.copyWith(color: Colors.white),
indicatorShape: ContinuousRectangleBorder(
side:
BorderSide(color: Colors.grey.withOpacity(.2), width: 2),
borderRadius: const BorderRadius.all(Radius.circular(10)),
),
leading: !screen.isBig
? GestureDetector(
onTap: provider.closeDrawer,
child: const Padding(
padding: EdgeInsets.all(20),
child: Logo(color: Colors.white),
),
)
: null,
destinations: [
...drawerMenu
.map<NavigationRailDestination>(
(item) => NavigationRailDestination(
padding: const EdgeInsets.all(10),
icon: svg(
item.icon,
size: 20,
color: Colors.white70,
),
selectedIcon: svg(
item.icon,
size: 20,
),
label: Text(item.name),
),
)
.toList(),
],
selectedIndex: provider.currentIndex,
),
),
SizeTransition(
sizeFactor: _animation,
axis: Axis.horizontal,
axisAlignment: -1,
child: Drawer(
backgroundColor: drawerColor,
shadowColor: Colors.black38,
elevation: 5,
shape: const ContinuousRectangleBorder(),
child: Padding(
padding: EdgeInsets.only(top: screen.isMobile ? 90 : 10),
child: GetBuilder<RoutesProvider>(
builder: (provider) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Scrollbar(
controller: scrollController,
child: ListView(
controller: scrollController,
children: getMenu(provider.currentMenu),
),
),
);
},
),
),
),
)
],
);
});
});
}
}
@@ -0,0 +1,14 @@
import 'package:flutter/material.dart';
import '../../classes/functions.class.dart';
class Logo extends StatelessWidget {
const Logo({this.color, super.key});
final Color? color;
@override
Widget build(BuildContext context) {
return svg('logo', size: 35, color: color);
}
}
@@ -0,0 +1,103 @@
import '../../widgets/responsive_widgets/responsive.widget.dart';
import 'package:flutter/material.dart';
import '../../classes/constents.class.dart';
import '../../classes/functions.class.dart';
import '../account.widget.dart';
import '../navbar_buttom.widget.dart';
import 'app_logo.widget.dart';
import 'app_search.widget.dart';
class AppNarBar extends StatelessWidget implements PreferredSizeWidget {
const AppNarBar({
this.title = '',
super.key,
});
final String title;
@override
Size get preferredSize => const Size.fromHeight(80);
@override
Widget build(BuildContext context) {
return ResponsiveWidget(builder: (context, screen) {
return AppBar(
elevation: 5,
shadowColor: Colors.black26,
scrolledUnderElevation: 5,
leadingWidth: 92,
toolbarHeight: preferredSize.height,
backgroundColor: navbarColor,
centerTitle: true,
leading: !screen.isBig
? Container(
width: 92,
padding: const EdgeInsets.all(12),
child: IconButton.filledTonal(
icon: const Icon(Icons.menu),
style: IconButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: primaryColor,
shadowColor: Colors.black26,
elevation: 5,
shape: const CircleBorder(),
),
onPressed: () {
Scaffold.of(context).openDrawer();
},
tooltip:
MaterialLocalizations.of(context).openAppDrawerTooltip,
),
)
: Container(
color: primaryColor,
child: const Padding(
padding: EdgeInsets.all(15),
child: Logo(color: Colors.white),
),
),
title: Row(
children: [
const Expanded(
child: Center(
child: SizedBox(
width: 900,
child: AppSearch(),
),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (!screen.isMobile)
SizedBox(
height: 50,
child: NavBarButtonWidget(
desktop: screen.isBig,
text: 'Show Model',
icon: Icons.add_rounded,
onTap: () => showModel('test'),
),
),
const Row(
mainAxisSize: MainAxisSize.min,
children: [],
),
SizedBox(
height: 50,
child: AccountWidget(
desktop: screen.isBig,
),
),
],
),
)
],
),
);
});
}
}
@@ -0,0 +1,117 @@
import '../../providers/search.provider.dart';
import '../../widgets/simple_loader.widget.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../classes/constents.class.dart';
import '../../classes/shortcuts.class.dart';
class AppSearch extends StatefulWidget {
const AppSearch({super.key});
@override
State<AppSearch> createState() => _AppSearchState();
}
class _AppSearchState extends State<AppSearch> {
final provider = Get.find<SearchProvider>();
@override
Widget build(BuildContext context) {
return CallbackShortcuts(
bindings: {
escape: provider.clear,
controlF: provider.focus,
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 5),
child: TextField(
controller: provider.searchController,
focusNode: provider.node,
onTapOutside: (e) => provider.unfocus(),
onSubmitted: provider.search,
onChanged: (value) => setState(() {}),
keyboardType: TextInputType.text,
textInputAction: TextInputAction.search,
decoration: InputDecoration(
hintText: 'Search',
fillColor: Colors.white,
prefixIcon: Padding(
padding: const EdgeInsets.all(15),
child: GetBuilder<SearchProvider>(
builder: (provider) {
return provider.isLoading
? const SimpleLoader(
size: 20,
strokeWidth: 2,
)
: const Icon(Icons.search_rounded);
},
),
),
suffixIcon: GetBuilder<SearchProvider>(
builder: (provider) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Visibility(
visible: provider.searchController.text.isEmpty,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 5,
),
decoration: const BoxDecoration(
color: Color(0x77EFEFEF),
borderRadius: BorderRadius.all(Radius.circular(5)),
),
child: Text(
'Ctrl + F',
style:
Theme.of(context).textTheme.labelSmall?.copyWith(
fontWeight: FontWeight.w600,
color: const Color(0xFF616162),
),
),
),
),
const SizedBox(width: 15),
Visibility(
visible: provider.searchController.text.isNotEmpty,
child: InkWell(
onTap: provider.clear,
child: const Icon(
Icons.cancel_rounded,
color: primaryColor,
),
),
),
Visibility(
visible: provider.searchController.text.isNotEmpty,
child: const SizedBox(width: 15),
),
],
);
},
),
filled: true,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: primaryColor.withOpacity(.1),
width: 2,
),
borderRadius: const BorderRadius.all(Radius.circular(10)),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(
color: primaryColor,
width: 2,
),
borderRadius: BorderRadius.all(Radius.circular(10)),
),
),
),
),
);
}
}
+37
View File
@@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import '../classes/constents.class.dart';
class CardWidget extends StatelessWidget {
const CardWidget({
required this.child,
this.callback,
this.width = 300,
this.height,
super.key,
});
final Widget child;
final VoidCallback? callback;
final double width;
final double? height;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: callback,
child: Container(
width: width,
height: height,
margin: const EdgeInsets.all(10),
padding: const EdgeInsets.all(30),
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(30)),
boxShadow: cardShadow,
color: Colors.white,
),
child: child,
),
);
}
}
@@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
typedef WidgetChildBuilder = Widget Function(
BuildContext context,
Widget child,
);
class ChildBuilder extends StatelessWidget {
/// Creates a widget that delegates its build to a callback.
///
/// The [builder] argument must not be null.
const ChildBuilder({
super.key,
required this.builder,
required this.child,
});
/// Called to obtain the child widget.
///
/// This function is called whenever this widget is included in its parent's
/// build and the old widget (if any) that it synchronizes with has a distinct
/// object identity. Typically the parent's build method will construct
/// a new tree of widgets and so a new Builder child will not be [identical]
/// to the corresponding old one.
final WidgetChildBuilder builder;
final Widget child;
@override
Widget build(BuildContext context) => builder(context, child);
}
@@ -0,0 +1,159 @@
import '../../classes/constents.class.dart';
import '../../widgets/responsive_widgets/responsive.widget.dart';
import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_datagrid/datagrid.dart';
// ignore: depend_on_referenced_packages
import 'package:syncfusion_flutter_core/theme.dart';
import '../../classes/functions.class.dart';
import '../../models/data_table_models/grid_column.model.dart';
class DataTableWidget extends StatelessWidget {
const DataTableWidget(this.columnNames, {required this.dataRows, super.key});
final List<GridColumnModel> columnNames;
final List<DataGridRow> dataRows;
@override
Widget build(BuildContext context) {
return ResponsiveWidget(builder: (context, screen) {
return Container(
decoration: BoxDecoration(
border: Border.all(
color: Colors.blueGrey.withOpacity(.1),
),
),
child: SfDataGridTheme(
data: SfDataGridThemeData(
rowHoverColor: primaryColor.withOpacity(.1),
headerHoverColor: primaryColor.withOpacity(.1),
sortIcon: const Icon(
Icons.arrow_downward_rounded,
size: 20,
color: Colors.blueGrey,
),
filterIcon: const Icon(
Icons.filter_alt_rounded,
size: 20,
color: Colors.blueGrey,
),
),
child: SfDataGrid(
rowsPerPage: 30,
allowFiltering: true,
allowSorting: true,
columnWidthMode: ColumnWidthMode.fill,
rowHeight: 70,
gridLinesVisibility: GridLinesVisibility.horizontal,
headerGridLinesVisibility: GridLinesVisibility.both,
source: TableDataList(
context,
data: dataRows,
isSmallScreen: screen.isSmall,
),
// tableSummaryRows: [
// GridTableSummaryRow(
// showSummaryInRow: true,
// title: 'Total: {Count} User',
// columns: [
// const GridSummaryColumn(
// name: 'Count',
// columnName: 'ID',
// summaryType: GridSummaryType.count,
// )
// ],
// position: GridTableSummaryRowPosition.bottom,
// )
// ],
columns: columnNames
.map(
(e) => GridColumn(
columnName: e.name,
width: e.width,
allowFiltering: e.allowFiltering,
allowSorting: e.allowSorting,
columnWidthMode: e.columnWidthMode,
filterIconPadding: EdgeInsets.zero,
label: Container(
padding: const EdgeInsets.all(16.0),
alignment: Alignment.center,
child: FittedBox(
child: Text(
e.name,
style:
Theme.of(context).textTheme.labelMedium?.copyWith(
fontWeight: FontWeight.w800,
color: Colors.blueGrey,
),
),
),
),
),
)
.toList(),
),
),
);
});
}
}
class TableDataList<T> extends DataGridSource {
TableDataList(
BuildContext cnx, {
required List<DataGridRow> data,
bool isSmallScreen = false,
}) {
context = cnx;
_usersList = data;
}
List<DataGridRow> _usersList = [];
late BuildContext context;
@override
List<DataGridRow> get rows => _usersList;
@override
Widget? buildTableSummaryCellWidget(
GridTableSummaryRow summaryRow,
GridSummaryColumn? summaryColumn,
RowColumnIndex rowColumnIndex,
String summaryValue) {
return Container(
alignment: Alignment.centerLeft,
padding: const EdgeInsets.all(15.0),
child: SelectableText(
summaryValue,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w700,
color: Colors.blueGrey,
),
),
);
}
@override
DataGridRowAdapter? buildRow(DataGridRow row) {
return DataGridRowAdapter(
cells: row.getCells().map<Widget>((dataGridCell) {
return Container(
width: double.infinity,
alignment: Alignment.center,
padding: const EdgeInsets.all(15.0),
child: dataGridCell.value is Widget
? dataGridCell.value
: dataGridCell.value is DateTime
? SelectableText(
formatDate(dataGridCell.value as DateTime),
style: Theme.of(context).textTheme.labelMedium,
)
: SelectableText(
dataGridCell.value.toString(),
style: Theme.of(context).textTheme.labelMedium?.copyWith(
color:
dataGridCell.value is int ? Colors.blueGrey : null),
),
);
}).toList());
}
}
@@ -0,0 +1,295 @@
import '../../classes/constents.class.dart';
import '../../classes/functions.class.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_datagrid/datagrid.dart';
import '../../../demo.data.dart';
import '../../classes/enums.class.dart';
import '../../models/data_table_models/grid_column.model.dart';
import '../card.widget.dart';
import '../dialogs/add_user.dialog.dart';
import '../menu_list.widget.dart';
import '../simple_loader.widget.dart';
import 'data_table.widget.dart';
import 'data_table_search.widget.dart';
class DataTableCard extends StatefulWidget {
const DataTableCard(this.isBig, this.is4K, this.isMobile, {super.key});
final bool isBig;
final bool is4K;
final bool isMobile;
@override
State<DataTableCard> createState() => _DataTableCardState();
}
class _DataTableCardState extends State<DataTableCard> {
bool isLoadingTable = true;
List<UsersModel> users = [];
List<UsersModel> filteredUsers = [];
final searchController = TextEditingController();
void set(VoidCallback func) => mounted ? setState(func) : null;
void onTableSearch(String value) {
if (value.isEmpty) {
filteredUsers = users.toList();
} else {
filteredUsers = users.where((e) {
return e.id == value ||
e.name.toLowerCase().contains(value.toLowerCase()) ||
e.email.toLowerCase().contains(value.toLowerCase()) ||
e.phone.toLowerCase().contains(value.toLowerCase());
}).toList();
}
set(() {});
}
Future<void> getTableData() async {
set(() => isLoadingTable = true);
await Future.delayed(const Duration(seconds: 1), () {
users = demoUsersList;
filteredUsers = demoUsersList;
set(() => isLoadingTable = false);
});
}
@override
void initState() {
getTableData();
super.initState();
}
@override
void dispose() {
searchController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return CardWidget(
height: 700,
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
FittedBox(
fit: BoxFit.scaleDown,
child: SelectableText(
'Users',
style: Theme.of(context)
.textTheme
.headlineSmall
?.copyWith(fontWeight: FontWeight.w700),
),
),
Row(
children: [
if (!widget.isMobile)
DataTableSearchWidget(
onChange: onTableSearch,
controller: searchController,
hint: 'Search Name, Email, Phone',
),
const SizedBox(width: 15),
IconButton.filledTonal(
onPressed: () {},
tooltip: 'Export CSV',
splashRadius: 25,
icon: const Icon(Icons.ios_share_rounded, size: 20),
),
const SizedBox(width: 15),
IconButton.filledTonal(
onPressed: getTableData,
tooltip: 'Refresh',
splashRadius: 25,
icon: isLoadingTable
? const SimpleLoader(
size: 20,
strokeWidth: 2,
)
: const Icon(Icons.refresh_rounded, size: 20),
),
const SizedBox(width: 15),
const IconButton.filledTonal(
onPressed: showAddUserDialog,
tooltip: 'Add User',
splashRadius: 25,
icon: Icon(Icons.add_rounded, size: 20),
),
],
),
],
),
const SizedBox(height: 20),
Expanded(
child: isLoadingTable
? const Center(child: SimpleLoader())
: DataTableWidget(
[
GridColumnModel(
name: '#',
width: 70,
allowFiltering: false,
allowSorting: false,
),
GridColumnModel(name: 'Name'),
if (widget.isBig) GridColumnModel(name: 'Date of birth'),
if (widget.isBig) GridColumnModel(name: 'Gender'),
GridColumnModel(name: 'Email'),
GridColumnModel(name: 'Phone'),
GridColumnModel(
name: 'Status',
allowFiltering: false,
allowSorting: false,
width: 120,
),
GridColumnModel(
name: 'Actions',
allowFiltering: false,
allowSorting: false,
columnWidthMode: ColumnWidthMode.lastColumnFill,
),
],
dataRows: filteredUsers
.map<DataGridRow>(
(user) => DataGridRow(
cells: [
DataGridCell<Widget>(
columnName: '#',
value: CircleAvatar(
radius: 35,
backgroundImage: NetworkImage(user.avatar),
),
),
DataGridCell<String>(
columnName: 'Name', value: user.name),
if (widget.isBig)
DataGridCell<DateTime>(
columnName: 'Date of birth',
value: user.dateOfBirth,
),
if (widget.isBig)
DataGridCell<String>(
columnName: 'Gender', value: user.gender),
DataGridCell<String>(
columnName: 'Email', value: user.email),
DataGridCell<String>(
columnName: 'Phone', value: user.phone),
DataGridCell<Widget>(
columnName: 'Status',
value: SizedBox(
height: 30,
child: FittedBox(
child: CupertinoSwitch(
value: user.status,
activeColor: primaryColor,
onChanged: (value) {
user.status = value;
set(() {});
toast(
'User "${user.name}" has been ${value ? 'Activated' : 'Deactivated'}',
type: value
? ToastType.success
: ToastType.warning,
);
},
),
),
),
),
DataGridCell<Widget>(
columnName: 'Actions',
value: widget.is4K
? Row(
mainAxisSize: MainAxisSize.min,
children: [
FittedBox(
child: IconButton(
onPressed: () {},
tooltip: 'Details',
splashRadius: 25,
icon: const Icon(
Icons.person_outline,
color: Colors.blueGrey,
size: 20),
),
),
const SizedBox(width: 10),
FittedBox(
child: IconButton(
onPressed: () {},
tooltip: 'Edit',
splashRadius: 25,
icon: const Icon(
Icons.edit_outlined,
color: Colors.blueGrey,
size: 20),
),
),
const SizedBox(width: 10),
FittedBox(
child: IconButton(
onPressed: () {},
tooltip: 'Delete Account',
splashRadius: 25,
icon: const Icon(
Icons.delete_outline_rounded,
color: Colors.red,
size: 20),
),
),
],
)
: MenuList(
actions: [
MenuListItem(
callback: () => print(user.id),
svgName: 'profile',
title: 'User Details',
),
MenuListItem(
callback: () => print(user.name),
svgName: 'menu',
title: 'Details',
),
MenuListItem(
callback: () => print(user.id),
title: 'Remove',
svgName: 'logout',
color: Colors.redAccent,
),
],
offset: const Offset(0, 5),
builder: (controller) => IconButton(
onPressed: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
icon: const Icon(
Icons.more_vert_rounded),
),
),
),
],
),
)
.toList(),
),
),
],
),
);
}
}
@@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import '../../classes/constents.class.dart';
import '../../classes/shortcuts.class.dart';
class DataTableSearchWidget extends StatelessWidget {
const DataTableSearchWidget(
{required this.controller, required this.onChange, this.hint, super.key});
final TextEditingController controller;
final void Function(String) onChange;
final String? hint;
clear() {
controller.clear();
onChange('');
}
@override
Widget build(BuildContext context) {
return Container(
constraints: const BoxConstraints(maxWidth: 400),
padding: const EdgeInsets.symmetric(horizontal: 5),
child: CallbackShortcuts(
bindings: {escape: clear},
child: TextField(
controller: controller,
onSubmitted: onChange,
onChanged: onChange,
keyboardType: TextInputType.text,
textInputAction: TextInputAction.search,
decoration: InputDecoration(
hintText: hint ?? 'Search',
hintStyle: Theme.of(context)
.textTheme
.labelMedium
?.copyWith(color: Colors.blueGrey),
fillColor: Colors.white,
prefixIcon: const Padding(
padding: EdgeInsets.all(15),
child: Icon(Icons.search_rounded),
),
suffixIcon: Row(
mainAxisSize: MainAxisSize.min,
children: [
Visibility(
visible: controller.text.isNotEmpty,
child: InkWell(
onTap: clear,
child: const Icon(
Icons.cancel_rounded,
color: primaryColor,
),
),
),
Visibility(
visible: controller.text.isNotEmpty,
child: const SizedBox(width: 15),
),
],
),
filled: true,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: primaryColor.withOpacity(.1),
width: 2,
),
borderRadius: const BorderRadius.all(Radius.circular(10)),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(
color: primaryColor,
width: 2,
),
borderRadius: BorderRadius.all(Radius.circular(10)),
),
),
),
),
);
}
}
@@ -0,0 +1,46 @@
import '../../classes/constents.class.dart';
import '../../classes/enums.class.dart';
import '../../classes/functions.class.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
Future<bool> showAddUserDialog() async {
bool ifSuccess() {
Get.back();
alert(
'User has been added !!',
type: AlertType.success,
autoDismissAfter: const Duration(seconds: 4),
);
return true;
}
bool ifFiled() {
return false;
}
return await Get.defaultDialog<bool>(
title: 'Add User',
backgroundColor: Colors.white,
buttonColor: primaryColor,
content: const AddUserDialogWidget(),
onConfirm: ifSuccess,
onCancel: ifFiled,
).then((value) {
return value ?? false ? ifSuccess() : ifFiled();
});
}
class AddUserDialogWidget extends StatelessWidget {
const AddUserDialogWidget({super.key});
@override
Widget build(BuildContext context) {
return Container(
width: 1000,
height: Get.height - 300,
color: Colors.white,
child: const SingleChildScrollView(),
);
}
}
@@ -0,0 +1,72 @@
import 'package:flutter/material.dart';
import '../classes/functions.class.dart';
class MenuList extends StatelessWidget {
const MenuList({
required this.builder,
required this.actions,
this.offset,
super.key,
});
final Widget Function(MenuController) builder;
final List<Widget> actions;
final Offset? offset;
@override
Widget build(BuildContext context) {
return MenuAnchor(
alignmentOffset: offset ?? const Offset(0, 15),
menuChildren: actions,
builder: (_, controller, __) => builder(controller),
style: MenuStyle(
shape: MaterialStateProperty.all(
const ContinuousRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(20)),
),
),
alignment: Alignment.bottomLeft,
),
);
}
}
class MenuListItem extends StatelessWidget {
const MenuListItem({
required this.title,
required this.svgName,
this.width = 200,
this.callback,
this.color,
super.key,
});
final double width;
final VoidCallback? callback;
final String title;
final String svgName;
final Color? color;
@override
Widget build(BuildContext context) {
return SizedBox(
width: width,
child: MenuItemButton(
style: MenuItemButton.styleFrom(
backgroundColor: Colors.white,
),
requestFocusOnHover: false,
onPressed: callback,
trailingIcon: svg(svgName, size: 16, color: color ?? Colors.blueGrey),
child: Text(
title,
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(color: color ?? Colors.blueGrey.shade900),
),
),
);
}
}
@@ -0,0 +1,55 @@
import '../classes/constents.class.dart';
import 'package:flutter/material.dart';
class NavBarButtonWidget extends StatelessWidget {
const NavBarButtonWidget({
required this.desktop,
required this.onTap,
required this.icon,
required this.text,
super.key,
});
final bool desktop;
final VoidCallback onTap;
final IconData icon;
final String text;
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 10),
child: desktop
? ElevatedButton.icon(
onPressed: onTap,
icon: Icon(icon),
label: Text(text),
style: ElevatedButton.styleFrom(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10)),
),
),
)
: SizedBox(
width: 45,
child: Tooltip(
message: text,
child: IconButton.filled(
onPressed: onTap,
icon: Icon(icon),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: primaryColor,
shadowColor: Colors.black26,
elevation: 5,
shape: const CircleBorder(),
// shape: const ContinuousRectangleBorder(
// borderRadius: BorderRadius.all(Radius.circular(10)),
// ),
),
),
),
),
);
}
}
@@ -0,0 +1,168 @@
import '../../classes/constents.class.dart';
import '../../providers/alert.provider.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../classes/enums.class.dart';
class Alert extends StatefulWidget {
const Alert(
this.message, {
this.type = AlertType.primary,
this.autoDismissAfter,
this.dismissible = false,
this.onDismiss,
super.key,
});
final String message;
final AlertType type;
final bool dismissible;
final Duration? autoDismissAfter;
final VoidCallback? onDismiss;
@override
State<Alert> createState() => _AlertState();
}
class _AlertState extends State<Alert> with TickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<double> _animation;
final provider = Get.find<AlertProvider>();
@override
void initState() {
_controller = AnimationController(
duration: const Duration(milliseconds: 275),
vsync: this,
);
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.easeIn,
);
provider.addListener(toggle);
if (provider.alert is Widget) {
_controller.forward();
}
super.initState();
}
toggle() {
if (provider.alert is Widget) {
_controller.forward();
}
if (provider.alert == null) {
_controller.reverse();
}
}
@override
void dispose() {
provider.removeListener(toggle);
_controller.dispose();
super.dispose();
}
Color getColor() {
switch (widget.type) {
case AlertType.success:
return Colors.green;
case AlertType.danger:
return Colors.red;
case AlertType.warning:
return Colors.amber;
case AlertType.info:
return Colors.blue;
default:
return primaryColor;
}
}
IconData getIcon() {
switch (widget.type) {
case AlertType.success:
return Icons.check_circle_rounded;
case AlertType.danger:
return Icons.cancel_rounded;
case AlertType.warning:
return Icons.warning_rounded;
case AlertType.info:
return Icons.info_rounded;
default:
return Icons.notifications_rounded;
}
}
void dissmiss() {
if (mounted) {
_controller.reverse().then((value) {
if (widget.onDismiss is VoidCallback) widget.onDismiss?.call();
});
}
}
@override
Widget build(BuildContext context) {
if (widget.autoDismissAfter is Duration &&
widget.autoDismissAfter!.inSeconds > 0) {
Future.delayed(widget.autoDismissAfter as Duration, dissmiss);
}
final color = getColor();
return SizeTransition(
sizeFactor: _animation,
axis: Axis.vertical,
axisAlignment: -1,
child: Container(
padding: const EdgeInsets.all(10),
margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 7),
decoration: BoxDecoration(
color: color.withOpacity(.1),
borderRadius: const BorderRadius.all(Radius.circular(10)),
),
width: double.infinity,
child: Row(
children: [
const SizedBox(width: 5),
Icon(getIcon(), color: color),
const SizedBox(width: 10),
Expanded(
child: SelectableText(
widget.message,
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(color: color),
),
),
if (widget.dismissible) ...[
const SizedBox(width: 10),
InkWell(
onTap: dissmiss,
borderRadius: const BorderRadius.all(Radius.circular(5)),
radius: 25,
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 6, vertical: 3),
decoration: BoxDecoration(
color: color.withOpacity(.1),
borderRadius: const BorderRadius.all(Radius.circular(5)),
),
child: Text(
'clear',
style: Theme.of(context)
.textTheme
.labelSmall
?.copyWith(color: color, fontWeight: FontWeight.bold),
),
),
),
const SizedBox(width: 10),
],
],
),
),
);
}
}
@@ -0,0 +1,142 @@
import '../../providers/toast.provider.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../classes/constents.class.dart';
import '../../classes/enums.class.dart';
class ToastModel {
final String? message;
final ToastType type;
final VoidCallback? callback;
ToastModel({
this.message,
this.callback,
this.type = ToastType.primary,
});
Color get color => getColor();
IconData get icon => getIcon();
Color getColor() {
switch (type) {
case ToastType.success:
return Colors.green;
case ToastType.danger:
return Colors.red;
case ToastType.warning:
return Colors.amber;
case ToastType.info:
return Colors.blue;
default:
return primaryColor;
}
}
IconData getIcon() {
switch (type) {
case ToastType.success:
return Icons.check_circle_rounded;
case ToastType.danger:
return Icons.cancel_rounded;
case ToastType.warning:
return Icons.warning_rounded;
case ToastType.info:
return Icons.info_rounded;
default:
return Icons.notifications_rounded;
}
}
}
class Toasts extends StatefulWidget {
const Toasts({super.key});
@override
State<Toasts> createState() => _ToastsState();
}
class _ToastsState extends State<Toasts> {
final listKey = GlobalKey<AnimatedListState>();
final provider = Get.find<ToastProvider>();
@override
void initState() {
provider.setKey(listKey);
super.initState();
}
final slideInTween =
Tween<Offset>(begin: const Offset(3, 0), end: Offset.zero);
@override
Widget build(BuildContext context) {
return AnimatedList(
shrinkWrap: true,
key: listKey,
itemBuilder: (cnx, i, animation) => provider.buildItem(provider.toasts[i], animation),
);
}
}
class Toast extends StatelessWidget {
const Toast(this.item, this.dismiss, {super.key});
final ToastModel item;
final void Function(ToastModel, DissmissType) dismiss;
@override
Widget build(BuildContext context) {
final color = item.color;
if (toastAutoDissmiss) {
Future.delayed(
toastDissmissDuration, () => dismiss(item, DissmissType.auto))
.catchError((e) {});
}
return Dismissible(
key: ValueKey(item.hashCode),
onDismissed: (_) => dismiss(item, DissmissType.swipe),
confirmDismiss: (direction) async {
return direction == DismissDirection.startToEnd;
},
child: Container(
margin: const EdgeInsets.symmetric(vertical: 10),
child: GestureDetector(
onTap: item.callback,
child: Container(
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(10)),
color: Colors.white,
border: Border.fromBorderSide(
BorderSide(color: color.withOpacity(.5), width: 2)),
),
child: Row(
children: [
Icon(
item.icon,
color: color,
),
const SizedBox(width: 10),
Expanded(
child: Text(item.message ?? ''),
),
const SizedBox(width: 10),
InkWell(
onTap: () => dismiss(item, DissmissType.click),
borderRadius: const BorderRadius.all(Radius.circular(5)),
splashColor: color.withOpacity(.1),
child: Icon(
Icons.close_rounded,
size: 20,
color: color,
),
),
],
),
),
),
),
);
}
}
@@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import '../child_builder.widget.dart';
class ResponsiveGridItem extends StatelessWidget {
const ResponsiveGridItem({
required this.child,
this.crossAxisCellCount = 3,
this.animationIndex = 0,
super.key,
});
final Widget child;
final int crossAxisCellCount;
final int animationIndex;
@override
Widget build(BuildContext context) {
return ChildBuilder(
child: AnimationConfiguration.staggeredGrid(
position: animationIndex,
duration: const Duration(milliseconds: 375),
columnCount: 6,
child: ScaleAnimation(
child: FadeInAnimation(
child: child,
),
),
),
builder: (context, child) {
return StaggeredGridTile.fit(
crossAxisCellCount: crossAxisCellCount,
child: child,
);
},
);
}
}
@@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import '../../classes/enums.class.dart';
typedef ResponsiveBuilder = Widget Function(
BuildContext context,
ScreenSize screen,
);
class ScreenSize {
final double width;
ScreenSize(this.width);
bool get is4K => width > 1600;
bool get isDesktop => width > 1024 && !is4K;
bool get isTablet => width > 768 && !isDesktop;
bool get isMobile => !isTablet && !isDesktop && !is4K;
bool get isSmall => isMobile || isTablet;
bool get isBig => isDesktop || is4K;
ScreenSizeType get screenType => calculateScreenType();
ScreenSizeType calculateScreenType() {
if (isDesktop) return ScreenSizeType.desktop;
if (isTablet) return ScreenSizeType.tablet;
return ScreenSizeType.mobile;
}
int gridAxis() {
if (is4K) return 24;
if (isDesktop) return 18;
if (isTablet) return 12;
return 6;
}
int setAxis(int? xl, int? lg, int? md, int? sm) {
xl ??= 24;
if (is4K) return xl;
if (isDesktop) return lg ?? xl;
if (isTablet) return md ?? lg ?? xl;
return sm ?? md ?? lg ?? xl;
}
}
class ResponsiveWidget extends StatelessWidget {
const ResponsiveWidget({
required this.builder,
super.key,
});
final ResponsiveBuilder builder;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
ScreenSize size = ScreenSize(constraints.maxWidth);
return builder(context, size);
},
);
}
}
@@ -0,0 +1,64 @@
import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:get/get.dart';
import '../../classes/constents.class.dart';
import 'responsive.widget.dart';
class ResponsiveGrid extends StatefulWidget {
const ResponsiveGrid({required this.childrenBuilder, super.key});
final List<Widget> Function(
BuildContext,
ScreenSize,
) childrenBuilder;
@override
State<ResponsiveGrid> createState() => _ResponsiveGridState();
}
class _ResponsiveGridState extends State<ResponsiveGrid> {
late final ScrollController controller;
@override
void initState() {
super.initState();
controller = ScrollController();
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ResponsiveWidget(
builder: (cnx, screen) {
return Scrollbar(
thickness: 10,
thumbVisibility: true,
controller: controller,
child: SingleChildScrollView(
controller: controller,
child: Container(
padding: EdgeInsets.symmetric(
horizontal: (Get.width - 304) > maxContainerWidth
? (((Get.width - 304) - maxContainerWidth) / 2)
: 20,
),
child: AnimationLimiter(
child: StaggeredGrid.count(
crossAxisCount: screen.gridAxis(),
children: widget.childrenBuilder(cnx, screen),
),
),
),
),
);
},
);
}
}
@@ -0,0 +1,145 @@
import '../classes/constents.class.dart';
import 'package:flutter/material.dart';
import 'package:vector_math/vector_math.dart' as math;
class RoundedProgressIndicator extends StatefulWidget {
const RoundedProgressIndicator({
this.textValue,
this.color = primaryColor,
this.backgroundColor = Colors.white,
this.value = 0.5,
this.size = 40,
this.stroke = 4,
super.key,
});
final Widget? textValue;
final Color color;
final Color backgroundColor;
final double value;
final double size;
final double stroke;
@override
State<RoundedProgressIndicator> createState() =>
_RoundedProgressIndicatorState();
}
class _RoundedProgressIndicatorState extends State<RoundedProgressIndicator>
with SingleTickerProviderStateMixin {
late Animation<double> _animation;
late AnimationController _animationController;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
);
_animation =
Tween<double>(begin: 0, end: widget.value).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.fastOutSlowIn,
));
_animationController.forward();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: widget.size,
width: widget.size,
child: widget.textValue is Widget
? Stack(
children: [
Positioned.fill(
child: AnimatedRoundedProgressIndicator(
backgroundColor: widget.backgroundColor,
color: widget.color,
stroke: widget.stroke,
animation: _animation,
),
),
Positioned.fill(
child: Center(child: widget.textValue as Widget),
),
],
)
: AnimatedRoundedProgressIndicator(
backgroundColor: widget.backgroundColor,
color: widget.color,
stroke: widget.stroke,
animation: _animation,
),
);
}
}
class AnimatedRoundedProgressIndicator extends AnimatedWidget {
const AnimatedRoundedProgressIndicator({
Key? key,
this.color = primaryColor,
this.backgroundColor = Colors.white,
this.stroke = 4,
required this.animation,
}) : super(key: key, listenable: animation);
final Animation<double> animation;
final Color color;
final Color backgroundColor;
final double stroke;
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: CartCircularprogress(
backColor: backgroundColor,
frontColor: color,
strokeWidth: stroke,
value: animation.value,
),
);
}
}
class CartCircularprogress extends CustomPainter {
const CartCircularprogress({
required this.backColor,
required this.frontColor,
required this.strokeWidth,
required this.value,
});
final Color backColor, frontColor;
final double strokeWidth, value;
@override
void paint(Canvas canvas, Size size) {
final w = size.width, h = size.height;
final paint1 = Paint()
..strokeWidth = strokeWidth
..color = backColor
..style = PaintingStyle.stroke;
final paint2 = Paint()
..strokeWidth = strokeWidth
..color = frontColor
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
final r =
Rect.fromCenter(center: Offset(w / 2, h / 2), width: w, height: h);
canvas.drawArc(r, math.radians(0), math.radians(360), false, paint1);
canvas.drawArc(
r, math.radians(270), math.radians(360 * value), false, paint2);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
@@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
class SimpleLoader extends StatelessWidget {
const SimpleLoader({
this.color,
this.strokeWidth = 4,
this.size = 40,
super.key,
});
final Color? color;
final double strokeWidth;
final double size;
@override
Widget build(BuildContext context) {
return SizedBox(
height: size,
width: size,
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation(color),
strokeWidth: strokeWidth,
),
);
}
}
+670
View File
@@ -0,0 +1,670 @@
// widgets/vigna_map_widget.dart
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';
import 'package:latlong2/latlong.dart';
import 'package:u_vineyard/entities/coordinates.dart';
import 'package:u_vineyard/entities/vineyard.dart';
import 'package:u_vineyard/template/src/providers/auth.provider.dart';
import 'package:u_vineyard/template/src/widgets/card.widget.dart';
// Definizione del tipo di callback per i tap sulla mappa
typedef MapTapCallback = void Function(TapPosition tapPosition, LatLng latLng);
class MainMapWidget extends StatefulWidget {
const MainMapWidget({super.key});
@override
State<MainMapWidget> createState() => _MainMapWidgetState();
}
class _MainMapWidgetState extends State<MainMapWidget> {
final MapController _mapController = MapController();
final Map<String, String> _mapUrlTemplates = {
'Standard': 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
'Terreno (OpenTopoMap)': 'https://opentopomap.org/{z}/{x}/{y}.png',
'Satellite (ESRI)': 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
};
final Map<String, Widget> _mapAttributions = {
'Standard': const Text('© OpenStreetMap contributors'),
'Terreno (OpenTopoMap)': const Text('Kartendaten: © OpenStreetMap-Mitwirkende, SRTM | Kartendarstellung: © OpenTopoMap (CC-BY-SA)'),
'Satellite (ESRI)': const Text('Tiles © Esri — Source: Esri, i-cubed, USDA FSA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'),
};
String? _selectedMapType = 'Standard';
double _currentZoom = 13.0; // Questo zoom verrà sovrascritto se ci sono aree
// Variabile per tenere traccia se la mappa è già stata centrata una volta
bool _mapCenteredInitially = false;
@override
void initState() {
super.initState();
// Non inizializzare qui _vineyardList o _areas
}
void _zoomIn() {
setState(() {
_currentZoom += 1;
_mapController.move(_mapController.camera.center, _currentZoom);
});
}
void _zoomOut() {
setState(() {
_currentZoom -= 1;
_mapController.move(_mapController.camera.center, _currentZoom);
});
}
// Funzione per calcolare i limiti e centrare la mappa
void _fitMapToPolygons(List<Map<int, Coordinates>> areas) {
if (areas.isEmpty) {
// Se non ci sono aree, centra sulla posizione predefinita
_mapController.move(const LatLng(45.4642, 9.1900), 10.0);
return;
}
// Raccogli tutti i punti da tutte le aree
List<LatLng> allPoints = [];
for (var areaMap in areas) {
for (var coord in areaMap.values) {
allPoints.add(LatLng(coord.latitude, coord.longitude));
}
}
if (allPoints.isEmpty) {
// Se per qualche motivo non ci sono punti validi, torna al default
_mapController.move(const LatLng(45.4642, 9.1900), 10.0);
return;
}
// Calcola i limiti (min/max latitudine e longitudine)
double? minLat, maxLat, minLon, maxLon;
for (var point in allPoints) {
if (minLat == null || point.latitude < minLat) minLat = point.latitude;
if (maxLat == null || point.latitude > maxLat) maxLat = point.latitude;
if (minLon == null || point.longitude < minLon) minLon = point.longitude;
if (maxLon == null || point.longitude > maxLon) maxLon = point.longitude;
}
if (minLat != null && maxLat != null && minLon != null && maxLon != null) {
// Crea un LatLngBounds
final LatLngBounds bounds = LatLngBounds(
LatLng(minLat, minLon),
LatLng(maxLat, maxLon),
);
// Adatta la camera per mostrare tutti i punti
_mapController.fitCamera(
CameraFit.bounds(
bounds: bounds,
padding: const EdgeInsets.all(50.0), // Aggiungi un po' di padding
),
);
// Aggiorna lo zoom corrente dopo il fit
_currentZoom = _mapController.camera.zoom;
}
}
@override
Widget build(BuildContext context) {
final AuthProvider authProvider = Get.find<AuthProvider>();
return CardWidget(
height: 700,
child: Obx(() {
final List<Vineyard> vineyardList = authProvider.profile?.owners?.expand((owner) => owner.vineyards ?? <Vineyard>[]).cast<Vineyard>().toList() ?? [];
final List<Map<int, Coordinates>> areas = vineyardList
.map((vineyard) => vineyard.area)
.whereType<Map<int, Coordinates>>()
.toList();
// Centra la mappa solo una volta o quando le aree cambiano significativamente
// Per semplicità, la centreremo solo la prima volta che le aree vengono caricate
// o se il flag _mapCenteredInitially è falso e ci sono aree.
if (areas.isNotEmpty && !_mapCenteredInitially) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_fitMapToPolygons(areas);
_mapCenteredInitially = true; // Imposta il flag a true dopo il primo centraggio
});
} else if (areas.isEmpty && _mapCenteredInitially) {
// Se le aree diventano vuote dopo essere state caricate, resetta il flag
_mapCenteredInitially = false;
}
// Logica per initialCenter della mappa (usata solo alla prima costruzione)
// Dopo il primo render, _mapController.move gestirà il centro e lo zoom
LatLng initialMapCenter;
if (vineyardList.isNotEmpty && vineyardList.first.coordinates != null) {
initialMapCenter = LatLng(vineyardList.first.coordinates!.latitude, vineyardList.first.coordinates!.longitude);
} else {
initialMapCenter = const LatLng(45.4642, 9.1900); // Centro di default (Milano)
}
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Vigne',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(color: Colors.green[800]),
),
DropdownButton<String>(
value: _selectedMapType,
items: const [
DropdownMenuItem(value: 'Standard', child: Text('Standard')),
DropdownMenuItem(value: 'Terreno (OpenTopoMap)', child: Text('Terreno')),
DropdownMenuItem(value: 'Satellite (ESRI)', child: Text('Satellite')),
],
onChanged: (String? newValue) {
setState(() {
_selectedMapType = newValue;
});
},
),
],
),
const SizedBox(height: 16),
Expanded(
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(8.0),
),
child: FlutterMap(
mapController: _mapController,
options: MapOptions(
initialCenter: initialMapCenter,
initialZoom: _currentZoom, // Usa _currentZoom che può essere aggiornato da _fitMapToPolygons
),
children: [
TileLayer(
urlTemplate: _mapUrlTemplates[_selectedMapType]!,
userAgentPackageName: 'com.example.vigna_app',
),
RichAttributionWidget(
attributions: [
TextSourceAttribution(
(_mapAttributions[_selectedMapType]! as Text).data!,
onTap: () => print('Attribution tapped: ${(_mapAttributions[_selectedMapType]! as Text).data}'),
),
],
),
MarkerLayer(
markers: vineyardList.map((vineyard) {
return Marker(
width: 40.0,
height: 40.0,
point: vineyard.coordinates != null
? LatLng(vineyard.coordinates!.latitude, vineyard.coordinates!.longitude)
: const LatLng(0, 0),
child: Tooltip(
message: vineyard.name,
child: const Icon(Icons.grass, color: Colors.green, size: 30.0),
),
);
}).toList(),
),
Positioned(
top: 40,
right: 10,
child: Column(
children: [
FloatingActionButton(
heroTag: 'zoomIn',
mini: true,
onPressed: _zoomIn,
child: const Icon(Icons.add),
),
const SizedBox(height: 8),
FloatingActionButton(
heroTag: 'zoomOut',
mini: true,
onPressed: _zoomOut,
child: const Icon(Icons.remove),
),
],
),
),
if (areas.isNotEmpty)
PolygonLayer(
polygons: areas.expand((areaMap) {
final List<LatLng> polygonPoints = areaMap.values
.map((coord) => LatLng(coord.latitude, coord.longitude))
.toList();
if (polygonPoints.isNotEmpty && polygonPoints.first != polygonPoints.last) {
polygonPoints.add(polygonPoints.first);
}
return [
if (polygonPoints.length >= 3)
Polygon(
points: polygonPoints,
color: Colors.blue.withOpacity(0.3),
borderStrokeWidth: 2,
borderColor: Colors.blue[900]!,
),
];
}).toList(),
),
],
),
),
),
],
);
}),
);
}
}
// class MainMapWidget extends StatefulWidget {
// const MainMapWidget({super.key});
// @override
// State<MainMapWidget> createState() => _MainMapWidgetState();
// }
// class _MainMapWidgetState extends State<MainMapWidget> {
// // Le variabili di stato devono essere qui
// final MapController _mapController = MapController();
// final Map<String, String> _mapUrlTemplates = {
// 'Standard': 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
// 'Terreno (OpenTopoMap)': 'https://opentopomap.org/{z}/{x}/{y}.png',
// 'Satellite (ESRI)': 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
// };
// final Map<String, Widget> _mapAttributions = {
// 'Standard': const Text('© OpenStreetMap contributors'),
// 'Terreno (OpenTopoMap)': const Text('Kartendaten: © OpenStreetMap-Mitwirkende, SRTM | Kartendarstellung: © OpenTopoMap (CC-BY-SA)'),
// 'Satellite (ESRI)': const Text('Tiles © Esri — Source: Esri, i-cubed, USDA FSA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'),
// };
// String? _selectedMapType = 'Standard'; // Tipo di mappa selezionato
// double _currentZoom = 13.0;
// List<Vineyard> _vineyardList = [];
// List<Map<int, Coordinates>> _areas = [];
// @override
// void initState() {
// super.initState();
// _vineyardList = Get.find<AuthProvider>().profile?.owners?.expand((owner) => owner.vineyards ?? <Vineyard>[]).cast<Vineyard>().toList() ?? [];
// _areas = _vineyardList
// .map((vineyard) => vineyard.area)
// .whereType<Map<int, Coordinates>>()
// .toList();
// }
// void _zoomIn() {
// setState(() {
// _currentZoom += 1;
// _mapController.move(_mapController.camera.center, _currentZoom);
// });
// }
// void _zoomOut() {
// setState(() {
// _currentZoom -= 1;
// _mapController.move(_mapController.camera.center, _currentZoom);
// });
// void _fitMapToPolygons(List<Map<int, Coordinates>> areas) {
// if (areas.isEmpty) {
// // Se non ci sono aree, centra sulla posizione predefinita
// _mapController.move(const LatLng(45.4642, 9.1900), 10.0);
// return;
// }
// // Raccogli tutti i punti da tutte le aree
// List<LatLng> allPoints = [];
// for (var areaMap in areas) {
// for (var coord in areaMap.values) {
// allPoints.add(LatLng(coord.latitude, coord.longitude));
// }
// }
// if (allPoints.isEmpty) {
// // Se per qualche motivo non ci sono punti validi, torna al default
// _mapController.move(const LatLng(45.4642, 9.1900), 10.0);
// return;
// }
// // Calcola i limiti (min/max latitudine e longitudine)
// double? minLat, maxLat, minLon, maxLon;
// for (var point in allPoints) {
// if (minLat == null || point.latitude < minLat) minLat = point.latitude;
// if (maxLat == null || point.latitude > maxLat) maxLat = point.latitude;
// if (minLon == null || point.longitude < minLon) minLon = point.longitude;
// if (maxLon == null || point.longitude > maxLon) maxLon = point.longitude;
// }
// if (minLat != null && maxLat != null && minLon != null && maxLon != null) {
// // Crea un LatLngBounds
// final LatLngBounds bounds = LatLngBounds(
// LatLng(minLat, minLon),
// LatLng(maxLat, maxLon),
// );
// // Adatta la camera per mostrare tutti i punti
// _mapController.fitCamera(
// CameraFit.bounds(
// bounds: bounds,
// padding: const EdgeInsets.all(50.0), // Aggiungi un po' di padding
// ),
// );
// // Aggiorna lo zoom corrente dopo il fit
// _currentZoom = _mapController.camera.zoom;
// }
// }
// }
// @override
// Widget build(BuildContext context) {
// // Puoi usare Obx per reagire ai cambiamenti in AuthProvider se il profilo può cambiare dinamicamente
// print( _areas);
// return CardWidget(
// height: 700,
// child: Column(
// children: [
// Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// Text(
// 'Vigne',
// style: Theme.of(context).textTheme.headlineSmall?.copyWith(color: Colors.green[800]),
// ),
// DropdownButton<String>(
// value: _selectedMapType,
// items: [
// DropdownMenuItem(value: 'Standard', child: const Text('Standard')),
// DropdownMenuItem(value: 'Terreno (OpenTopoMap)', child: const Text('Terreno')),
// DropdownMenuItem(value: 'Satellite (ESRI)', child: const Text('Satellite')),
// ],
// onChanged: (String? newValue) {
// // Chiamata a setState() per aggiornare l'UI
// setState(() {
// _selectedMapType = newValue; // newValue può essere null, ma il DropdownButton lo gestisce
// });
// },
// ),
// ],
// ),
// const SizedBox(height: 16),
// Expanded(
// child: Container(
// // Rimuovi height: 500 se Expanded è sufficiente per il layout
// decoration: BoxDecoration(
// border: Border.all(color: Colors.grey),
// borderRadius: BorderRadius.circular(8.0),
// ),
// child: FlutterMap(
// mapController: _mapController,
// options: MapOptions(
// initialCenter:initialMapCenter, // Centro iniziale (es. Milano)
// initialZoom: 10.0,
// ),
// children: [
// TileLayer(
// // Usa l'operatore '!' perché _selectedMapType è garantito non essere null qui
// // dato che ha un valore iniziale e il DropdownButton non permette null.
// urlTemplate: _mapUrlTemplates[_selectedMapType]!,
// userAgentPackageName: 'com.example.vigna_app',
// ),
// RichAttributionWidget(
// attributions: [
// TextSourceAttribution(
// (_mapAttributions[_selectedMapType]! as Text).data!,
// onTap: () => print('Attribution tapped: ${(_mapAttributions[_selectedMapType]! as Text).data}'),
// ),
// ],
// ),
// // MarkerLayer per i vigneti
// MarkerLayer(
// markers: _vineyardList.map((vineyard) {
// return Marker(
// width: 40.0,
// height: 40.0,
// point: vineyard.coordinates != null
// ? LatLng(vineyard.coordinates!.latitude, vineyard.coordinates!.longitude)
// : const LatLng(0, 0),
// child: Tooltip( // Aggiunto Tooltip per mostrare il nome del vigneto al tap
// message: vineyard.name,
// child: const Icon(Icons.grass, color: Colors.green, size: 30.0),
// ),
// );
// }).toList(),
// ),
// Positioned(
// top: 40,
// right: 10,
// child: Column(
// children: [
// FloatingActionButton(
// heroTag: 'zoomIn',
// mini: true,
// onPressed: _zoomIn,
// child: Icon(Icons.add),
// ),
// SizedBox(height: 8),
// FloatingActionButton(
// heroTag: 'zoomOut',
// mini: true,
// onPressed: _zoomOut,
// child: Icon(Icons.remove),
// ),
// ],
// ),
// ),
// if (_areas.isNotEmpty) // Mostra il layer solo se ci sono aree da disegnare
// PolygonLayer(
// polygons: _areas.expand((areaMap) {
// // Converte la Map<int, Coordinates> in List<LatLng>
// final List<LatLng> polygonPoints = areaMap.values
// .map((coord) => LatLng(coord.latitude, coord.longitude))
// .toList();
// // FlutterMap PolygonLayer richiede che il poligono sia chiuso.
// // Ovvero, il primo e l'ultimo punto devono essere uguali.
// // Aggiungiamo il primo punto alla fine se non è già così.
// if (polygonPoints.isNotEmpty && polygonPoints.first != polygonPoints.last) {
// polygonPoints.add(polygonPoints.first);
// }
// // Restituisci una lista di Polygon, uno per ogni area
// return [
// if (polygonPoints.length >= 3) // Un poligono valido ha almeno 3 punti
// Polygon(
// points: polygonPoints,
// color: Colors.blue.withOpacity(0.3), // Colore di riempimento dell'area
// borderStrokeWidth: 2, // Spessore del bordo
// borderColor: Colors.blue[900]!, // Colore del bordo
// ),
// ];
// }).toList(),
// ),
// ],
// ),
// ),
// ),
// // Questa SizedBox era posizionata male, l'ho rimossa o spostata se necessaria altrove.
// // Se volevi spazio alla destra della mappa, dovrebbe essere nel Row con Expanded.
// // const SizedBox(width: 16), // Rimosso o riposizionato se necessario
// ],
// ),
// );
// }
// }
class VignaMapWidget extends StatefulWidget {
final List<LatLng> polygonPoints;
final MapTapCallback onMapTap;
final VoidCallback onClearPolygon;
final double calculatedArea;
const VignaMapWidget({
super.key,
required this.polygonPoints,
required this.onMapTap,
required this.onClearPolygon,
required this.calculatedArea,
});
@override
State<VignaMapWidget> createState() => _VignaMapWidgetState();
}
class _VignaMapWidgetState extends State<VignaMapWidget> {
final MapController _mapController = MapController();
String _selectedMapType = 'Standard'; // Default: OpenStreetMap Standard
final Map<String, String> _mapUrlTemplates = {
'Standard': 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
'Terreno (OpenTopoMap)': 'https://opentopomap.org/{z}/{x}/{y}.png',
'Satellite (ESRI)': 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
};
final Map<String, Widget> _mapAttributions = {
'Standard': const Text('© OpenStreetMap contributors'),
'Terreno (OpenTopoMap)': const Text('Kartendaten: © OpenStreetMap-Mitwirkende, SRTM | Kartendarstellung: © OpenTopoMap (CC-BY-SA)'),
'Satellite (ESRI)': const Text('Tiles © Esri — Source: Esri, i-cubed, USDA FSA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'),
};
@override
Widget build(BuildContext context) {
return CardWidget(
height: 700,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Mappa Vigna (Disegna Poligono)',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(color: Colors.green[800]),
),
// Dropdown per cambiare il tipo di mappa
DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: _selectedMapType,
dropdownColor: Colors.green[700],
style: const TextStyle(color: Colors.white, fontSize: 16),
icon: const Icon(Icons.map, color: Colors.green), // Icona qui, non nell'AppBar
items: _mapUrlTemplates.keys.map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
onChanged: (String? newValue) {
setState(() {
_selectedMapType = newValue!;
});
},
),
),
],
),
const SizedBox(height: 32),
Container(
height: 300,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(8.0),
),
child: FlutterMap(
mapController: _mapController,
options: MapOptions(
initialCenter: const LatLng(45.4642, 9.1900), // Centro iniziale (es. Milano)
initialZoom: 10.0,
onTap: widget.onMapTap, // Usa la callback passata
),
children: [
TileLayer(
urlTemplate: _mapUrlTemplates[_selectedMapType]!,
userAgentPackageName: 'com.example.vigna_app',
),
RichAttributionWidget(
attributions: [
TextSourceAttribution(
(_mapAttributions[_selectedMapType]! as Text).data!,
onTap: () => print('Attribution tapped: ${(_mapAttributions[_selectedMapType]! as Text).data}'),
),
],
),
if (widget.polygonPoints.isNotEmpty)
PolygonLayer(
polygons: [
Polygon(
points: widget.polygonPoints,
color: Colors.green.withOpacity(0.5),
borderStrokeWidth: 3,
borderColor: Colors.green[900]!,
),
],
),
if (widget.polygonPoints.isNotEmpty)
MarkerLayer(
markers: widget.polygonPoints.map((point) {
return Marker(
width: 40.0,
height: 40.0,
point: point,
child: const Icon(
Icons.grass,
color: Colors.red,
size: 30.0,
),
);
}).toList(),
),
],
),
),
const SizedBox(height: 10),
Align(
alignment: Alignment.centerRight,
child: ElevatedButton.icon(
onPressed: widget.onClearPolygon, // Usa la callback passata
icon: const Icon(Icons.clear),
label: const Text('Resetta Poligono'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red[400],
foregroundColor: Colors.white,
),
),
),
if (widget.calculatedArea > 0)
Padding(
padding: const EdgeInsets.only(top: 10.0),
child: Text(
'Area Calcolata: ${widget.calculatedArea.toStringAsFixed(2)} ettari',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.green[800],
),
),
),
],
),
),
);
}
}