commit f8d5751182b93faeb8ba2accdc3b339a6c0eb42d
parent ac24f870c555ec997939b4eeb2a87aa2ca2eb7df
Author: ajcoder13 <avnijhalani@gmail.com>
Date: Sun, 30 Nov 2025 23:30:26 +0530
Canvas page made
Diffstat:
18 files changed, 1690 insertions(+), 11 deletions(-)
diff --git a/assets/icons/arrow-go-back-line.svg b/assets/icons/arrow-go-back-line.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M4.85701 5.83312L6.96996 7.94608L5.79145 9.12458L1.66666 4.99979L5.79145 0.875L6.96996 2.05352L4.85701 4.16646H10.8333C14.5152 4.16646 17.5 7.15123 17.5 10.8332C17.5 14.515 14.5152 17.4998 10.8333 17.4998H3.33332V15.8332H10.8333C13.5947 15.8332 15.8333 13.5946 15.8333 10.8332C15.8333 8.0717 13.5947 5.83312 10.8333 5.83312H4.85701Z" fill="#27272A"/>
+</svg>
diff --git a/assets/icons/arrow-go-forward-line.svg b/assets/icons/arrow-go-forward-line.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M15.143 5.83312H9.16667C6.40524 5.83312 4.16667 8.0717 4.16667 10.8332C4.16667 13.5946 6.40524 15.8332 9.16667 15.8332H16.6667V17.4998H9.16667C5.48477 17.4998 2.5 14.515 2.5 10.8332C2.5 7.15123 5.48477 4.16646 9.16667 4.16646H15.143L13.03 2.05352L14.2086 0.875L18.3333 4.99979L14.2086 9.12458L13.03 7.94608L15.143 5.83312Z" fill="#9F9FA9"/>
+</svg>
diff --git a/assets/icons/arrow-left-s-line.svg b/assets/icons/arrow-left-s-line.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M9.02366 10.0006L13.1485 14.1253L11.97 15.3038L6.66666 10.0006L11.97 4.69727L13.1485 5.87577L9.02366 10.0006Z" fill="#27272A"/>
+</svg>
diff --git a/assets/icons/brush-line.svg b/assets/icons/brush-line.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M12.8805 8.06267L12.762 7.94428C12.2218 7.42344 11.5458 7.02822 10.7696 6.82024C8.34737 6.1712 6.02122 7.54408 5.35992 10.0121C5.32422 10.1453 5.30482 10.2949 5.27379 10.6711C5.14523 12.2295 4.77878 13.3916 3.77387 14.5703C5.6571 15.3107 7.88814 15.8323 9.58471 15.8323C11.6419 15.8323 13.4704 14.4496 14.0105 12.4336C14.4422 10.8226 13.9595 9.18189 12.8805 8.06267ZM11.0739 5.17765L15.1899 1.97632C15.5216 1.71831 15.9935 1.74769 16.2907 2.04487L18.7859 4.54003C19.0831 4.83721 19.1125 5.30916 18.8545 5.6409L15.654 9.75581C15.8982 10.7461 15.903 11.8102 15.6204 12.8651C14.872 15.6582 12.3452 17.499 9.58471 17.499C6.66671 17.499 2.91671 16.2474 0.833374 14.9974C4.14986 12.4974 3.37272 10.9889 3.75004 9.58073C4.63207 6.28896 7.78524 4.36627 11.0739 5.17765ZM13.918 6.74315C13.9728 6.79602 14.0266 6.84984 14.0794 6.90458L15.0248 7.85001L17.0872 5.19836L15.6325 3.74356L12.9808 5.80596L13.918 6.74315Z" fill="#27272A"/>
+</svg>
diff --git a/assets/icons/eraser-line.svg b/assets/icons/eraser-line.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M7.1547 7.37892L3.02991 11.5037L7.35851 15.8323L8.33321 15.8317V15.8307H9.3095L12.458 12.6822L7.1547 7.37892ZM8.33321 6.20041L13.6365 11.5037L15.9935 9.14668L10.6903 3.84339L8.33321 6.20041ZM11.6666 15.8307H17.4999V17.4973H9.99992L6.66857 17.4994L1.26214 12.0929C0.936708 11.7675 0.936708 11.2399 1.26214 10.9144L10.101 2.07562C10.4264 1.75018 10.9541 1.75018 11.2795 2.07562L17.7613 8.55743C18.0868 8.88285 18.0868 9.41052 17.7613 9.73593L11.6666 15.8307Z" fill="#9F9FA9"/>
+</svg>
diff --git a/assets/icons/file-image-line.svg b/assets/icons/file-image-line.svg
@@ -0,0 +1,10 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_1437_1511)">
+<path d="M11.6667 2.5V4.16667H3.33335V15.3209L4.80237 14.1667H16.6667V8.33333H18.3334V15C18.3334 15.4602 17.9603 15.8333 17.5 15.8333H5.37881L1.66669 18.75V3.33333C1.66669 2.8731 2.03979 2.5 2.50002 2.5H11.6667ZM15.8334 2.5V0H17.5V2.5H20V4.16667H17.5V6.66667H15.8334V4.16667H13.3334V2.5H15.8334Z" fill="#27272A"/>
+</g>
+<defs>
+<clipPath id="clip0_1437_1511">
+<rect width="20" height="20" fill="white"/>
+</clipPath>
+</defs>
+</svg>
diff --git a/assets/icons/magic_draw.svg b/assets/icons/magic_draw.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M3.92783 5.94009L3.72235 6.41149C3.57197 6.7566 3.09464 6.7566 2.94427 6.41149L2.7388 5.94009C2.37255 5.09956 1.71287 4.43034 0.889725 4.06423L0.256706 3.78268C-0.0855685 3.63044 -0.0855685 3.13234 0.256706 2.9801L0.854333 2.71428C1.69865 2.33876 2.37014 1.64477 2.7301 0.77569L2.9411 0.266278C3.08815 -0.0887594 3.57847 -0.0887594 3.72552 0.266278L3.93652 0.77569C4.29647 1.64477 4.96798 2.33876 5.81233 2.71428L6.4099 2.9801C6.75226 3.13234 6.75226 3.63044 6.4099 3.78268L5.7769 4.06423C4.95376 4.43034 4.29407 5.09956 3.92783 5.94009ZM12.762 7.94404L12.8804 8.06243C13.9594 9.18167 14.4422 10.8223 14.0105 12.4334C13.4703 14.4494 11.6418 15.832 9.58467 15.832C7.8881 15.832 5.65706 15.3105 3.77383 14.5701C4.77874 13.3913 5.14519 12.2292 5.27375 10.6708C5.30478 10.2947 5.32417 10.1451 5.35987 10.0118C6.02117 7.54384 8.34733 6.17096 10.7696 6.81999C11.5457 7.02797 12.2217 7.42319 12.762 7.94404ZM15.1898 1.97608L11.0738 5.17741C7.7852 4.36602 4.63202 6.28872 3.75 9.5805C3.66522 9.89692 3.63872 10.2183 3.61097 10.555C3.51524 11.7166 3.40462 13.0589 0.833333 14.9972C2.91667 16.2472 6.66667 17.4987 9.58467 17.4987C12.3452 17.4987 14.8719 15.658 15.6203 12.8648C15.903 11.8099 15.8982 10.7458 15.6539 9.75558L18.8544 5.64066C19.1124 5.30892 19.0831 4.83696 18.7858 4.53978L16.2907 2.04462C15.9935 1.74745 15.5216 1.71807 15.1898 1.97608ZM14.0793 6.90433C14.0266 6.8496 13.9727 6.79578 13.9179 6.74291L12.9807 5.80571L15.6324 3.74332L17.0872 5.19812L15.0247 7.84977L14.0793 6.90433Z" fill="#9F9FA9"/>
+</svg>
diff --git a/assets/icons/media.svg b/assets/icons/media.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M12.5 6.6665V3.33317H4.16667V16.6665H15.8333V6.6665H12.5ZM2.5 2.493C2.5 2.03655 2.87291 1.6665 3.33208 1.6665H13.3333L17.4998 5.83317L17.5 17.4936C17.5 17.9573 17.1292 18.3332 16.6722 18.3332H3.32783C2.87063 18.3332 2.5 17.9538 2.5 17.5067V2.493ZM9.16667 7.9165C9.16667 8.60684 8.607 9.1665 7.91667 9.1665C7.22631 9.1665 6.66667 8.60684 6.66667 7.9165C6.66667 7.22615 7.22631 6.6665 7.91667 6.6665C8.607 6.6665 9.16667 7.22615 9.16667 7.9165ZM14.5833 14.1665L11.25 8.33317L6.66667 14.1665H14.5833Z" fill="#9F9FA9"/>
+</svg>
diff --git a/assets/icons/plugins.svg b/assets/icons/plugins.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M2.08331 5.8335C2.08331 7.90456 3.76225 9.5835 5.83331 9.5835C7.90438 9.5835 9.58331 7.90456 9.58331 5.8335C9.58331 3.76243 7.90438 2.0835 5.83331 2.0835C3.76225 2.0835 2.08331 3.76243 2.08331 5.8335ZM2.08331 14.1668C2.08331 16.2379 3.76225 17.9168 5.83331 17.9168C7.90438 17.9168 9.58331 16.2379 9.58331 14.1668C9.58331 12.0957 7.90438 10.4168 5.83331 10.4168C3.76225 10.4168 2.08331 12.0957 2.08331 14.1668ZM10.4166 14.1668C10.4166 16.2379 12.0956 17.9168 14.1666 17.9168C16.2377 17.9168 17.9166 16.2379 17.9166 14.1668C17.9166 12.0957 16.2377 10.4168 14.1666 10.4168C12.0956 10.4168 10.4166 12.0957 10.4166 14.1668ZM7.91665 5.8335C7.91665 6.98409 6.9839 7.91683 5.83331 7.91683C4.68272 7.91683 3.74998 6.98409 3.74998 5.8335C3.74998 4.68291 4.68272 3.75016 5.83331 3.75016C6.9839 3.75016 7.91665 4.68291 7.91665 5.8335ZM7.91665 14.1668C7.91665 15.3174 6.9839 16.2502 5.83331 16.2502C4.68272 16.2502 3.74998 15.3174 3.74998 14.1668C3.74998 13.0162 4.68272 12.0835 5.83331 12.0835C6.9839 12.0835 7.91665 13.0162 7.91665 14.1668ZM16.25 14.1668C16.25 15.3174 15.3172 16.2502 14.1666 16.2502C13.0161 16.2502 12.0833 15.3174 12.0833 14.1668C12.0833 13.0162 13.0161 12.0835 14.1666 12.0835C15.3172 12.0835 16.25 13.0162 16.25 14.1668ZM14.6043 9.296L14.8355 8.76567C15.2475 7.82015 15.9896 7.06732 16.9156 6.65548L17.6278 6.33876C18.0129 6.1675 18.0129 5.60716 17.6278 5.43591L16.9556 5.13688C16.0056 4.71444 15.2501 3.93376 14.8452 2.9561L14.6079 2.38304C14.4424 1.98365 13.8908 1.98365 13.7254 2.38304L13.488 2.9561C13.0831 3.93376 12.3276 4.71444 11.3778 5.13688L10.7055 5.43591C10.3204 5.60716 10.3204 6.1675 10.7055 6.33876L11.4176 6.65548C12.3436 7.06732 13.0857 7.82015 13.4978 8.76567L13.729 9.296C13.8981 9.68425 14.4351 9.68425 14.6043 9.296Z" fill="#9F9FA9"/>
+</svg>
diff --git a/assets/icons/settings-line.svg b/assets/icons/settings-line.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M9.99998 0.833496L17.9166 5.41683V14.5835L9.99998 19.1668L2.08331 14.5835V5.41683L9.99998 0.833496ZM9.99998 2.75933L3.74998 6.37775V13.6226L9.99998 17.241L16.25 13.6226V6.37775L9.99998 2.75933ZM9.99998 13.3335C8.15903 13.3335 6.66665 11.8411 6.66665 10.0002C6.66665 8.15921 8.15903 6.66683 9.99998 6.66683C11.8409 6.66683 13.3333 8.15921 13.3333 10.0002C13.3333 11.8411 11.8409 13.3335 9.99998 13.3335ZM9.99998 11.6668C10.9205 11.6668 11.6666 10.9207 11.6666 10.0002C11.6666 9.07966 10.9205 8.3335 9.99998 8.3335C9.07948 8.3335 8.33331 9.07966 8.33331 10.0002C8.33331 10.9207 9.07948 11.6668 9.99998 11.6668Z" fill="#27272A"/>
+</svg>
diff --git a/assets/icons/stylesheet.svg b/assets/icons/stylesheet.svg
@@ -0,0 +1,4 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M7.06236 2.938C5.92338 1.79901 4.07705 1.7991 2.93801 2.938C1.79898 4.07702 1.79898 5.92331 2.93801 7.06235L3.08043 7.20557C3.24301 7.36826 3.24298 7.63206 3.08043 7.79476L2.93801 7.938C1.79898 9.077 1.79898 10.9233 2.93801 12.0623L3.08043 12.2056C3.24301 12.3682 3.24298 12.6321 3.08043 12.7947L2.93801 12.938C1.79898 14.077 1.79898 15.9233 2.93801 17.0623C4.07705 18.2012 5.92336 18.2013 7.06236 17.0623L7.20559 16.9199C7.36832 16.7573 7.63211 16.7572 7.79478 16.9199L7.93801 17.0623C9.07708 18.2012 10.9233 18.2013 12.0623 17.0623L12.2056 16.9199C12.3683 16.7573 12.6321 16.7572 12.7948 16.9199L12.938 17.0623C14.0771 18.2012 15.9233 18.2013 17.0623 17.0623C18.2012 15.9233 18.2012 14.077 17.0623 12.938L16.9199 12.7947C16.7572 12.6321 16.7572 12.3683 16.9199 12.2056L17.0623 12.0623C18.1311 10.9935 18.1967 9.30233 17.2601 8.1561L15.9703 9.21075C16.3708 9.701 16.3414 10.4264 15.884 10.884L15.7408 11.0272C14.9275 11.8407 14.9275 13.1597 15.7408 13.9732L15.884 14.1164C16.372 14.6045 16.372 15.3958 15.884 15.884C15.3958 16.3721 14.6046 16.372 14.1164 15.884L13.9732 15.7407C13.1597 14.9275 11.8408 14.9276 11.0272 15.7407L10.884 15.884C10.3958 16.3721 9.60458 16.372 9.11642 15.884L8.97317 15.7407C8.15968 14.9275 6.84074 14.9276 6.02721 15.7407L5.88398 15.884C5.39585 16.3721 4.60456 16.372 4.1164 15.884C3.62824 15.3958 3.62824 14.6045 4.1164 14.1164L4.25963 13.9732C5.07273 13.1597 5.07275 11.8407 4.25963 11.0272L4.1164 10.884C3.62824 10.3958 3.62824 9.6045 4.1164 9.11641L4.25963 8.97316C5.07273 8.15964 5.07275 6.84068 4.25963 6.02719L4.1164 5.88396C3.62824 5.39581 3.62824 4.60454 4.1164 4.11638C4.60457 3.62835 5.39587 3.62827 5.88398 4.11638L6.02721 4.25961C6.84074 5.07277 8.15967 5.07284 8.97317 4.25961L9.11642 4.11638C9.574 3.65895 10.2993 3.62954 10.7896 4.03011L11.8443 2.74024C10.698 1.8036 9.00683 1.86928 7.93801 2.938L7.79478 3.08041C7.63209 3.2431 7.36832 3.24304 7.20559 3.08041L7.06236 2.938ZM16.2257 1.09961C16.0787 0.744734 15.5885 0.744847 15.4412 1.09961L15.2297 1.60906C14.8698 2.478 14.1988 3.17196 13.3547 3.54753L12.7565 3.81365C12.4144 3.96591 12.4145 4.46374 12.7565 4.61606L13.3897 4.89763C14.2128 5.2637 14.8723 5.93302 15.2387 6.77345L15.4445 7.24464C15.5949 7.58975 16.0722 7.58975 16.2225 7.24464L16.4276 6.77345C16.7938 5.93303 17.4535 5.26375 18.2766 4.89763L18.9097 4.61606C19.252 4.46381 19.252 3.96589 18.9097 3.81365L18.3123 3.54753C17.4681 3.172 16.7965 2.47811 16.4366 1.60906L16.2257 1.09961Z" fill="#9F9FA9"/>
+<path d="M12.5 9.7533L10 7.5L7.5 9.7533L8.33334 10.5044L9.41075 9.53267V12.5H10.5893V9.53374L11.6667 10.5044L12.5 9.7533Z" fill="#9F9FA9"/>
+</svg>
diff --git a/assets/icons/text.svg b/assets/icons/text.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M4.16667 4.16667V15.8333H15.8333V4.16667H4.16667ZM3.33333 2.5H16.6667C17.1269 2.5 17.5 2.8731 17.5 3.33333V16.6667C17.5 17.1269 17.1269 17.5 16.6667 17.5H3.33333C2.8731 17.5 2.5 17.1269 2.5 16.6667V3.33333C2.5 2.8731 2.8731 2.5 3.33333 2.5ZM10.8333 8.33333V14.1667H9.16667V8.33333H5.83333V6.66667H14.1667V8.33333H10.8333Z" fill="#9F9FA9"/>
+</svg>
diff --git a/assets/icons/tools.svg b/assets/icons/tools.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M5.87525 11.7679L4.10748 13.5357L6.4645 15.8927L15.8926 6.46461L13.5356 4.10758L11.7678 5.87535L12.9463 7.05386L11.7678 8.23237L10.5893 7.05386L9.41081 8.23237L10.5893 9.41088L9.41081 10.5894L8.23227 9.41088L7.05376 10.5894L8.23227 11.7679L7.05376 12.9464L5.87525 11.7679ZM14.1248 2.33981L17.6604 5.87535C17.9858 6.20079 17.9858 6.72842 17.6604 7.05386L7.05376 17.6605C6.72832 17.9859 6.20069 17.9859 5.87525 17.6605L2.33971 14.125C2.01428 13.7995 2.01428 13.2719 2.33971 12.9464L12.9463 2.33981C13.2717 2.01438 13.7994 2.01438 14.1248 2.33981ZM11.7678 15.303L12.9463 14.1245L14.8151 15.9933H15.9936V14.8148L14.1248 12.946L15.3033 11.7675L17.4997 13.9639V17.5H13.9648L11.7678 15.303ZM4.69662 8.23185L2.3396 5.87482C2.01416 5.54939 2.01416 5.02175 2.3396 4.69631L4.69662 2.33929C5.02205 2.01386 5.5497 2.01386 5.87513 2.33929L8.23215 4.69631L7.05365 5.87482L5.28588 4.10706L4.10736 5.28557L5.87513 7.05334L4.69662 8.23185Z" fill="#9F9FA9"/>
+</svg>
diff --git a/assets/icons/upload-2-line.svg b/assets/icons/upload-2-line.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M3.33335 15.8333H16.6667V10H18.3334V16.6667C18.3334 17.1269 17.9603 17.5 17.5 17.5H2.50002C2.03979 17.5 1.66669 17.1269 1.66669 16.6667V10H3.33335V15.8333ZM10.8334 7.5V13.3333H9.16669V7.5H5.00002L10 2.5L15 7.5H10.8334Z" fill="#27272A"/>
+</svg>
diff --git a/lib/ui/pages/canvas_board_page.dart b/lib/ui/pages/canvas_board_page.dart
@@ -0,0 +1,829 @@
+import 'dart:io';
+import 'dart:ui' as ui;
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:image_picker/image_picker.dart';
+import 'package:path_provider/path_provider.dart';
+import 'package:undo/undo.dart';
+import './canvas_toolbar/magic_draw_overlay.dart';
+
+// --- MODELS ---
+class DrawingPoint {
+ final Offset offset;
+ final Paint paint;
+ const DrawingPoint({required this.offset, required this.paint});
+}
+
+class DrawingPath {
+ final List<DrawingPoint> points;
+ final Color color;
+ final double strokeWidth;
+ final bool isEraser;
+ DrawingPath({
+ required this.points,
+ required this.color,
+ required this.strokeWidth,
+ required this.isEraser,
+ });
+}
+
+class CanvasBoardPage extends StatefulWidget {
+ final String projectId;
+ final double width;
+ final double height;
+ final File? initialImage;
+ const CanvasBoardPage({
+ super.key,
+ required this.projectId,
+ required this.width,
+ required this.height,
+ this.initialImage,
+ });
+
+ @override
+ State<CanvasBoardPage> createState() => _CanvasBoardPageState();
+}
+
+class _CanvasBoardPageState extends State<CanvasBoardPage> {
+ // --- EXISTING STATE ---
+ final ChangeStack _changeStack = ChangeStack();
+ final ImagePicker _picker = ImagePicker();
+ List<Map<String, dynamic>> elements = [];
+ String? selectedId;
+ Offset? _dragStartPosition;
+ late Size _canvasSize;
+
+ // --- NEW MAGIC DRAW STATE ---
+ bool _isMagicDrawActive = false;
+
+ // Drawing Data
+ List<DrawingPath> _paths = [];
+ List<DrawingPoint> _currentPoints = [];
+ Color _selectedColor = const Color(0xFFFF4081);
+ double _strokeWidth = 10.0;
+ bool _isEraser = false;
+
+ // Key to capture the drawing
+ final GlobalKey _drawingKey = GlobalKey();
+
+ @override
+ void initState() {
+ super.initState();
+ _canvasSize = Size(widget.width, widget.height);
+
+ if (widget.initialImage != null) {
+ elements.add({
+ 'id': 'bg_${DateTime.now().millisecondsSinceEpoch}',
+ 'type': 'file_image',
+ 'content': widget.initialImage!.path,
+ 'position': const Offset(0, 0),
+ 'size': Size(widget.width, widget.height),
+ 'rotation': 0.0,
+ });
+ }
+ }
+
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: const Color(0xFFF5F5F5),
+ appBar: !_isMagicDrawActive ? _buildAppBar() : null,
+ body: Stack(
+ children: [
+ // -------------------------------------------
+ // LAYER 1: INTERACTIVE CANVAS + DRAWING LAYER
+ // -------------------------------------------
+ InteractiveViewer(
+ // Disable panning/zooming when drawing
+ scaleEnabled: !_isMagicDrawActive,
+ panEnabled: !_isMagicDrawActive,
+ boundaryMargin: const EdgeInsets.all(double.infinity),
+ minScale: 0.1,
+ maxScale: 5.0,
+ transformationController: TransformationController(
+ Matrix4.identity()
+ ..scale(0.85)
+ ..translate(40.0, 40.0),
+ ),
+ child: SizedBox(
+ width: 3000,
+ height: 3000,
+ child: Stack(
+ alignment: Alignment.center,
+ children: [
+ // THE WHITE BOARD CONTAINER
+ Container(
+ width: _canvasSize.width,
+ height: _canvasSize.height,
+ decoration: BoxDecoration(
+ color: Colors.white,
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withOpacity(0.08),
+ blurRadius: 24,
+ offset: const Offset(0, 4),
+ ),
+ ],
+ ),
+ child: ClipRect(
+ child: Stack(
+ children: [
+ // 1. Existing Images/Elements
+ ...elements.map((e) => _buildCanvasElement(e)),
+
+ // 2. THE DRAWING LAYER (Only visible when active)
+ // Positioned exactly on top, clipped to the board
+ if (_isMagicDrawActive)
+ RepaintBoundary(
+ key: _drawingKey,
+ child: GestureDetector(
+ onPanStart: _onPanStart,
+ onPanUpdate: _onPanUpdate,
+ onPanEnd: _onPanEnd,
+ child: CustomPaint(
+ size: Size.infinite,
+ painter: CanvasPainter(
+ paths: _paths,
+ currentPoints: _currentPoints,
+ currentColor:
+ _isEraser
+ ? Colors.transparent
+ : _selectedColor,
+ currentWidth: _strokeWidth,
+ isEraser: _isEraser,
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ // -------------------------------------------
+ // LAYER 2: MAGIC DRAW TOOLS (Floating UI)
+ // -------------------------------------------
+ MagicDrawTools(
+ isActive: _isMagicDrawActive,
+ selectedColor: _selectedColor,
+ strokeWidth: _strokeWidth,
+ isEraser: _isEraser,
+ onClose: _saveAndCloseMagicDraw, // This saves the drawing!
+ onColorChanged: (c) => setState(() => _selectedColor = c),
+ onWidthChanged: (w) => setState(() => _strokeWidth = w),
+ onEraserToggle: (e) => setState(() => _isEraser = e),
+ ),
+
+ // -------------------------------------------
+ // LAYER 3: BOTTOM NAVIGATION
+ // -------------------------------------------
+ Positioned(
+ bottom: 0,
+ left: 0,
+ right: 0,
+ child: CanvasBottomBar(
+ activeItem: _isMagicDrawActive ? "Magic Draw" : null,
+ onMagicDraw:
+ () =>
+ setState(() => _isMagicDrawActive = !_isMagicDrawActive),
+ onMedia: _pickImageFromGallery,
+ onStylesheet: () => _showComingSoon('Stylesheet'),
+ onTools: () => _showComingSoon('Tools'),
+ onText: () => _showComingSoon('Text'),
+ onSelect: () => _showComingSoon('Select'),
+ onPlugins: () => _showComingSoon('Plugins'),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ // --- DRAWING LOGIC ---
+
+ void _onPanStart(DragStartDetails details) {
+ // No logic needed here for now
+ }
+
+ void _onPanUpdate(DragUpdateDetails details) {
+ setState(() {
+ _currentPoints.add(
+ DrawingPoint(
+ offset:
+ details
+ .localPosition, // Uses local coordinates relative to White Board
+ paint: Paint(),
+ ),
+ );
+ });
+ }
+
+ void _onPanEnd(DragEndDetails details) {
+ setState(() {
+ if (_currentPoints.isNotEmpty) {
+ _paths.add(
+ DrawingPath(
+ points: List.from(_currentPoints),
+ color: _selectedColor,
+ strokeWidth: _strokeWidth,
+ isEraser: _isEraser,
+ ),
+ );
+ _currentPoints = [];
+ }
+ });
+ }
+
+ // --- SAVE & CLOSE LOGIC ---
+
+ Future<void> _saveAndCloseMagicDraw() async {
+ // 1. If nothing was drawn, just close
+ if (_paths.isEmpty && _currentPoints.isEmpty) {
+ setState(() => _isMagicDrawActive = false);
+ return;
+ }
+
+ try {
+ // 2. Capture the Drawing Layer as an Image
+ RenderRepaintBoundary boundary =
+ _drawingKey.currentContext!.findRenderObject()
+ as RenderRepaintBoundary;
+ ui.Image image = await boundary.toImage(pixelRatio: 3.0); // High res
+ ByteData? byteData = await image.toByteData(
+ format: ui.ImageByteFormat.png,
+ );
+ Uint8List pngBytes = byteData!.buffer.asUint8List();
+
+ // 3. Save to File
+ final directory = await getApplicationDocumentsDirectory();
+ final String filePath =
+ '${directory.path}/drawing_${DateTime.now().millisecondsSinceEpoch}.png';
+ File imgFile = File(filePath);
+ await imgFile.writeAsBytes(pngBytes);
+
+ // 4. Add as a new Element to the Board
+ final oldList = _deepCopy(elements);
+ setState(() {
+ elements.add({
+ 'id': 'drawing_${DateTime.now().millisecondsSinceEpoch}',
+ 'type': 'file_image',
+ 'content': filePath,
+ // Position it over the whole canvas since that's where we drew it
+ 'position': const Offset(0, 0),
+ 'size': _canvasSize,
+ 'rotation': 0.0,
+ });
+
+ // 5. Clear the drawing paths and close mode
+ _paths.clear();
+ _currentPoints.clear();
+ _isMagicDrawActive = false;
+ });
+
+ // Add to Undo Stack
+ _changeStack.add(
+ Change(
+ oldList,
+ () => setState(() => elements = _deepCopy(elements)),
+ (val) => setState(() => elements = val),
+ ),
+ );
+ } catch (e) {
+ debugPrint("Error saving drawing: $e");
+ setState(() => _isMagicDrawActive = false);
+ }
+ }
+
+ // --- EXISTING LOGIC ---
+
+ PreferredSizeWidget _buildAppBar() {
+ return AppBar(
+ leadingWidth: 140,
+ leading: SafeArea(
+ child: Row(
+ children: [
+ IconButton(
+ icon: SvgPicture.asset(
+ 'assets/icons/arrow-left-s-line.svg',
+ width: 22,
+ ),
+ onPressed: () => Navigator.of(context).maybePop(),
+ ),
+ IconButton(
+ icon: SvgPicture.asset(
+ 'assets/icons/arrow-go-back-line.svg',
+ width: 22,
+ colorFilter: ColorFilter.mode(
+ _changeStack.canUndo ? Colors.black : Colors.grey[400]!,
+ BlendMode.srcIn,
+ ),
+ ),
+ onPressed:
+ _changeStack.canUndo
+ ? () => setState(() => _changeStack.undo())
+ : null,
+ ),
+ IconButton(
+ icon: SvgPicture.asset(
+ 'assets/icons/arrow-go-forward-line.svg',
+ width: 22,
+ colorFilter: ColorFilter.mode(
+ _changeStack.canRedo ? Colors.black : Colors.grey[400]!,
+ BlendMode.srcIn,
+ ),
+ ),
+ onPressed:
+ _changeStack.canRedo
+ ? () => setState(() => _changeStack.redo())
+ : null,
+ ),
+ ],
+ ),
+ ),
+ backgroundColor: Colors.white,
+ foregroundColor: Colors.black,
+ elevation: 0,
+ actions: [
+ SafeArea(
+ child: Row(
+ children: [
+ IconButton(
+ icon: SvgPicture.asset(
+ 'assets/icons/file-image-line.svg',
+ width: 22,
+ ),
+ onPressed: _openLayers,
+ ),
+ IconButton(
+ icon: SvgPicture.asset(
+ 'assets/icons/upload-2-line.svg',
+ width: 22,
+ ),
+ onPressed: _exportProject,
+ ),
+ IconButton(
+ icon: SvgPicture.asset(
+ 'assets/icons/settings-line.svg',
+ width: 22,
+ ),
+ onPressed: _openSettings,
+ ),
+ const SizedBox(width: 8),
+ ],
+ ),
+ ),
+ ],
+ );
+ }
+
+ Widget _buildCanvasElement(Map<String, dynamic> e) {
+ final isSelected = selectedId == e['id'];
+ final position = e['position'] as Offset;
+ final size = e['size'] as Size;
+ final rotation = (e['rotation'] ?? 0.0) as double;
+
+ return Positioned(
+ left: position.dx,
+ top: position.dy,
+ child: Transform.rotate(
+ angle: rotation,
+ child: GestureDetector(
+ onTap: () {
+ if (!_isMagicDrawActive) setState(() => selectedId = e['id']);
+ },
+ child: Stack(
+ children: [
+ // Main Image Container
+ Container(
+ width: size.width,
+ height: size.height,
+ decoration: BoxDecoration(
+ border:
+ isSelected
+ ? Border.all(color: Colors.blue, width: 2)
+ : null,
+ ),
+ child:
+ e['type'] == 'file_image'
+ ? Image.file(File(e['content']), fit: BoxFit.contain)
+ : Container(),
+ ),
+
+ // Transform Controls (only when selected)
+ if (isSelected && !_isMagicDrawActive) ...[
+ // Resize Handle - Bottom Right
+ Positioned(
+ right: -8,
+ bottom: -8,
+ child: GestureDetector(
+ onPanUpdate: (details) {
+ setState(() {
+ final newWidth = (size.width + details.delta.dx).clamp(
+ 50.0,
+ _canvasSize.width,
+ );
+ final newHeight = (size.height + details.delta.dy)
+ .clamp(50.0, _canvasSize.height);
+ e['size'] = Size(newWidth, newHeight);
+ });
+ },
+ child: Container(
+ width: 24,
+ height: 24,
+ decoration: BoxDecoration(
+ color: Colors.blue,
+ shape: BoxShape.circle,
+ border: Border.all(color: Colors.white, width: 2),
+ ),
+ child: const Icon(
+ Icons.zoom_out_map,
+ size: 12,
+ color: Colors.white,
+ ),
+ ),
+ ),
+ ),
+
+ // Rotate Handle - Top Right
+ Positioned(
+ right: -8,
+ top: -8,
+ child: GestureDetector(
+ onPanUpdate: (details) {
+ setState(() {
+ final center = Offset(
+ position.dx + size.width / 2,
+ position.dy + size.height / 2,
+ );
+ final angle =
+ (details.globalPosition - center).direction;
+ e['rotation'] = angle;
+ });
+ },
+ child: Container(
+ width: 24,
+ height: 24,
+ decoration: BoxDecoration(
+ color: Colors.green,
+ shape: BoxShape.circle,
+ border: Border.all(color: Colors.white, width: 2),
+ ),
+ child: const Icon(
+ Icons.rotate_right,
+ size: 12,
+ color: Colors.white,
+ ),
+ ),
+ ),
+ ),
+
+ // Move Handle - Center
+ Positioned.fill(
+ child: GestureDetector(
+ onPanStart: (details) {
+ setState(() {
+ _dragStartPosition = position;
+ });
+ },
+ onPanUpdate: (details) {
+ setState(() {
+ final newPosition = position + details.delta;
+ e['position'] = Offset(
+ newPosition.dx.clamp(
+ 0,
+ _canvasSize.width - size.width,
+ ),
+ newPosition.dy.clamp(
+ 0,
+ _canvasSize.height - size.height,
+ ),
+ );
+ });
+ },
+ onPanEnd: (details) {
+ if (_dragStartPosition != null &&
+ _dragStartPosition != position) {
+ _addDragToUndoStack(
+ e['id'],
+ _dragStartPosition!,
+ position,
+ );
+ }
+ _dragStartPosition = null;
+ },
+ child: Container(color: Colors.transparent),
+ ),
+ ),
+ ],
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ Future<void> _pickImageFromGallery() async {
+ final List<XFile> images = await _picker.pickMultiImage();
+ if (images.isNotEmpty) {
+ final oldList = _deepCopy(elements);
+
+ // Smart grid layout for multiple images
+ final double imageSize = 150.0;
+ final double padding = 20.0;
+ final int columns = ((_canvasSize.width - padding * 2) /
+ (imageSize + padding))
+ .floor()
+ .clamp(1, 100);
+
+ setState(() {
+ for (int i = 0; i < images.length; i++) {
+ final int row = i ~/ columns;
+ final int col = i % columns;
+
+ final double x = padding + (col * (imageSize + padding));
+ final double y = padding + (row * (imageSize + padding));
+
+ elements.add({
+ 'id': '${DateTime.now().millisecondsSinceEpoch}_$i',
+ 'type': 'file_image',
+ 'content': images[i].path,
+ 'position': Offset(
+ x.clamp(padding, _canvasSize.width - imageSize - padding),
+ y.clamp(padding, _canvasSize.height - imageSize - padding),
+ ),
+ 'size': const Size(150, 150),
+ 'rotation': 0.0,
+ });
+ }
+ });
+
+ _changeStack.add(
+ Change(
+ oldList,
+ () => setState(() => elements = _deepCopy(elements)),
+ (val) => setState(() => elements = val),
+ ),
+ );
+ }
+ }
+
+ void _addDragToUndoStack(String id, Offset oldPos, Offset newPos) {
+ final oldList = _deepCopy(elements);
+ final oldItem = oldList.firstWhere((x) => x['id'] == id);
+ oldItem['position'] = oldPos;
+ final newList = _deepCopy(elements);
+ _changeStack.add(
+ Change(
+ oldList,
+ () => setState(() => elements = newList),
+ (val) => setState(() => elements = val),
+ ),
+ );
+ }
+
+ List<Map<String, dynamic>> _deepCopy(List<Map<String, dynamic>> source) {
+ return source.map((e) {
+ final copy = Map<String, dynamic>.from(e);
+ if (e['position'] is Offset) {
+ copy['position'] = Offset(
+ (e['position'] as Offset).dx,
+ (e['position'] as Offset).dy,
+ );
+ }
+ if (e['size'] is Size) {
+ copy['size'] = Size(
+ (e['size'] as Size).width,
+ (e['size'] as Size).height,
+ );
+ }
+ return copy;
+ }).toList();
+ }
+
+ void _openLayers() => _showComingSoon('Layers');
+ void _exportProject() => _showComingSoon('Export');
+ void _openSettings() => _showComingSoon('Settings');
+ void _showComingSoon(String feature) => ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(SnackBar(content: Text('$feature coming soon')));
+}
+
+// --- PAINTER ---
+
+class CanvasPainter extends CustomPainter {
+ final List<DrawingPath> paths;
+ final List<DrawingPoint> currentPoints;
+ final Color currentColor;
+ final double currentWidth;
+ final bool isEraser;
+
+ CanvasPainter({
+ required this.paths,
+ required this.currentPoints,
+ required this.currentColor,
+ required this.currentWidth,
+ required this.isEraser,
+ });
+
+ @override
+ void paint(Canvas canvas, Size size) {
+ canvas.saveLayer(Rect.fromLTWH(0, 0, size.width, size.height), Paint());
+
+ // Draw committed paths
+ for (final path in paths) {
+ final paint =
+ Paint()
+ ..color = path.isEraser ? Colors.transparent : path.color
+ ..blendMode = path.isEraser ? BlendMode.clear : BlendMode.srcOver
+ ..strokeWidth = path.strokeWidth
+ ..strokeCap = StrokeCap.round
+ ..strokeJoin = StrokeJoin.round
+ ..style = PaintingStyle.stroke;
+
+ if (path.points.length > 1) {
+ final Path p = Path();
+ p.moveTo(path.points.first.offset.dx, path.points.first.offset.dy);
+ for (int i = 1; i < path.points.length; i++) {
+ p.lineTo(path.points[i].offset.dx, path.points[i].offset.dy);
+ }
+ canvas.drawPath(p, paint);
+ } else if (path.points.isNotEmpty) {
+ canvas.drawPoints(ui.PointMode.points, [
+ path.points.first.offset,
+ ], paint);
+ }
+ }
+
+ // Draw current stroke
+ if (currentPoints.isNotEmpty) {
+ final paint =
+ Paint()
+ ..color = isEraser ? Colors.transparent : currentColor
+ ..blendMode = isEraser ? BlendMode.clear : BlendMode.srcOver
+ ..strokeWidth = currentWidth
+ ..strokeCap = StrokeCap.round
+ ..strokeJoin = StrokeJoin.round
+ ..style = PaintingStyle.stroke;
+
+ final Path p = Path();
+ p.moveTo(currentPoints.first.offset.dx, currentPoints.first.offset.dy);
+ for (int i = 1; i < currentPoints.length; i++) {
+ p.lineTo(currentPoints[i].offset.dx, currentPoints[i].offset.dy);
+ }
+ canvas.drawPath(p, paint);
+ }
+ canvas.restore();
+ }
+
+ @override
+ bool shouldRepaint(covariant CanvasPainter oldDelegate) => true;
+}
+
+// --- BOTTOM BAR ---
+
+class CanvasBottomBar extends StatelessWidget {
+ final String? activeItem;
+ final VoidCallback onMagicDraw;
+ final VoidCallback onMedia;
+ final VoidCallback onStylesheet;
+ final VoidCallback onTools;
+ final VoidCallback onText;
+ final VoidCallback onSelect;
+ final VoidCallback onPlugins;
+
+ const CanvasBottomBar({
+ super.key,
+ this.activeItem,
+ required this.onMagicDraw,
+ required this.onMedia,
+ required this.onStylesheet,
+ required this.onTools,
+ required this.onText,
+ required this.onSelect,
+ required this.onPlugins,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ decoration: BoxDecoration(
+ color: Colors.white,
+ border: Border(top: BorderSide(color: Colors.grey[200]!)),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withOpacity(0.05),
+ blurRadius: 10,
+ offset: const Offset(0, -5),
+ ),
+ ],
+ ),
+ child: SafeArea(
+ top: false,
+ child: Container(
+ padding: const EdgeInsets.symmetric(vertical: 10),
+ child: SingleChildScrollView(
+ scrollDirection: Axis.horizontal,
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ child: Row(
+ children: [
+ _BottomBarItem(
+ label: "Magic Draw",
+ iconPath: "assets/icons/magic_draw.svg",
+ onTap: onMagicDraw,
+ isActive: activeItem == "Magic Draw",
+ ),
+ const SizedBox(width: 24),
+ _BottomBarItem(
+ label: "Media",
+ iconPath: "assets/icons/media.svg",
+ onTap: onMedia,
+ ),
+ const SizedBox(width: 24),
+ _BottomBarItem(
+ label: "Stylesheet",
+ iconPath: "assets/icons/stylesheet.svg",
+ onTap: onStylesheet,
+ ),
+ const SizedBox(width: 24),
+ _BottomBarItem(
+ label: "Tools",
+ iconPath: "assets/icons/tools.svg",
+ onTap: onTools,
+ ),
+ const SizedBox(width: 24),
+ _BottomBarItem(
+ label: "Text",
+ iconPath: "assets/icons/text.svg",
+ onTap: onText,
+ ),
+ const SizedBox(width: 24),
+ _BottomBarItem(
+ label: "Select",
+ iconPath: "assets/icons/select.svg",
+ onTap: onSelect,
+ ),
+ const SizedBox(width: 24),
+ _BottomBarItem(
+ label: "Plugins",
+ iconPath: "assets/icons/plugins.svg",
+ onTap: onPlugins,
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class _BottomBarItem extends StatelessWidget {
+ final String label;
+ final String iconPath;
+ final VoidCallback onTap;
+ final bool isActive;
+
+ const _BottomBarItem({
+ required this.label,
+ required this.iconPath,
+ required this.onTap,
+ this.isActive = false,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return InkWell(
+ onTap: onTap,
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ SvgPicture.asset(
+ iconPath,
+ width: 24,
+ colorFilter: ColorFilter.mode(
+ isActive ? Colors.blue : Colors.black87,
+ BlendMode.srcIn,
+ ),
+ ),
+ const SizedBox(height: 6),
+ Text(
+ label,
+ style: TextStyle(
+ fontSize: 11,
+ fontWeight: FontWeight.w500,
+ color: isActive ? Colors.blue : Colors.black87,
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/ui/pages/canvas_toolbar/magic_draw_overlay.dart b/lib/ui/pages/canvas_toolbar/magic_draw_overlay.dart
@@ -0,0 +1,792 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_colorpicker/flutter_colorpicker.dart';
+
+// --- MAIN WIDGET: TOOLS OVERLAY ---
+class MagicDrawTools extends StatefulWidget {
+ final bool isActive;
+ final Color selectedColor;
+ final double strokeWidth;
+ final bool isEraser;
+ final VoidCallback onClose;
+ final Function(Color) onColorChanged;
+ final Function(double) onWidthChanged;
+ final Function(bool) onEraserToggle;
+
+ const MagicDrawTools({
+ super.key,
+ required this.isActive,
+ required this.selectedColor,
+ required this.strokeWidth,
+ required this.isEraser,
+ required this.onClose,
+ required this.onColorChanged,
+ required this.onWidthChanged,
+ required this.onEraserToggle,
+ });
+
+ @override
+ State<MagicDrawTools> createState() => _MagicDrawToolsState();
+}
+
+class _MagicDrawToolsState extends State<MagicDrawTools> {
+ bool _showStrokeSlider = false;
+
+ @override
+ Widget build(BuildContext context) {
+ if (!widget.isActive) return const SizedBox.shrink();
+
+ return Stack(
+ children: [
+ // 1. Close Button (Top Left - Safe Area)
+ Positioned(
+ top: 0,
+ left: 16,
+ child: SafeArea(
+ child: Padding(
+ padding: const EdgeInsets.only(top: 10.0),
+ child: Material(
+ type: MaterialType.circle,
+ color: Colors.white,
+ elevation: 4,
+ child: IconButton(
+ icon: const Icon(Icons.close, color: Colors.black),
+ onPressed: widget.onClose,
+ ),
+ ),
+ ),
+ ),
+ ),
+
+ // 2. Floating Tools UI (Bottom)
+ // MOVED UP: Changed bottom from 100 to 140
+ Positioned(
+ bottom: 140,
+ left: 16,
+ right: 16,
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ if (_showStrokeSlider) _buildTaperedStrokeSlider(),
+ const SizedBox(height: 8),
+ _buildMagicDrawPanel(),
+ ],
+ ),
+ ),
+ ],
+ );
+ }
+
+ Widget _buildMagicDrawPanel() {
+ return Container(
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(24),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withOpacity(0.1),
+ blurRadius: 15,
+ offset: const Offset(0, 5),
+ ),
+ ],
+ ),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ // Prompt Bar
+ Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Row(
+ children: [
+ Container(
+ padding: const EdgeInsets.all(8),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ border: Border.all(color: Colors.grey.shade300),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Icon(
+ Icons.emergency_recording,
+ color: Color(0xFFD8705D),
+ size: 20,
+ ),
+ ),
+ const SizedBox(width: 12),
+ const Expanded(
+ child: TextField(
+ decoration: InputDecoration.collapsed(
+ hintText: "tap imagination...",
+ hintStyle: TextStyle(fontSize: 14, color: Colors.black54),
+ ),
+ ),
+ ),
+ const Icon(Icons.mic_none, color: Colors.grey),
+ const SizedBox(width: 8),
+ Container(
+ width: 44,
+ height: 44,
+ decoration: const BoxDecoration(
+ color: Color(0xFF2B2B2B),
+ shape: BoxShape.circle,
+ ),
+ child: const Icon(
+ Icons.auto_awesome,
+ color: Colors.white,
+ size: 20,
+ ),
+ ),
+ ],
+ ),
+ ),
+ Divider(height: 1, color: Colors.grey.shade200),
+ // Tools Row
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ _buildToolIcon(Icons.crop_free, false, () {}),
+ _buildToolIcon(Icons.brush, !widget.isEraser, () {
+ widget.onEraserToggle(false);
+ }),
+
+ // Color Picker Trigger
+ GestureDetector(
+ onTap: () => _showAdvancedColorPicker(context),
+ child: Container(
+ width: 28,
+ height: 28,
+ decoration: BoxDecoration(
+ color: widget.selectedColor,
+ shape: BoxShape.circle,
+ border: Border.all(color: Colors.white, width: 2),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withOpacity(0.1),
+ blurRadius: 4,
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ GestureDetector(
+ onTap:
+ () => setState(
+ () => _showStrokeSlider = !_showStrokeSlider,
+ ),
+ child: Container(
+ padding: const EdgeInsets.all(8),
+ decoration: BoxDecoration(
+ color:
+ _showStrokeSlider
+ ? Colors.grey.shade200
+ : Colors.transparent,
+ shape: BoxShape.circle,
+ ),
+ child: Container(
+ width: 10,
+ height: 10,
+ decoration: const BoxDecoration(
+ color: Colors.black87,
+ shape: BoxShape.circle,
+ ),
+ ),
+ ),
+ ),
+ _buildToolIcon(
+ Icons.cleaning_services_outlined,
+ widget.isEraser,
+ () {
+ widget.onEraserToggle(true);
+ },
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildToolIcon(IconData icon, bool isActive, VoidCallback onTap) {
+ return GestureDetector(
+ onTap: onTap,
+ child: Container(
+ padding: const EdgeInsets.all(10),
+ decoration: BoxDecoration(
+ color: isActive ? Colors.grey.shade200 : Colors.transparent,
+ shape: BoxShape.circle,
+ ),
+ child: Icon(icon, size: 20, color: Colors.black54),
+ ),
+ );
+ }
+
+ Widget _buildTaperedStrokeSlider() {
+ return Container(
+ width: 250,
+ height: 50,
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(30),
+ boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 8)],
+ ),
+ child: Stack(
+ alignment: Alignment.center,
+ children: [
+ CustomPaint(
+ size: const Size(200, 30),
+ painter: _TaperedSliderPainter(),
+ ),
+ SliderTheme(
+ data: SliderThemeData(
+ trackHeight: 0,
+ thumbColor: Colors.white,
+ thumbShape: const RoundSliderThumbShape(
+ enabledThumbRadius: 10,
+ elevation: 3,
+ ),
+ overlayShape: SliderComponentShape.noOverlay,
+ activeTrackColor: Colors.transparent,
+ inactiveTrackColor: Colors.transparent,
+ ),
+ child: SizedBox(
+ width: 210,
+ child: Slider(
+ value: widget.strokeWidth,
+ min: 2.0,
+ max: 30.0,
+ onChanged: widget.onWidthChanged,
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ // --- NEW ADVANCED COLOR PICKER ---
+ void _showAdvancedColorPicker(BuildContext context) {
+ showModalBottomSheet(
+ context: context,
+ isScrollControlled: true,
+ backgroundColor: Colors.transparent,
+ builder: (context) {
+ return _AdvancedColorPickerSheet(
+ initialColor: widget.selectedColor,
+ onColorChanged: widget.onColorChanged,
+ );
+ },
+ );
+ }
+}
+
+// --- PAINTER FOR SLIDER ---
+class _TaperedSliderPainter extends CustomPainter {
+ @override
+ void paint(Canvas canvas, Size size) {
+ final paint =
+ Paint()
+ ..color = const Color(0xFF2B2B2B)
+ ..style = PaintingStyle.fill;
+ final path = Path();
+ path.moveTo(10, size.height / 2 - 2);
+ path.lineTo(size.width - 10, size.height / 2 - 10);
+ path.lineTo(size.width - 10, size.height / 2 + 10);
+ path.lineTo(10, size.height / 2 + 2);
+ path.close();
+ canvas.drawPath(path, paint);
+ }
+
+ @override
+ bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
+}
+
+// ==========================================
+// ADVANCED COLOR PICKER WIDGET
+// ==========================================
+
+class _AdvancedColorPickerSheet extends StatefulWidget {
+ final Color initialColor;
+ final ValueChanged<Color> onColorChanged;
+
+ const _AdvancedColorPickerSheet({
+ required this.initialColor,
+ required this.onColorChanged,
+ });
+
+ @override
+ State<_AdvancedColorPickerSheet> createState() =>
+ _AdvancedColorPickerSheetState();
+}
+
+class _AdvancedColorPickerSheetState extends State<_AdvancedColorPickerSheet>
+ with SingleTickerProviderStateMixin {
+ late TabController _tabController;
+ late Color _currentColor;
+
+ // Dummy Brand Palette (Simulating StyleSheet Fetch)
+ final List<Color> _brandPalette = [
+ Colors.blue,
+ const Color(0xFFCCFF00), // Neon Lime
+ Colors.purpleAccent,
+ const Color(0xFFF0F0F0), // Off White
+ Colors.black,
+ ];
+
+ final List<Color> _recentColors = [
+ Colors.blue,
+ Colors.purple,
+ const Color(0xFFD81B60),
+ Colors.pinkAccent,
+ Colors.amber,
+ ];
+
+ @override
+ void initState() {
+ super.initState();
+ _currentColor = widget.initialColor;
+ _tabController = TabController(length: 3, vsync: this);
+ }
+
+ @override
+ void dispose() {
+ _tabController.dispose();
+ super.dispose();
+ }
+
+ void _updateColor(Color color) {
+ setState(() => _currentColor = color);
+ widget.onColorChanged(color);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ height: MediaQuery.of(context).size.height * 0.85,
+ decoration: const BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
+ ),
+ child: SafeArea(
+ child: Column(
+ children: [
+ // --- HEADER ---
+ const SizedBox(height: 8),
+ Container(
+ width: 40,
+ height: 4,
+ decoration: BoxDecoration(
+ color: Colors.grey[300],
+ borderRadius: BorderRadius.circular(2),
+ ),
+ ),
+ const SizedBox(height: 16),
+
+ // --- TABS ---
+ TabBar(
+ controller: _tabController,
+ labelColor: Colors.black,
+ unselectedLabelColor: Colors.grey,
+ indicatorColor: Colors.black,
+ labelStyle: const TextStyle(fontWeight: FontWeight.w600),
+ tabs: const [
+ Tab(text: 'Grid'),
+ Tab(text: 'Spectrum'),
+ Tab(text: 'Sliders'),
+ ],
+ ),
+
+ const SizedBox(height: 16),
+
+ // --- HEX Preview ---
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ child: Row(
+ children: [
+ Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 8,
+ vertical: 4,
+ ),
+ decoration: BoxDecoration(
+ border: Border.all(color: Colors.grey.shade300),
+ borderRadius: BorderRadius.circular(6),
+ ),
+ child: Row(
+ children: const [
+ Text("Hex", style: TextStyle(fontSize: 12)),
+ Icon(Icons.arrow_drop_down, size: 16),
+ ],
+ ),
+ ),
+ const SizedBox(width: 8),
+ Container(
+ width: 30,
+ height: 30,
+ decoration: BoxDecoration(
+ color: _currentColor,
+ borderRadius: BorderRadius.circular(6),
+ ),
+ ),
+ const SizedBox(width: 8),
+ Expanded(
+ child: Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 10,
+ vertical: 8,
+ ),
+ decoration: BoxDecoration(
+ border: Border.all(color: Colors.grey.shade300),
+ borderRadius: BorderRadius.circular(6),
+ ),
+ child: Text(
+ "#${_currentColor.value.toRadixString(16).toUpperCase().substring(2)}",
+ style: const TextStyle(fontWeight: FontWeight.w500),
+ ),
+ ),
+ ),
+ const SizedBox(width: 8),
+ const Icon(Icons.edit, size: 18, color: Colors.grey),
+ ],
+ ),
+ ),
+
+ const SizedBox(height: 16),
+
+ // --- TAB VIEWS ---
+ Expanded(
+ child: TabBarView(
+ controller: _tabController,
+ physics:
+ const NeverScrollableScrollPhysics(), // Prevent swipe conflict
+ children: [
+ _buildGridTab(),
+ _buildSpectrumTab(),
+ _buildSlidersTab(),
+ ],
+ ),
+ ),
+
+ // --- SHARED FOOTER (Recently Used, Brand, Gradients) ---
+ _buildSharedFooter(),
+ ],
+ ),
+ ),
+ );
+ }
+
+ // --- TAB 1: GRID ---
+ Widget _buildGridTab() {
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ child: Column(
+ children: [
+ Expanded(
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(12),
+ child: GridView.builder(
+ physics: const BouncingScrollPhysics(),
+ gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
+ crossAxisCount: 10,
+ crossAxisSpacing: 2,
+ mainAxisSpacing: 2,
+ ),
+ itemCount: 100,
+ itemBuilder: (context, index) {
+ // Generate a varied color grid
+ final double hue = (index % 10) * 36.0;
+ final double saturation = ((index ~/ 10) + 1) / 10.0;
+ final color =
+ HSVColor.fromAHSV(1.0, hue, saturation, 0.9).toColor();
+
+ return GestureDetector(
+ onTap: () => _updateColor(color),
+ child: Container(color: color),
+ );
+ },
+ ),
+ ),
+ ),
+ const SizedBox(height: 10),
+ _buildHueSlider(),
+ _buildOpacitySlider(),
+ ],
+ ),
+ );
+ }
+
+ // --- TAB 2: SPECTRUM ---
+ Widget _buildSpectrumTab() {
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ child: Column(
+ children: [
+ Expanded(
+ child: ColorPicker(
+ pickerColor: _currentColor,
+ onColorChanged: _updateColor,
+ enableAlpha: false,
+ displayThumbColor: true,
+ paletteType: PaletteType.hsvWithHue,
+ labelTypes: const [], // Hide default text inputs
+ pickerAreaHeightPercent: 0.8,
+ pickerAreaBorderRadius: BorderRadius.circular(12),
+ // We hide standard sliders to use our custom ones to match design
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ // --- TAB 3: SLIDERS ---
+ Widget _buildSlidersTab() {
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ child: Column(
+ children: [
+ const SizedBox(height: 20),
+ _buildRGBSlider("R", Colors.red, _currentColor.red, (v) {
+ _updateColor(_currentColor.withRed(v.toInt()));
+ }),
+ _buildRGBSlider("G", Colors.green, _currentColor.green, (v) {
+ _updateColor(_currentColor.withGreen(v.toInt()));
+ }),
+ _buildRGBSlider("B", Colors.blue, _currentColor.blue, (v) {
+ _updateColor(_currentColor.withBlue(v.toInt()));
+ }),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildRGBSlider(
+ String label,
+ Color activeColor,
+ int value,
+ ValueChanged<double> onChanged,
+ ) {
+ return Padding(
+ padding: const EdgeInsets.only(bottom: 16.0),
+ child: Row(
+ children: [
+ Expanded(
+ child: SizedBox(
+ height: 36,
+ child: SliderTheme(
+ data: SliderThemeData(
+ trackHeight: 36,
+ activeTrackColor: activeColor,
+ inactiveTrackColor: activeColor.withOpacity(0.2),
+ thumbColor:
+ Colors.transparent, // Hide knob, make it look like a bar
+ thumbShape: const RoundSliderThumbShape(
+ enabledThumbRadius: 0,
+ ),
+ overlayShape: SliderComponentShape.noOverlay,
+ trackShape: const RectangularSliderTrackShape(),
+ ),
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(18),
+ child: Slider(
+ value: value.toDouble(),
+ min: 0,
+ max: 255,
+ onChanged: onChanged,
+ ),
+ ),
+ ),
+ ),
+ ),
+ const SizedBox(width: 12),
+ Container(
+ width: 50,
+ padding: const EdgeInsets.all(8),
+ alignment: Alignment.center,
+ decoration: BoxDecoration(
+ border: Border.all(color: Colors.grey.shade300),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Text(
+ "$value",
+ style: const TextStyle(fontWeight: FontWeight.bold),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ // --- SHARED COMPONENTS ---
+
+ Widget _buildHueSlider() {
+ return SizedBox(
+ height: 15,
+ child: SliderTheme(
+ data: SliderThemeData(
+ trackHeight: 8,
+ trackShape: const RectangularSliderTrackShape(),
+ thumbShape: const RoundSliderThumbShape(
+ enabledThumbRadius: 10,
+ elevation: 2,
+ ),
+ thumbColor: Colors.white,
+ overlayShape: SliderComponentShape.noOverlay,
+ ),
+ child: ColorPicker(
+ pickerColor: _currentColor,
+ onColorChanged: _updateColor,
+ enableAlpha: false,
+ displayThumbColor: true,
+ paletteType: PaletteType.hsv,
+ labelTypes: const [],
+ pickerAreaHeightPercent: 0.0, // Only sliders
+ ),
+ ),
+ );
+ }
+
+ Widget _buildOpacitySlider() {
+ return Padding(
+ padding: const EdgeInsets.only(top: 8.0),
+ child: SizedBox(
+ height: 15,
+ child: SliderTheme(
+ data: SliderThemeData(
+ trackHeight: 8,
+ thumbShape: const RoundSliderThumbShape(
+ enabledThumbRadius: 10,
+ elevation: 2,
+ ),
+ thumbColor: Colors.white,
+ overlayShape: SliderComponentShape.noOverlay,
+ ),
+ child: Slider(
+ value: 1.0,
+ onChanged: (v) {},
+ activeColor: Colors.blue,
+ inactiveColor: Colors.grey[300],
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildSharedFooter() {
+ return Container(
+ padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ "Recently used",
+ style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ Row(
+ children: _recentColors.map((c) => _buildColorCircle(c)).toList(),
+ ),
+ const SizedBox(height: 16),
+
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ const Text(
+ "Brand Palette",
+ style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold),
+ ),
+ Text(
+ "Edit",
+ style: TextStyle(
+ fontSize: 12,
+ color: Colors.blue[700],
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 8),
+ Row(
+ children: [
+ Container(
+ width: 32,
+ height: 32,
+ margin: const EdgeInsets.only(right: 8),
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ border: Border.all(color: Colors.grey.shade300),
+ ),
+ child: Icon(Icons.block, size: 16, color: Colors.red[300]),
+ ),
+ ..._brandPalette.map((c) => _buildColorCircle(c)).toList(),
+ Container(
+ width: 32,
+ height: 32,
+ decoration: BoxDecoration(
+ color: Colors.grey[200],
+ shape: BoxShape.circle,
+ ),
+ child: const Icon(Icons.add, size: 18, color: Colors.black54),
+ ),
+ ],
+ ),
+ const SizedBox(height: 16),
+
+ const Text(
+ "Gradients",
+ style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ Row(
+ children: [
+ _buildGradientCircle(Colors.black, Colors.white),
+ _buildGradientCircle(Colors.grey, Colors.black),
+ _buildGradientCircle(Colors.white, Colors.grey),
+ _buildGradientCircle(Colors.black, Colors.grey),
+ const SizedBox(width: 8),
+ Container(
+ width: 32,
+ height: 32,
+ decoration: BoxDecoration(
+ color: Colors.grey[100],
+ shape: BoxShape.circle,
+ ),
+ child: const Icon(Icons.tune, size: 16, color: Colors.black54),
+ ),
+ ],
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildColorCircle(Color color) {
+ return Container(
+ width: 32,
+ height: 32,
+ margin: const EdgeInsets.only(right: 12),
+ decoration: BoxDecoration(
+ color: color,
+ shape: BoxShape.circle,
+ border: Border.all(color: Colors.grey.withOpacity(0.2)),
+ ),
+ );
+ }
+
+ Widget _buildGradientCircle(Color c1, Color c2) {
+ return Container(
+ width: 32,
+ height: 32,
+ margin: const EdgeInsets.only(right: 12),
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ gradient: LinearGradient(
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ colors: [c1, c2],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/ui/pages/create_file_page.dart b/lib/ui/pages/create_file_page.dart
@@ -1,6 +1,7 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
+import 'canvas_board_page.dart';
class CreateFilePage extends StatefulWidget {
final File? file; // Made optional for blank canvas creation
@@ -357,18 +358,21 @@ class _CreateFilePageState extends State<CreateFilePage> {
}
void _navigateToEditor(int width, int height) {
- // If widget.file is null, we create a blank canvas.
- // If widget.file is not null, we can place that file on the canvas.
- final mode = widget.file == null ? "Blank Canvas" : "Image Canvas";
-
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text('Selected: $width x $height ($mode)')),
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder:
+ (context) => CanvasBoardPage(
+ // Generate a random ID for the project
+ projectId: 'proj_${DateTime.now().millisecondsSinceEpoch}',
+ // Pass the dimensions from the preset
+ width: width.toDouble(),
+ height: height.toDouble(),
+ // Pass the file if it exists (e.g. from "Image to Canvas" flow)
+ initialImage: widget.file,
+ ),
+ ),
);
-
- // TODO: Navigate to Editor Page passing:
- // - width
- // - height
- // - widget.file (optional)
}
}
diff --git a/pubspec.yaml b/pubspec.yaml
@@ -26,6 +26,10 @@ dependencies:
google_mlkit_text_recognition: ^0.15.0
flutter_svg: ^2.2.3
google_fonts: ^6.1.0
+ undo: ^1.0.1
+ flutter_box_transform: ^0.4.7
+ dotted_border: ^2.0.0
+ flutter_colorpicker: ^1.1.0
dev_dependencies:
flutter_test: