[mobile][photos] Advance image editor (#6618)

## Description
This PR integrates a fully-featured advanced image editor which supports
the following features

- Added image adjustment tools (e.g., brightness, contrast, saturation)
- Enabled text annotations on images
- Added freehand drawing support
- Included sticker placement functionality
This commit is contained in:
Ashil
2025-07-30 17:44:24 +05:30
committed by GitHub
43 changed files with 2939 additions and 29 deletions

View File

@@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.0117 17.6875C15.0497 17.6875 17.5117 15.225 17.5117 12.1875C17.5117 9.15 15.0497 6.6875 12.0117 6.6875C8.97372 6.6875 6.51172 9.1495 6.51172 12.1875C6.51172 15.2255 8.97422 17.6875 12.0117 17.6875Z" stroke="white" stroke-width="2" stroke-linejoin="round"/>
<path d="M19.4773 4.50781L18.2473 5.74481M5.53081 18.3753L4.50781 19.4048M11.9973 20.6793V22.1793M21.9973 11.6798H19.9973M18.7633 18.1708L19.9973 19.4048" stroke="white" stroke-width="2" stroke-linecap="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.0117 8.67969C11.5521 8.67969 11.097 8.77022 10.6723 8.94611C10.2477 9.122 9.86185 9.37981 9.53685 9.70481C9.21184 10.0298 8.95403 10.4157 8.77814 10.8403C8.60225 11.2649 8.51172 11.7201 8.51172 12.1797C8.51172 12.6393 8.60225 13.0944 8.77814 13.5191C8.95403 13.9437 9.21184 14.3296 9.53685 14.6546C9.86185 14.9796 10.2477 15.2374 10.6723 15.4133C11.097 15.5892 11.5521 15.6797 12.0117 15.6797" fill="white"/>
<path d="M2 12.1797H4M5.022 4.50769L6.0485 5.53419M12 1.67969V3.67969" stroke="white" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 22C6.477 22 2 17.523 2 12C2 6.477 6.477 2 12 2C17.523 2 22 6.477 22 12C22 17.523 17.523 22 12 22ZM12 20C14.1217 20 16.1566 19.1571 17.6569 17.6569C19.1571 16.1566 20 14.1217 20 12C20 9.87827 19.1571 7.84344 17.6569 6.34315C16.1566 4.84285 14.1217 4 12 4C9.87827 4 7.84344 4.84285 6.34315 6.34315C4.84285 7.84344 4 9.87827 4 12C4 14.1217 4.84285 16.1566 6.34315 17.6569C7.84344 19.1571 9.87827 20 12 20ZM12 18V6C13.5913 6 15.1174 6.63214 16.2426 7.75736C17.3679 8.88258 18 10.4087 18 12C18 13.5913 17.3679 15.1174 16.2426 16.2426C15.1174 17.3679 13.5913 18 12 18Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 695 B

View File

@@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 6C5.55228 6 6 5.55228 6 5C6 4.44772 5.55228 4 5 4C4.44772 4 4 4.44772 4 5C4 5.55228 4.44772 6 5 6Z" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1.66797 7.9974C1.66797 5.01184 1.66797 3.51906 2.59546 2.59156C3.52296 1.66406 5.01574 1.66406 8.0013 1.66406C10.9868 1.66406 12.4796 1.66406 13.4072 2.59156C14.3346 3.51906 14.3346 5.01184 14.3346 7.9974C14.3346 10.9829 14.3346 12.4757 13.4072 13.4033C12.4796 14.3307 10.9868 14.3307 8.0013 14.3307C5.01574 14.3307 3.52296 14.3307 2.59546 13.4033C1.66797 12.4757 1.66797 10.9829 1.66797 7.9974Z" stroke="black"/>
<path d="M3.33203 14C6.247 10.5166 9.51476 5.92267 14.3304 9.02823" stroke="black"/>
</svg>

After

Width:  |  Height:  |  Size: 788 B

View File

@@ -0,0 +1,4 @@
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 12.9896C3 15.7394 3 17.1143 3.85427 17.9687C4.70854 18.8229 6.08348 18.8229 8.83333 18.8229C11.5832 18.8229 12.9581 18.8229 13.8124 17.9687C14.6667 17.1143 14.6667 15.7394 14.6667 12.9896C14.6667 10.2398 14.6667 8.86483 13.8124 8.01053C12.9581 7.15625 11.5832 7.15625 8.83333 7.15625C6.08348 7.15625 4.70854 7.15625 3.85427 8.01053C3 8.86483 3 10.2398 3 12.9896Z" stroke="white" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.5168 2.15625L11.3953 3.08136C10.7984 3.57378 10.5 3.81999 10.5 4.12595H11.3333C14.476 4.12595 16.0474 4.12595 17.0237 5.10226C18 6.07857 18 7.64993 18 10.7926V11.3229" stroke="white" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 828 B

View File

@@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.00391 15C2.00391 18.87 5.13391 22 9.00391 22L7.95392 20.25" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M22 9C22 5.13 18.87 2 15 2L16.05 3.75" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 5V12C7 14.357 7 15.5355 7.73223 16.2678C8.46447 17 9.64298 17 12 17H19" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17.0039 19V12C17.0039 9.64298 17.0039 8.46447 16.2717 7.73223C15.5394 7 14.3609 7 12.0039 7H5.00391" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 750 B

View File

@@ -0,0 +1,6 @@
<svg width="29" height="31" viewBox="0 0 29 31" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M23 9.125L22.2977 20.4868C22.1182 23.3896 22.0285 24.841 21.3009 25.8846C20.9411 26.4005 20.478 26.8359 19.9408 27.1631C18.8544 27.825 17.4002 27.825 14.4917 27.825C11.5795 27.825 10.1234 27.825 9.03626 27.1619C8.49877 26.8341 8.03547 26.3979 7.67584 25.8811C6.94846 24.8359 6.86071 23.3824 6.68522 20.4756L6 9.125" stroke="#FF3A2C" stroke-width="1.7" stroke-linecap="round"/>
<path d="M4.29688 4.99792H24.6969M19.0933 4.99792L18.3197 3.40188C17.8058 2.34168 17.5487 1.81157 17.1055 1.48097C17.0072 1.40763 16.9031 1.3424 16.7941 1.28591C16.3033 1.03125 15.7142 1.03125 14.536 1.03125C13.3282 1.03125 12.7243 1.03125 12.2253 1.29659C12.1147 1.35539 12.0092 1.42327 11.9098 1.49951C11.4614 1.84351 11.2109 2.39301 10.71 3.49201L10.0235 4.99792" stroke="#FF3A2C" stroke-width="1.7" stroke-linecap="round"/>
<path d="M11.6719 21.5969V14.7969" stroke="#FF3A2C" stroke-width="1.7" stroke-linecap="round"/>
<path d="M17.3281 21.5969V14.7969" stroke="#FF3A2C" stroke-width="1.7" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 7.8C21 6.11984 21 5.27976 20.673 4.63803C20.3854 4.07354 19.9265 3.6146 19.362 3.32698C18.7202 3 17.8802 3 16.2 3H7.8C6.11984 3 5.27976 3 4.63803 3.32698C4.07354 3.6146 3.6146 4.07354 3.32698 4.63803C3 5.27976 3 6.11984 3 7.8V16.2C3 17.8802 3 18.7202 3.32698 19.362C3.6146 19.9265 4.07354 20.3854 4.63803 20.673C5.27976 21 6.11984 21 7.8 21H16.2C17.8802 21 18.7202 21 19.362 20.673C19.9265 20.3854 20.3854 19.9265 20.673 19.362C21 18.7202 21 17.8802 21 16.2V7.8ZM6 7H11V8.5H6V7ZM19 19H5L19 5V19ZM14.5 16V18H16V16H18V14.5H16V12.5H14.5V14.5H12.5V16H14.5Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 685 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -0,0 +1,3 @@
<svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.6511 8.0918C13.2317 7.97762 12.7903 7.91667 12.3346 7.91667C10.2236 7.91667 8.41814 9.22492 7.6848 11.0748M13.6511 8.0918C15.7739 8.66967 17.3346 10.6109 17.3346 12.9167C17.3346 15.6781 15.0961 17.9167 12.3346 17.9167C11.0541 17.9167 9.88589 17.4352 9.0013 16.6435M13.6511 8.0918C13.8771 7.52182 14.0013 6.90042 14.0013 6.25C14.0013 3.48857 11.7627 1.25 9.0013 1.25C6.23988 1.25 4.0013 3.48857 4.0013 6.25C4.0013 6.90042 4.12549 7.52182 4.35145 8.0918M7.6848 11.0748C7.4588 11.6448 7.33464 12.2662 7.33464 12.9167C7.33464 14.3975 7.97839 15.728 9.0013 16.6435M7.6848 11.0748C6.16085 10.66 4.92654 9.5425 4.35145 8.0918M9.0013 16.6435C8.11672 17.4352 6.94856 17.9167 5.66797 17.9167C2.90654 17.9167 0.667969 15.6781 0.667969 12.9167C0.667969 10.6109 2.22875 8.66967 4.35145 8.0918" stroke="white" stroke-width="1.25" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 957 B

View File

@@ -0,0 +1,4 @@
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.8333 8.55726V12.4461M14.25 3.0142C13.8337 2.9999 13.4168 2.99917 13 3.00042M14.25 17.989C13.8337 18.0034 13.4168 18.0041 13 18.0029M16.5456 3.4492C17.3442 3.85264 17.9928 4.5037 18.3932 5.30417M18.4167 15.6514C18.0184 16.4737 17.36 17.1426 16.5456 17.554" stroke="white" stroke-width="1.25" stroke-linecap="round"/>
<path d="M10.5052 5.08751C10.5052 3.45579 9.97371 3.00579 8.42187 3.00579C6.31214 3.00579 3.87715 2.79751 2.67428 4.87934C2.17188 5.74886 2.17188 6.91622 2.17188 9.25093V11.749C2.17188 14.0838 2.17188 15.2511 2.67428 16.1207C3.87715 18.2025 6.31214 17.9942 8.42187 17.9942C9.97371 17.9942 10.5052 17.5442 10.5052 15.9125V5.08751Z" stroke="white" stroke-width="1.25"/>
</svg>

After

Width:  |  Height:  |  Size: 800 B

View File

@@ -0,0 +1,8 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.5 10.5C8.32843 10.5 9 9.82843 9 9C9 8.17157 8.32843 7.5 7.5 7.5C6.67157 7.5 6 8.17157 6 9C6 9.82843 6.67157 10.5 7.5 10.5Z" fill="white"/>
<path d="M12 8.25C12.8284 8.25 13.5 7.57843 13.5 6.75C13.5 5.92157 12.8284 5.25 12 5.25C11.1716 5.25 10.5 5.92157 10.5 6.75C10.5 7.57843 11.1716 8.25 12 8.25Z" fill="white"/>
<path d="M16.5 10.5C17.3284 10.5 18 9.82843 18 9C18 8.17157 17.3284 7.5 16.5 7.5C15.6716 7.5 15 8.17157 15 9C15 9.82843 15.6716 10.5 16.5 10.5Z" fill="white"/>
<path d="M17.25 15C18.0784 15 18.75 14.3284 18.75 13.5C18.75 12.6716 18.0784 12 17.25 12C16.4216 12 15.75 12.6716 15.75 13.5C15.75 14.3284 16.4216 15 17.25 15Z" fill="white"/>
<path d="M14.25 18.75C15.0784 18.75 15.75 18.0784 15.75 17.25C15.75 16.4216 15.0784 15.75 14.25 15.75C13.4216 15.75 12.75 16.4216 12.75 17.25C12.75 18.0784 13.4216 18.75 14.25 18.75Z" fill="white"/>
<path d="M12.405 1.49219C10.9924 1.43766 9.58338 1.66894 8.26234 2.17214C6.9413 2.67535 5.73548 3.44013 4.71716 4.42063C3.69884 5.40113 2.889 6.57716 2.33618 7.87821C1.78336 9.17927 1.49895 10.5786 1.5 11.9922C1.49997 12.5482 1.62818 13.0967 1.87466 13.5951C2.12115 14.0934 2.47927 14.5282 2.92117 14.8656C3.36308 15.2031 3.87685 15.434 4.42255 15.5405C4.96825 15.647 5.53116 15.6262 6.0675 15.4797L6.9075 15.2472C7.24166 15.156 7.59237 15.1431 7.9323 15.2097C8.27223 15.2762 8.5922 15.4204 8.8673 15.6309C9.14239 15.8414 9.36516 16.1125 9.51827 16.4232C9.67138 16.7339 9.75068 17.0758 9.75 17.4222V20.2422C9.75 20.8389 9.98706 21.4112 10.409 21.8332C10.831 22.2551 11.4033 22.4922 12 22.4922C13.4136 22.4932 14.8129 22.2088 16.114 21.656C17.415 21.1032 18.5911 20.2933 19.5716 19.275C20.5521 18.2567 21.3168 17.0509 21.82 15.7299C22.3233 14.4088 22.5545 12.9998 22.5 11.5872C22.3912 8.94469 21.2927 6.43974 19.4226 4.56963C17.5525 2.69952 15.0475 1.60101 12.405 1.49219ZM18.4875 18.2247C17.6501 19.1015 16.6432 19.7989 15.528 20.2747C14.4127 20.7504 13.2125 20.9945 12 20.9922C11.8011 20.9922 11.6103 20.9132 11.4697 20.7725C11.329 20.6319 11.25 20.4411 11.25 20.2422V17.4222C11.25 16.4276 10.8549 15.4738 10.1517 14.7705C9.44839 14.0673 8.49456 13.6722 7.5 13.6722C7.16295 13.6722 6.82743 13.7176 6.5025 13.8072L5.6625 14.0397C5.34876 14.1238 5.01987 14.1345 4.70133 14.0709C4.38279 14.0074 4.08317 13.8714 3.82569 13.6734C3.56821 13.4754 3.35979 13.2207 3.2166 12.9292C3.07341 12.6376 2.9993 12.317 3 11.9922C2.99907 10.78 3.24301 9.58019 3.71719 8.46463C4.19136 7.34907 4.886 6.34079 5.75942 5.50028C6.63285 4.65978 7.66706 4.00437 8.8 3.57339C9.93295 3.1424 11.1413 2.94471 12.3525 2.99219C14.6079 3.10966 16.7398 4.05844 18.3368 5.6554C19.9338 7.25237 20.8825 9.3843 21 11.6397C21.0512 12.8516 20.8548 14.0613 20.4228 15.1947C19.9908 16.3282 19.3323 17.3617 18.4875 18.2322V18.2247Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -0,0 +1,3 @@
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.163 17H14.836M12.5 3V4M18.864 5.636L18.157 6.343M21.5 12H20.5M4.5 12H3.5M6.843 6.343L6.136 5.636M8.964 15.536C8.26487 14.8367 7.7888 13.9458 7.59598 12.9759C7.40316 12.006 7.50225 11.0008 7.88073 10.0872C8.25921 9.17366 8.90007 8.39284 9.72229 7.84349C10.5445 7.29414 11.5111 7.00093 12.5 7.00093C13.4889 7.00093 14.4555 7.29414 15.2777 7.84349C16.0999 8.39284 16.7408 9.17366 17.1193 10.0872C17.4977 11.0008 17.5968 12.006 17.404 12.9759C17.2112 13.9458 16.7351 14.8367 16.036 15.536L15.488 16.083C15.1745 16.3962 14.9259 16.7682 14.7564 17.1776C14.5868 17.587 14.4997 18.0259 14.5 18.469V19C14.5 19.5304 14.2893 20.0391 13.9142 20.4142C13.5391 20.7893 13.0304 21 12.5 21C11.9696 21 11.4609 20.7893 11.0858 20.4142C10.7107 20.0391 10.5 19.5304 10.5 19V18.469C10.5 17.574 10.144 16.715 9.512 16.083L8.964 15.536Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1011 B

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.49478 13.752C10.5833 10.1634 17.5788 3.1538 20.5387 3.00347C22.3699 2.82808 18.7218 9.32449 10.0785 16.4329M11.4581 10.0439L13.7157 12.3239M3 20.8536C3.70948 18.3462 3.26187 19.5784 3.50407 16.6909C3.63306 16.2634 3.89258 14.9367 5.51358 14.2755C7.35618 13.5239 8.70698 14.6601 9.05612 15.194C10.0847 16.3092 10.2039 17.6942 9.05612 19.2764C7.9083 20.8586 4.50352 21.2517 3 20.8536Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 582 B

View File

@@ -0,0 +1,3 @@
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.0449 2.83799C14.7701 2.62935 14.3777 2.6834 14.1689 2.95811C13.9603 3.23293 14.0143 3.62529 14.2891 3.83408L15.25 4.56455C16.056 5.17686 16.6055 5.59611 16.9619 5.95713C16.9896 5.98516 17.0149 6.01299 17.0391 6.03916H8C4.89347 6.03916 2.3751 8.55765 2.375 11.6642C2.375 14.7708 4.89341 17.2892 8 17.2892H11.333C11.6782 17.2892 11.958 17.0093 11.958 16.6642C11.9579 16.3191 11.6781 16.0392 11.333 16.0392H8C5.58376 16.0392 3.625 14.0804 3.625 11.6642C3.6251 9.24801 5.58382 7.28916 8 7.28916H17.0488C17.0218 7.31866 16.9934 7.35003 16.9619 7.38193C16.6055 7.74294 16.056 8.16221 15.25 8.77451L14.2891 9.50498C14.0142 9.71375 13.9602 10.1061 14.1689 10.381C14.3778 10.6557 14.7701 10.7089 15.0449 10.5001L16.0059 9.76963C16.7807 9.18098 17.4174 8.6996 17.8516 8.25986C18.2952 7.81041 18.625 7.30951 18.625 6.66904C18.6248 6.02878 18.2952 5.52855 17.8516 5.0792C17.4174 4.63943 16.7808 4.15716 16.0059 3.56846L15.0449 2.83799Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,4 @@
<svg width="16" height="20" viewBox="0 0 16 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.76819 16.3867V5.71094C6.34676 6.99951 3.50391 9.44665 3.50391 12.9695C3.50391 15.5767 6.34676 16.4224 7.76819 16.3867Z" fill="white"/>
<path d="M15.1412 12.8481C15.1412 7.85094 7.99833 0.710938 7.99833 0.710938C7.99833 0.710938 0.855469 7.85094 0.855469 12.8481C0.95579 14.6453 1.76312 16.3298 3.10118 17.5338C4.43975 18.7395 6.56975 19.2738 7.99833 19.2738C9.4269 19.2738 11.5569 18.7381 12.8955 17.5338C14.2335 16.3298 15.0409 14.6453 15.1412 12.8481Z" stroke="white" stroke-width="1.42857" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 657 B

View File

@@ -0,0 +1,3 @@
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 0.585938V19.4999H2.087L21 0.585938ZM6.916 17.4999H19V5.41394L6.916 17.4999ZM21 20.9999V22.9999H2V20.9999H21Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 241 B

View File

@@ -0,0 +1,3 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.20312 6.32812C2.20313 5.23411 2.63772 4.1849 3.41131 3.41131C4.1849 2.63772 5.23411 2.20313 6.32812 2.20312H17.3281C18.4221 2.20313 19.4714 2.63772 20.2449 3.41131C21.0185 4.1849 21.4531 5.23411 21.4531 6.32812V11.2781C21.0135 11.0536 20.553 10.8726 20.0781 10.7377V6.32812C20.0781 5.59878 19.7884 4.89931 19.2727 4.38358C18.7569 3.86786 18.0575 3.57812 17.3281 3.57812H6.32812C5.59878 3.57812 4.89931 3.86786 4.38358 4.38358C3.86786 4.89931 3.57812 5.59878 3.57812 6.32812V17.3281C3.57812 18.0575 3.86786 18.7569 4.38358 19.2727C4.89931 19.7884 5.59878 20.0781 6.32812 20.0781H10.7377C10.8743 20.5585 11.0545 21.0168 11.2781 21.4531H6.32812C5.23411 21.4531 4.1849 21.0185 3.41131 20.2449C2.63772 19.4714 2.20313 18.4221 2.20312 17.3281V6.32812ZM9.04237 15.3385C9.459 15.5461 10.0324 15.7606 10.7611 15.8734C10.6314 16.3144 10.5421 16.7664 10.4944 17.2236C9.77616 17.1113 9.07928 16.8902 8.42775 16.5677C8.11828 16.4169 7.82365 16.2374 7.54775 16.0315C7.44444 15.9533 7.34487 15.8702 7.24937 15.7826L7.22875 15.7634L7.22187 15.7565L7.21912 15.7537L7.21637 15.7524C7.08692 15.6238 7.01382 15.4491 7.01318 15.2667C7.01254 15.0842 7.08439 14.909 7.21294 14.7796C7.34148 14.6501 7.51619 14.577 7.69863 14.5764C7.88107 14.5757 8.05629 14.6476 8.18575 14.7761L8.18987 14.7789L8.22012 14.8064C8.25129 14.8339 8.30217 14.8751 8.37275 14.9301C8.51575 15.0374 8.7385 15.1873 9.04237 15.3385ZM8.39062 10.4531C8.7553 10.4531 9.10503 10.3083 9.3629 10.0504C9.62076 9.79253 9.76562 9.4428 9.76562 9.07812C9.76562 8.71345 9.62076 8.36372 9.3629 8.10585C9.10503 7.84799 8.7553 7.70312 8.39062 7.70312C8.02595 7.70312 7.67622 7.84799 7.41835 8.10585C7.16049 8.36372 7.01562 8.71345 7.01562 9.07812C7.01562 9.4428 7.16049 9.79253 7.41835 10.0504C7.67622 10.3083 8.02595 10.4531 8.39062 10.4531ZM16.6406 9.07812C16.6406 9.4428 16.4958 9.79253 16.2379 10.0504C15.98 10.3083 15.6303 10.4531 15.2656 10.4531C14.901 10.4531 14.5512 10.3083 14.2934 10.0504C14.0355 9.79253 13.8906 9.4428 13.8906 9.07812C13.8906 8.71345 14.0355 8.36372 14.2934 8.10585C14.5512 7.84799 14.901 7.70312 15.2656 7.70312C15.6303 7.70312 15.98 7.84799 16.2379 8.10585C16.4958 8.36372 16.6406 8.71345 16.6406 9.07812ZM24.2031 18.0156C24.2031 19.6567 23.5512 21.2305 22.3908 22.3908C21.2305 23.5512 19.6567 24.2031 18.0156 24.2031C16.3746 24.2031 14.8008 23.5512 13.6404 22.3908C12.48 21.2305 11.8281 19.6567 11.8281 18.0156C11.8281 16.3746 12.48 14.8008 13.6404 13.6404C14.8008 12.48 16.3746 11.8281 18.0156 11.8281C19.6567 11.8281 21.2305 12.48 22.3908 13.6404C23.5512 14.8008 24.2031 16.3746 24.2031 18.0156ZM18.7031 15.2656C18.7031 15.0833 18.6307 14.9084 18.5018 14.7795C18.3728 14.6506 18.198 14.5781 18.0156 14.5781C17.8333 14.5781 17.6584 14.6506 17.5295 14.7795C17.4006 14.9084 17.3281 15.0833 17.3281 15.2656V17.3281H15.2656C15.0833 17.3281 14.9084 17.4006 14.7795 17.5295C14.6506 17.6584 14.5781 17.8333 14.5781 18.0156C14.5781 18.198 14.6506 18.3728 14.7795 18.5018C14.9084 18.6307 15.0833 18.7031 15.2656 18.7031H17.3281V20.7656C17.3281 20.948 17.4006 21.1228 17.5295 21.2518C17.6584 21.3807 17.8333 21.4531 18.0156 21.4531C18.198 21.4531 18.3728 21.3807 18.5018 21.2518C18.6307 21.1228 18.7031 20.948 18.7031 20.7656V18.7031H20.7656C20.948 18.7031 21.1228 18.6307 21.2518 18.5018C21.3807 18.3728 21.4531 18.198 21.4531 18.0156C21.4531 17.8333 21.3807 17.6584 21.2518 17.5295C21.1228 17.4006 20.948 17.3281 20.7656 17.3281H18.7031V15.2656Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2C12.83 1.99993 13.6285 2.3174 14.2319 2.88731C14.8353 3.45722 15.1978 4.23637 15.245 5.065L15.25 5.25L15.251 13.202L15.331 13.271C16.2621 14.1024 16.8485 15.2521 16.975 16.494L16.994 16.746L17 17C17.0001 17.8238 16.7967 18.6349 16.4079 19.3612C16.019 20.0874 15.4567 20.7063 14.7709 21.1629C14.0852 21.6194 13.2973 21.8994 12.4772 21.9781C11.6572 22.0567 10.8304 21.9315 10.0704 21.6135C9.31039 21.2956 8.64073 20.7948 8.12093 20.1557C7.60113 19.5166 7.24731 18.7589 7.09092 17.9501C6.93453 17.1412 6.98042 16.3063 7.2245 15.5195C7.46858 14.7326 7.9033 14.0183 8.49 13.44L8.67 13.27L8.749 13.202L8.75 5.25C8.74983 4.4513 9.04378 3.68051 9.57573 3.08474C10.1077 2.48897 10.8404 2.10995 11.634 2.02L11.816 2.005L12 2ZM12 3.5C11.5607 3.50004 11.1374 3.66533 10.8143 3.96305C10.4912 4.26076 10.2919 4.66912 10.256 5.107L10.25 5.25V13.945L9.941 14.169C9.35063 14.5984 8.90932 15.2017 8.67884 15.8944C8.44835 16.5871 8.44024 17.3345 8.65563 18.0321C8.87102 18.7296 9.29913 19.3423 9.88004 19.7845C10.4609 20.2266 11.1656 20.476 11.8953 20.4979C12.625 20.5197 13.3432 20.3127 13.9495 19.9061C14.5558 19.4994 15.0197 18.9134 15.2764 18.2299C15.533 17.5465 15.5696 16.7999 15.3809 16.0947C15.1922 15.3895 14.7877 14.7609 14.224 14.297L14.059 14.17L13.752 13.945L13.75 5.25C13.75 4.78587 13.5656 4.34075 13.2374 4.01256C12.9092 3.68437 12.4641 3.5 12 3.5ZM12 8C12.1989 8 12.3897 8.07902 12.5303 8.21967C12.671 8.36032 12.75 8.55109 12.75 8.75V14.615C13.3201 14.7942 13.8071 15.1716 14.123 15.6788C14.4389 16.1861 14.5627 16.7896 14.4721 17.3803C14.3814 17.9709 14.0823 18.5096 13.6288 18.8988C13.1754 19.2879 12.5976 19.5019 12 19.5019C11.4024 19.5019 10.8246 19.2879 10.3712 18.8988C9.91772 18.5096 9.61859 17.9709 9.52794 17.3803C9.4373 16.7896 9.56112 16.1861 9.877 15.6788C10.1929 15.1716 10.6799 14.7942 11.25 14.615V8.75C11.25 8.55109 11.329 8.36032 11.4697 8.21967C11.6103 8.07902 11.8011 8 12 8Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,7 @@
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.5 16.3359V18.8359" stroke="white" stroke-opacity="0.6" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.5 8.83594V12.1693" stroke="white" stroke-opacity="0.6" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.5 2.16406V4.66406" stroke="white" stroke-opacity="0.6" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.16599 6.7474C7.16599 6.04415 7.09652 5.23249 7.79099 4.83153C8.08106 4.66406 8.47049 4.66406 9.24936 4.66406H11.7494C12.5282 4.66406 12.9176 4.66406 13.2077 4.83153C13.9021 5.23249 13.8327 6.04415 13.8327 6.7474C13.8327 7.45064 13.9021 8.2623 13.2077 8.66326C12.9176 8.83073 12.5282 8.83073 11.7494 8.83073H9.24936C8.47049 8.83073 8.08106 8.83073 7.79099 8.66326C7.09652 8.2623 7.16599 7.45064 7.16599 6.7474Z" stroke="white" stroke-opacity="0.6" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.83006 14.2474C3.83006 13.5441 3.76058 12.7325 4.45506 12.3316C4.74512 12.1641 5.13455 12.1641 5.91339 12.1641H15.0801C15.8589 12.1641 16.2483 12.1641 16.5384 12.3316C17.2328 12.7325 17.1634 13.5441 17.1634 14.2474C17.1634 14.9506 17.2328 15.7623 16.5384 16.1632C16.2483 16.3307 15.8589 16.3307 15.0801 16.3307H5.91339C5.13455 16.3307 4.74512 16.3307 4.45506 16.1632C3.76058 15.7623 3.83006 14.9506 3.83006 14.2474Z" stroke="white" stroke-opacity="0.6" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,5 @@
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.1699 6.7474C7.1699 6.04415 7.10042 5.23249 7.7949 4.83153C8.08496 4.66406 8.47438 4.66406 9.25322 4.66406H10.0865C10.8654 4.66406 11.2548 4.66406 11.5449 4.83153C12.2394 5.23249 12.1699 6.04415 12.1699 6.7474C12.1699 7.45064 12.2394 8.2623 11.5449 8.66326C11.2548 8.83073 10.8654 8.83073 10.0865 8.83073H9.25322C8.47438 8.83073 8.08496 8.83073 7.7949 8.66326C7.10042 8.2623 7.1699 7.45064 7.1699 6.7474Z" stroke="white" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.1699 14.2474C7.1699 13.5441 7.10042 12.7325 7.7949 12.3316C8.08496 12.1641 8.47438 12.1641 9.25322 12.1641H15.0865C15.8654 12.1641 16.2548 12.1641 16.5449 12.3316C17.2394 12.7325 17.1699 13.5441 17.1699 14.2474C17.1699 14.9506 17.2394 15.7623 16.5449 16.1632C16.2548 16.3307 15.8654 16.3307 15.0865 16.3307H9.25322C8.47438 16.3307 8.08496 16.3307 7.7949 16.1632C7.10042 15.7623 7.1699 14.9506 7.1699 14.2474Z" stroke="white" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.83203 2.16406V18.8307" stroke="white" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,5 @@
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.8301 6.7474C13.8301 6.04415 13.8996 5.23249 13.2051 4.83153C12.915 4.66406 12.5256 4.66406 11.7468 4.66406H10.9135C10.1346 4.66406 9.7452 4.66406 9.45512 4.83153C8.76062 5.23249 8.83012 6.04415 8.83012 6.7474C8.83012 7.45064 8.76062 8.2623 9.45512 8.66326C9.7452 8.83073 10.1346 8.83073 10.9135 8.83073H11.7468C12.5256 8.83073 12.915 8.83073 13.2051 8.66326C13.8996 8.2623 13.8301 7.45064 13.8301 6.7474Z" stroke="white" stroke-opacity="0.6" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.8301 14.2474C13.8301 13.5441 13.8996 12.7325 13.2051 12.3316C12.915 12.1641 12.5256 12.1641 11.7468 12.1641H5.91345C5.13462 12.1641 4.7452 12.1641 4.45512 12.3316C3.76062 12.7325 3.83012 13.5441 3.83012 14.2474C3.83012 14.9506 3.76062 15.7623 4.45512 16.1632C4.7452 16.3307 5.13462 16.3307 5.91345 16.3307H11.7468C12.5256 16.3307 12.915 16.3307 13.2051 16.1632C13.8996 15.7623 13.8301 14.9506 13.8301 14.2474Z" stroke="white" stroke-opacity="0.6" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17.168 2.16406V18.8307" stroke="white" stroke-opacity="0.6" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,3 @@
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.0573 15.1667L14.732 17.0147C14.9587 17.636 15.7107 17.9773 16.412 17.7747C17.112 17.5747 17.496 16.9067 17.2693 16.2853L14.536 8.80533C14.2733 8.08267 13.636 7.516 12.8227 7.28267C11.4227 6.88 9.91733 7.56133 9.46267 8.804L6.73333 16.284C6.50667 16.9067 6.89067 17.5733 7.59067 17.776C8.29067 17.976 9.044 17.636 9.27067 17.0147L9.944 15.1667H14.0573ZM13.084 12.5H10.9173L12 9.53467L13.084 12.5ZM6.66667 4.5H17.3333C18.0406 4.5 18.7189 4.78095 19.219 5.28105C19.719 5.78115 20 6.45942 20 7.16667V17.8333C20 18.5406 19.719 19.2189 19.219 19.719C18.7189 20.219 18.0406 20.5 17.3333 20.5H6.66667C5.95942 20.5 5.28115 20.219 4.78105 19.719C4.28095 19.2189 4 18.5406 4 17.8333V7.16667C4 6.45942 4.28095 5.78115 4.78105 5.28105C5.28115 4.78095 5.95942 4.5 6.66667 4.5Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 895 B

View File

@@ -0,0 +1,5 @@
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.5 11L16 10.5L15.5 11.5L15 12.5C14.1667 13.1667 12.4 14.5 12 14.5C11.6 14.5 10.8333 15.8333 10.5 16.5L8.5 18H7.5L6 17L4.5 15.5L2 13.5L2.5 11Z" fill="white"/>
<path d="M16.3346 10.6052L11.2836 15.6699C9.79772 17.1598 9.05472 17.9048 8.14291 17.9871C7.99217 18.0008 7.84051 18.0008 7.68977 17.9871C6.77793 17.9048 6.03496 17.1598 4.54902 15.6699L2.86536 13.9816C1.9355 13.0492 1.9355 11.5376 2.86536 10.6052M16.3346 10.6052L9.59997 3.85229M16.3346 10.6052H2.86536M2.86536 10.6052L9.59997 3.85229M9.59997 3.85229L7.91635 2.16406" stroke="white" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.8333 17.1641C18.8333 18.0846 18.0872 18.8307 17.1667 18.8307C16.2462 18.8307 15.5 18.0846 15.5 17.1641C15.5 16.2436 17.1667 14.6641 17.1667 14.6641C17.1667 14.6641 18.8333 16.2436 18.8333 17.1641Z" stroke="white" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1020 B

View File

@@ -0,0 +1,4 @@
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.168 16.3307L9.75705 9.45398C8.63754 6.2607 8.07778 4.66406 7.16797 4.66406C6.25816 4.66406 5.6984 6.2607 4.57888 9.45398L2.16797 16.3307M4.2513 10.4974H10.0846" stroke="white" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.8067 12.1145V15.8645M18.8067 12.1145C18.846 11.4286 18.8475 10.9061 18.7652 10.4796C18.571 9.47428 17.5205 8.86845 16.504 8.74519C15.5311 8.62726 14.7503 8.8777 13.9603 10.0288M18.8067 12.1145H16.437C16.073 12.1145 15.7058 12.1321 15.355 12.2296C13.2111 12.8256 13.3683 15.8321 15.5185 16.2027C15.7573 16.2439 16.0011 16.2615 16.243 16.2507C16.8076 16.2255 17.3286 15.9527 17.7758 15.607C18.2994 15.2019 18.8067 14.6365 18.8067 13.7812V12.1145Z" stroke="white" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 904 B

View File

@@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.5 17.5H7.5" stroke="white" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.0013 2.5V17.5007M10.0013 2.5C11.1575 2.5 12.6426 2.52545 13.825 2.64707C14.3251 2.69849 14.5751 2.72421 14.7964 2.81492C15.2568 3.00357 15.6278 3.41718 15.7675 3.89733C15.8346 4.12817 15.8346 4.39159 15.8346 4.91845M10.0013 2.5C8.84514 2.5 7.36003 2.52545 6.17764 2.64707C5.67755 2.69849 5.4275 2.72421 5.20619 2.81492C4.74584 3.00357 4.37477 3.41718 4.23511 3.89733C4.16797 4.12817 4.16797 4.39159 4.16797 4.91845" stroke="white" stroke-width="1.25" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 701 B

View File

@@ -0,0 +1,7 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.49609 12C2.49609 7.52166 2.49609 5.28249 3.88733 3.89124C5.27858 2.5 7.51775 2.5 11.9961 2.5C16.4744 2.5 18.7136 2.5 20.1049 3.89124C21.4961 5.28249 21.4961 7.52166 21.4961 12C21.4961 16.4783 21.4961 18.7175 20.1049 20.1088C18.7136 21.5 16.4744 21.5 11.9961 21.5C7.51775 21.5 5.27858 21.5 3.88733 20.1088C2.49609 18.7175 2.49609 16.4783 2.49609 12Z" stroke="white" stroke-width="1.5" stroke-linejoin="round"/>
<path d="M9.99609 15.5C9.99609 16.3284 9.32452 17 8.49609 17C7.66766 17 6.99609 16.3284 6.99609 15.5C6.99609 14.6716 7.66766 14 8.49609 14C9.32452 14 9.99609 14.6716 9.99609 15.5Z" stroke="white" stroke-width="1.5"/>
<path d="M17 8.5C17 7.67157 16.3284 7 15.5 7C14.6716 7 14 7.67157 14 8.5C14 9.32843 14.6716 10 15.5 10C16.3284 10 17 9.32843 17 8.5Z" stroke="white" stroke-width="1.5"/>
<path d="M8.49609 14V7" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
<path d="M15.5 10V17" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,4 @@
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 6.66406H13C15.7614 6.66406 18 8.90265 18 11.6641C18 14.4255 15.7614 16.6641 13 16.6641H9.66667" stroke="white" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.33333 3.33594L5.37183 4.06637C3.79061 5.26761 3 5.86823 3 6.66927C3 7.47031 3.79061 8.07093 5.37183 9.27219L6.33333 10.0026" stroke="white" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 516 B

View File

@@ -233,6 +233,8 @@ extension CustomColorScheme on ColorScheme {
? const Color(0xFF424242)
: const Color(0xFFFFFFFF);
Color get imageEditorPrimaryColor => const Color.fromRGBO(8, 194, 37, 1);
Color get defaultBackgroundColor =>
brightness == Brightness.light ? backgroundBaseLight : backgroundBaseDark;

View File

@@ -0,0 +1,76 @@
import 'package:flutter/material.dart';
import "package:flutter_svg/svg.dart";
import "package:photos/ente_theme_data.dart";
import "package:photos/theme/ente_theme.dart";
class CircularIconButton extends StatelessWidget {
final String label;
final VoidCallback onTap;
final String? svgPath;
final double size;
final bool isSelected;
const CircularIconButton({
super.key,
required this.label,
required this.onTap,
this.svgPath,
this.size = 60,
this.isSelected = false,
});
@override
Widget build(BuildContext context) {
final textTheme = getEnteTextTheme(context);
final colorScheme = getEnteColorScheme(context);
return SizedBox(
width: 90,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
GestureDetector(
onTap: onTap,
child: Container(
height: size,
width: size,
decoration: BoxDecoration(
color: isSelected
? Theme.of(context)
.colorScheme
.imageEditorPrimaryColor
.withOpacity(0.24)
: colorScheme.backgroundElevated2,
shape: BoxShape.circle,
border: Border.all(
color: isSelected
? Theme.of(context).colorScheme.imageEditorPrimaryColor
: colorScheme.backgroundElevated2,
width: 2,
),
),
child: svgPath != null
? SvgPicture.asset(
svgPath!,
width: 12,
height: 12,
fit: BoxFit.scaleDown,
colorFilter: ColorFilter.mode(
colorScheme.tabIcon,
BlendMode.srcIn,
),
)
: const SizedBox(),
),
),
const SizedBox(height: 6),
Text(
label,
style: textTheme.small,
textAlign: TextAlign.center,
),
],
),
);
}
}

View File

@@ -0,0 +1,110 @@
import 'package:flutter/material.dart';
import "package:flutter_svg/svg.dart";
import "package:photos/ente_theme_data.dart";
import "package:photos/theme/ente_theme.dart";
import "package:pro_image_editor/models/editor_configs/pro_image_editor_configs.dart";
class ImageEditorAppBar extends StatelessWidget implements PreferredSizeWidget {
const ImageEditorAppBar({
super.key,
required this.configs,
this.undo,
this.redo,
required this.done,
required this.close,
this.enableUndo = false,
this.enableRedo = false,
this.isMainEditor = false,
});
final ProImageEditorConfigs configs;
final Function()? undo;
final Function()? redo;
final Function() done;
final Function() close;
final bool enableUndo;
final bool enableRedo;
final bool isMainEditor;
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
@override
Widget build(BuildContext context) {
final colorScheme = getEnteColorScheme(context);
return AppBar(
elevation: 0,
automaticallyImplyLeading: false,
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TextButton(
onPressed: () {
enableUndo ? close() : Navigator.of(context).pop();
},
child: Text(
'Cancel',
style: getEnteTextTheme(context).body,
),
),
if (undo != null && redo != null)
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
tooltip: 'Undo',
onPressed: () {
undo != null ? undo!() : null;
},
icon: SvgPicture.asset(
"assets/image-editor/image-editor-undo.svg",
colorFilter: ColorFilter.mode(
enableUndo ? colorScheme.textBase : colorScheme.textMuted,
BlendMode.srcIn,
),
),
),
const SizedBox(width: 12),
IconButton(
tooltip: 'Redo',
onPressed: () {
redo != null ? redo!() : null;
},
icon: SvgPicture.asset(
'assets/image-editor/image-editor-redo.svg',
colorFilter: ColorFilter.mode(
enableRedo ? colorScheme.textBase : colorScheme.textMuted,
BlendMode.srcIn,
),
),
),
],
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 250),
transitionBuilder: (child, animation) =>
FadeTransition(opacity: animation, child: child),
child: TextButton(
key: ValueKey(isMainEditor ? 'save_copy' : 'done'),
onPressed: done,
child: Text(
isMainEditor ? 'Save Copy' : 'Done',
style: getEnteTextTheme(context).body.copyWith(
color: isMainEditor
? (enableUndo
? Theme.of(context)
.colorScheme
.imageEditorPrimaryColor
: colorScheme.textMuted)
: Theme.of(context)
.colorScheme
.imageEditorPrimaryColor,
),
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,163 @@
import "package:flutter/material.dart";
import "package:photos/theme/ente_theme.dart";
class ImageEditorColorPicker extends StatefulWidget {
final double value;
final ValueChanged<double> onChanged;
const ImageEditorColorPicker({
super.key,
required this.value,
required this.onChanged,
});
@override
ColorSliderState createState() => ColorSliderState();
}
class ColorSliderState extends State<ImageEditorColorPicker> {
Color get _selectedColor {
final hue = widget.value * 360;
return HSVColor.fromAHSV(1.0, hue, 1.0, 1.0).toColor();
}
@override
Widget build(BuildContext context) {
final colorScheme = getEnteColorScheme(context);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: SizedBox(
height: 40,
child: Stack(
alignment: Alignment.center,
children: [
Container(
height: 36,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25),
gradient: const LinearGradient(
colors: [
Color(0xFFFF0000),
Color(0xFFFF8000),
Color(0xFFFFFF00),
Color(0xFF80FF00),
Color(0xFF00FF00),
Color(0xFF00FF80),
Color(0xFF00FFFF),
Color(0xFF0080FF),
Color(0xFF0000FF),
Color(0xFF8000FF),
Color(0xFFFF00FF),
Color(0xFFFF0080),
Color(0xFFFF0000),
],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
border: Border.all(
color: colorScheme.backgroundElevated2,
width: 6,
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: SliderTheme(
data: SliderTheme.of(context).copyWith(
trackHeight: 36,
thumbShape: _CustomThumbShape(_selectedColor),
activeTrackColor: Colors.transparent,
inactiveTrackColor: Colors.transparent,
overlayShape: const RoundSliderOverlayShape(overlayRadius: 0),
trackShape: const _TransparentTrackShape(),
),
child: Slider(
value: widget.value,
onChanged: widget.onChanged,
min: 0.0,
max: 1.0,
),
),
),
],
),
),
);
}
}
class _CustomThumbShape extends SliderComponentShape {
final Color color;
const _CustomThumbShape(this.color);
@override
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
return const Size(28, 28);
}
@override
void paint(
PaintingContext context,
Offset center, {
required Animation<double> activationAnimation,
required Animation<double> enableAnimation,
required bool isDiscrete,
required TextPainter labelPainter,
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required TextDirection textDirection,
required double value,
required double textScaleFactor,
required Size sizeWithOverflow,
}) {
final Canvas canvas = context.canvas;
final Paint thumbPaint = Paint()
..color = color
..style = PaintingStyle.fill;
canvas.drawCircle(center, 11, thumbPaint);
final Paint borderPaint = Paint()
..color = Colors.white
..style = PaintingStyle.stroke
..strokeWidth = 3;
canvas.drawCircle(center, 13, borderPaint);
}
}
class _TransparentTrackShape extends SliderTrackShape {
const _TransparentTrackShape();
@override
Rect getPreferredRect({
required RenderBox parentBox,
Offset offset = Offset.zero,
required SliderThemeData sliderTheme,
bool isEnabled = false,
bool isDiscrete = false,
}) {
final double trackHeight = sliderTheme.trackHeight!;
final double trackLeft = offset.dx;
final double trackTop =
offset.dy + (parentBox.size.height - trackHeight) / 2;
final double trackWidth = parentBox.size.width;
return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
}
@override
void paint(
PaintingContext context,
Offset offset, {
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required Animation<double> enableAnimation,
required TextDirection textDirection,
required Offset thumbCenter,
Offset? secondaryOffset,
bool isDiscrete = false,
bool isEnabled = false,
}) {}
}

View File

@@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
import "package:pro_image_editor/mixins/converted_configs.dart";
import "package:pro_image_editor/models/editor_callbacks/pro_image_editor_callbacks.dart";
import "package:pro_image_editor/models/editor_configs/pro_image_editor_configs.dart";
/// A mixin providing access to simple editor configurations.
mixin SimpleConfigsAccess on StatefulWidget {
ProImageEditorConfigs get configs;
ProImageEditorCallbacks get callbacks;
}
mixin SimpleConfigsAccessState<T extends StatefulWidget>
on State<T>, ImageEditorConvertedConfigs {
SimpleConfigsAccess get _widget => (widget as SimpleConfigsAccess);
@override
ProImageEditorConfigs get configs => _widget.configs;
ProImageEditorCallbacks get callbacks => _widget.callbacks;
}

View File

@@ -0,0 +1,8 @@
/// The duration used for fade-in animations.
const fadeInDuration = Duration(milliseconds: 220);
/// The stagger delay between multiple fade-in animations
const fadeInDelay = Duration(milliseconds: 25);
/// The height of the sub-bar.
const editorBottomBarHeight = 180.0;

View File

@@ -0,0 +1,226 @@
import 'package:flutter/material.dart';
import "package:flutter_svg/svg.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/tools/editor/image_editor/circular_icon_button.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_configs_mixin.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_constants.dart";
import "package:pro_image_editor/mixins/converted_configs.dart";
import "package:pro_image_editor/models/editor_callbacks/pro_image_editor_callbacks.dart";
import "package:pro_image_editor/models/editor_configs/pro_image_editor_configs.dart";
import "package:pro_image_editor/modules/crop_rotate_editor/crop_rotate_editor.dart";
import "package:pro_image_editor/widgets/animated/fade_in_up.dart";
enum CropAspectRatioType {
original(
label: "Original",
ratio: null,
svg: "assets/image-editor/image-editor-crop-original.svg",
),
free(
label: "Free",
ratio: null,
svg: "assets/video-editor/video-crop-free-action.svg",
),
square(
label: "1:1",
ratio: 1.0,
svg: "assets/video-editor/video-crop-ratio_1_1-action.svg",
),
widescreen(
label: "16:9",
ratio: 16.0 / 9.0,
svg: "assets/video-editor/video-crop-ratio_16_9-action.svg",
),
portrait(
label: "9:16",
ratio: 9.0 / 16.0,
svg: "assets/video-editor/video-crop-ratio_9_16-action.svg",
),
photo(
label: "4:3",
ratio: 4.0 / 3.0,
svg: "assets/video-editor/video-crop-ratio_4_3-action.svg",
),
photo_3_4(
label: "3:4",
ratio: 3.0 / 4.0,
svg: "assets/video-editor/video-crop-ratio_3_4-action.svg",
);
const CropAspectRatioType({
required this.label,
required this.ratio,
required this.svg,
});
final String label;
final String svg;
final double? ratio;
}
class ImageEditorCropRotateBar extends StatefulWidget with SimpleConfigsAccess {
const ImageEditorCropRotateBar({
super.key,
required this.configs,
required this.callbacks,
required this.editor,
});
final CropRotateEditorState editor;
@override
final ProImageEditorConfigs configs;
@override
final ProImageEditorCallbacks callbacks;
@override
State<ImageEditorCropRotateBar> createState() =>
_ImageEditorCropRotateBarState();
}
class _ImageEditorCropRotateBarState extends State<ImageEditorCropRotateBar>
with ImageEditorConvertedConfigs, SimpleConfigsAccessState {
CropAspectRatioType selectedAspectRatio = CropAspectRatioType.original;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildFunctions(constraints),
],
);
},
);
}
Widget _buildFunctions(BoxConstraints constraints) {
return BottomAppBar(
color: getEnteColorScheme(context).backgroundBase,
padding: EdgeInsets.zero,
height: editorBottomBarHeight,
child: Align(
alignment: Alignment.bottomCenter,
child: FadeInUp(
duration: fadeInDuration,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularIconButton(
svgPath: "assets/image-editor/image-editor-crop-rotate.svg",
label: "Rotate",
onTap: () {
widget.editor.rotate();
},
),
const SizedBox(width: 6),
CircularIconButton(
svgPath: "assets/image-editor/image-editor-flip.svg",
label: "Flip",
onTap: () {
widget.editor.flip();
},
),
],
),
SizedBox(
height: 40,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: CropAspectRatioType.values.length,
itemBuilder: (context, index) {
final aspectRatio = CropAspectRatioType.values[index];
final isSelected = selectedAspectRatio == aspectRatio;
return Padding(
padding: const EdgeInsets.only(
left: 6.0,
right: 6.0,
),
child: CropAspectChip(
label: aspectRatio.label,
svg: aspectRatio.svg,
isSelected: isSelected,
onTap: () {
setState(() {
selectedAspectRatio = aspectRatio;
});
widget.editor
.updateAspectRatio(aspectRatio.ratio ?? -1);
},
),
);
},
),
),
],
),
),
),
);
}
}
class CropAspectChip extends StatelessWidget {
final String? label;
final IconData? icon;
final String? svg;
final bool isSelected;
final VoidCallback? onTap;
const CropAspectChip({
super.key,
this.label,
this.icon,
this.svg,
required this.isSelected,
this.onTap,
});
@override
Widget build(BuildContext context) {
final colorScheme = getEnteColorScheme(context);
return GestureDetector(
onTap: onTap,
child: Container(
decoration: BoxDecoration(
color: isSelected
? colorScheme.fillBasePressed
: colorScheme.backgroundElevated2,
borderRadius: BorderRadius.circular(25),
),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (svg != null) ...[
SvgPicture.asset(
svg!,
height: 32,
colorFilter: ColorFilter.mode(
isSelected ? colorScheme.backdropBase : colorScheme.tabIcon,
BlendMode.srcIn,
),
),
],
const SizedBox(width: 4),
if (label != null)
Text(
label!,
style: TextStyle(
color: isSelected
? colorScheme.backdropBase
: colorScheme.tabIcon,
),
),
const SizedBox(width: 4),
],
),
),
);
}
}

View File

@@ -0,0 +1,275 @@
import 'package:figma_squircle/figma_squircle.dart';
import 'package:flutter/material.dart';
import "package:photos/ente_theme_data.dart";
import "package:photos/theme/ente_theme.dart";
import 'package:pro_image_editor/pro_image_editor.dart';
class GlacierFilterMatrix {
static const saturation = [
0.97,
0.02,
0.00,
0.00,
0.00,
0.01,
0.98,
0.00,
0.00,
0.00,
0.01,
0.02,
0.96,
0.00,
0.00,
0.00,
0.00,
0.00,
1.00,
0.00,
];
static const contrast = [
0.94,
0.00,
0.00,
0.00,
7.07,
0.00,
0.94,
0.00,
0.00,
7.07,
0.00,
0.00,
0.94,
0.00,
7.07,
0.00,
0.00,
0.00,
1.00,
0.00,
];
static const hue = [
1.01,
0.40,
-0.41,
0.00,
0.00,
-0.04,
0.91,
0.14,
0.00,
0.00,
0.38,
-0.25,
0.87,
0.00,
0.00,
0.00,
0.00,
0.00,
1.00,
0.00,
];
static const temperature = [
0.80,
0.00,
0.00,
0.00,
0.00,
0.00,
1.00,
0.00,
0.00,
0.00,
0.00,
0.00,
1.00,
0.00,
0.00,
0.00,
0.00,
0.00,
1.00,
0.00,
];
}
final filterList = [
const FilterModel(
name: "None",
filters: [],
),
FilterModel(
name: 'Pop',
filters: [
ColorFilterAddons.saturation(0.3),
ColorFilterAddons.brightness(0.15),
],
),
FilterModel(
name: "Amber",
filters: [
ColorFilterAddons.rgbScale(1.01, 1.04, 1),
ColorFilterAddons.saturation(0.3),
],
),
FilterModel(
name: 'Dust',
filters: [
ColorFilterAddons.sepia(0.4),
ColorFilterAddons.brightness(0.13),
ColorFilterAddons.contrast(-.05),
],
),
FilterModel(
name: 'Carbon',
filters: [
ColorFilterAddons.contrast(0.2),
ColorFilterAddons.grayscale(),
],
),
const FilterModel(
name: 'Glacier',
filters: [
GlacierFilterMatrix.saturation,
GlacierFilterMatrix.temperature,
GlacierFilterMatrix.hue,
GlacierFilterMatrix.contrast,
],
),
FilterModel(
name: 'Haze',
filters: [
ColorFilterAddons.colorOverlay(228, 130, 225, 0.13),
ColorFilterAddons.saturation(-0.2),
],
),
FilterModel(
name: 'Meadow',
filters: [
ColorFilterAddons.rgbScale(1.05, 1.1, 1),
],
),
FilterModel(
name: 'Zest',
filters: [
ColorFilterAddons.brightness(.1),
ColorFilterAddons.contrast(.1),
ColorFilterAddons.saturation(.15),
],
),
FilterModel(
name: 'Retro',
filters: [
ColorFilterAddons.colorOverlay(25, 240, 252, 0.05),
ColorFilterAddons.sepia(0.3),
],
),
FilterModel(
name: 'Sepia',
filters: [
ColorFilterAddons.contrast(-0.15),
ColorFilterAddons.saturation(0.1),
],
),
];
class ImageEditorFilterBar extends StatefulWidget {
const ImageEditorFilterBar({
required this.filterModel,
required this.isSelected,
required this.onSelectFilter,
required this.editorImage,
this.filterKey,
super.key,
});
final FilterModel filterModel;
final bool isSelected;
final VoidCallback onSelectFilter;
final Widget editorImage;
final Key? filterKey;
@override
State<ImageEditorFilterBar> createState() => _ImageEditorFilterBarState();
}
class _ImageEditorFilterBarState extends State<ImageEditorFilterBar> {
@override
Widget build(BuildContext context) {
return buildFilteredOptions(
widget.editorImage,
widget.isSelected,
widget.filterModel.name,
widget.onSelectFilter,
widget.filterKey ?? ValueKey(widget.filterModel.name),
);
}
Widget buildFilteredOptions(
Widget editorImage,
bool isSelected,
String filterName,
VoidCallback onSelectFilter,
Key filterKey,
) {
return GestureDetector(
onTap: () => onSelectFilter(),
child: SizedBox(
height: 90,
width: 80,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Container(
height: 60,
width: 60,
decoration: ShapeDecoration(
shape: SmoothRectangleBorder(
borderRadius: SmoothBorderRadius(
cornerRadius: 12,
cornerSmoothing: 0.6,
),
side: BorderSide(
color: isSelected
? Theme.of(context).colorScheme.imageEditorPrimaryColor
: Colors.transparent,
width: 1.5,
),
),
),
child: Padding(
padding: isSelected ? const EdgeInsets.all(2) : EdgeInsets.zero,
child: ClipSmoothRect(
radius: SmoothBorderRadius(
cornerRadius: 9.69,
cornerSmoothing: 0.4,
),
child: SizedBox(
height: isSelected ? 56 : 60,
width: isSelected ? 56 : 60,
child: editorImage,
),
),
),
),
const SizedBox(height: 8),
Text(
filterName,
style: isSelected
? getEnteTextTheme(context).smallBold
: getEnteTextTheme(context).smallMuted,
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
);
}
}

View File

@@ -0,0 +1,136 @@
import 'dart:math';
import 'package:flutter/material.dart';
import "package:photos/ui/tools/editor/image_editor/circular_icon_button.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_configs_mixin.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_constants.dart";
import "package:pro_image_editor/mixins/converted_configs.dart";
import 'package:pro_image_editor/pro_image_editor.dart';
class ImageEditorMainBottomBar extends StatefulWidget with SimpleConfigsAccess {
const ImageEditorMainBottomBar({
super.key,
required this.configs,
required this.callbacks,
required this.editor,
});
final ProImageEditorState editor;
@override
final ProImageEditorConfigs configs;
@override
final ProImageEditorCallbacks callbacks;
@override
State<ImageEditorMainBottomBar> createState() =>
ImageEditorMainBottomBarState();
}
class ImageEditorMainBottomBarState extends State<ImageEditorMainBottomBar>
with ImageEditorConvertedConfigs, SimpleConfigsAccessState {
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildFunctions(constraints),
],
);
},
);
}
Widget _buildFunctions(BoxConstraints constraints) {
return BottomAppBar(
height: editorBottomBarHeight,
padding: EdgeInsets.zero,
clipBehavior: Clip.none,
child: AnimatedSwitcher(
layoutBuilder: (currentChild, previousChildren) => Stack(
clipBehavior: Clip.none,
alignment: Alignment.bottomCenter,
children: <Widget>[
...previousChildren,
if (currentChild != null) currentChild,
],
),
duration: const Duration(milliseconds: 400),
reverseDuration: const Duration(milliseconds: 0),
transitionBuilder: (child, animation) {
return FadeTransition(
opacity: animation,
child: SizeTransition(
sizeFactor: animation,
axis: Axis.vertical,
axisAlignment: -1,
child: child,
),
);
},
switchInCurve: Curves.ease,
child: widget.editor.isSubEditorOpen &&
!widget.editor.isSubEditorClosing
? const SizedBox.shrink()
: Align(
alignment: Alignment.center,
child: SingleChildScrollView(
clipBehavior: Clip.none,
scrollDirection: Axis.horizontal,
child: ConstrainedBox(
constraints: BoxConstraints(
minWidth: min(constraints.maxWidth, 600),
maxWidth: 600,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
CircularIconButton(
svgPath: "assets/image-editor/image-editor-crop.svg",
label: "Crop",
onTap: () {
widget.editor.openCropRotateEditor();
},
),
CircularIconButton(
svgPath:
"assets/image-editor/image-editor-filter.svg",
label: "Filter",
onTap: () {
widget.editor.openFilterEditor();
},
),
CircularIconButton(
svgPath: "assets/image-editor/image-editor-tune.svg",
label: "Adjust",
onTap: () {
widget.editor.openTuneEditor();
},
),
CircularIconButton(
svgPath: "assets/image-editor/image-editor-paint.svg",
label: "Draw",
onTap: () {
widget.editor.openPaintingEditor();
},
),
CircularIconButton(
svgPath:
"assets/image-editor/image-editor-sticker.svg",
label: "Sticker",
onTap: () {
widget.editor.openEmojiEditor();
},
),
],
),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,561 @@
import "dart:async";
import "dart:io";
import "dart:math";
import "dart:typed_data";
import 'dart:ui' as ui show Image;
import 'package:flutter/material.dart';
import "package:flutter_image_compress/flutter_image_compress.dart";
import "package:flutter_svg/svg.dart";
import "package:logging/logging.dart";
import 'package:path/path.dart' as path;
import "package:photo_manager/photo_manager.dart";
import "package:photos/core/event_bus.dart";
import "package:photos/db/files_db.dart";
import "package:photos/ente_theme_data.dart";
import "package:photos/events/local_photos_updated_event.dart";
import "package:photos/generated/l10n.dart";
import 'package:photos/models/file/file.dart' as ente;
import "package:photos/models/location/location.dart";
import "package:photos/services/sync/sync_service.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/components/action_sheet_widget.dart";
import "package:photos/ui/components/buttons/button_widget.dart";
import "package:photos/ui/components/models/button_type.dart";
import "package:photos/ui/notification/toast.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_app_bar.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_constants.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_crop_rotate.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_filter_bar.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_main_bottom_bar.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_paint_bar.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_text_bar.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_tune_bar.dart";
import "package:photos/ui/viewer/file/detail_page.dart";
import "package:photos/utils/dialog_util.dart";
import "package:photos/utils/navigation_util.dart";
import "package:pro_image_editor/models/editor_configs/main_editor_configs.dart";
import 'package:pro_image_editor/pro_image_editor.dart';
class NewImageEditor extends StatefulWidget {
final ente.EnteFile originalFile;
final File file;
final DetailPageConfiguration detailPageConfig;
const NewImageEditor({
super.key,
required this.file,
required this.originalFile,
required this.detailPageConfig,
});
@override
State<NewImageEditor> createState() => _NewImageEditorState();
}
class _NewImageEditorState extends State<NewImageEditor> {
final _mainEditorBarKey = GlobalKey<ImageEditorMainBottomBarState>();
final editorKey = GlobalKey<ProImageEditorState>();
final _logger = Logger("ImageEditor");
Future<void> saveImage(Uint8List? bytes) async {
if (bytes == null) return;
final dialog = createProgressDialog(context, S.of(context).saving);
await dialog.show();
debugPrint("Image saved with size: ${bytes.length} bytes");
final DateTime start = DateTime.now();
final ui.Image decodedResult = await decodeImageFromList(bytes);
final result = await FlutterImageCompress.compressWithList(
bytes,
minWidth: decodedResult.width,
minHeight: decodedResult.height,
);
_logger.info('Size after compression = ${result.length}');
final Duration diff = DateTime.now().difference(start);
_logger.info('image_editor time : $diff');
try {
final fileName =
path.basenameWithoutExtension(widget.originalFile.title!) +
"_edited_" +
DateTime.now().microsecondsSinceEpoch.toString() +
".JPEG";
//Disabling notifications for assets changing to insert the file into
//files db before triggering a sync.
await PhotoManager.stopChangeNotify();
final AssetEntity newAsset =
await (PhotoManager.editor.saveImage(result, filename: fileName));
final newFile = await ente.EnteFile.fromAsset(
widget.originalFile.deviceFolder ?? '',
newAsset,
);
newFile.creationTime = widget.originalFile.creationTime;
newFile.collectionID = widget.originalFile.collectionID;
newFile.location = widget.originalFile.location;
if (!newFile.hasLocation && widget.originalFile.localID != null) {
final assetEntity = await widget.originalFile.getAsset;
if (assetEntity != null) {
final latLong = await assetEntity.latlngAsync();
newFile.location = Location(
latitude: latLong.latitude,
longitude: latLong.longitude,
);
}
}
newFile.generatedID = await FilesDB.instance.insertAndGetId(newFile);
Bus.instance.fire(LocalPhotosUpdatedEvent([newFile], source: "editSave"));
unawaited(SyncService.instance.sync());
showShortToast(context, S.of(context).editsSaved);
_logger.info("Original file " + widget.originalFile.toString());
_logger.info("Saved edits to file " + newFile.toString());
final files = widget.detailPageConfig.files;
// the index could be -1 if the files fetched doesn't contain the newly
// edited files
int selectionIndex =
files.indexWhere((file) => file.generatedID == newFile.generatedID);
if (selectionIndex == -1) {
files.add(newFile);
selectionIndex = files.length - 1;
}
await dialog.hide();
replacePage(
context,
DetailPage(
widget.detailPageConfig.copyWith(
files: files,
selectedIndex: min(selectionIndex, files.length - 1),
),
),
);
} catch (e, s) {
await dialog.hide();
showToast(context, S.of(context).oopsCouldNotSaveEdits);
_logger.severe(e, s);
} finally {
await PhotoManager.startChangeNotify();
}
}
Future<void> _showExitConfirmationDialog(BuildContext context) async {
final actionResult = await showActionSheet(
context: context,
buttons: [
ButtonWidget(
labelText: S.of(context).yesDiscardChanges,
buttonType: ButtonType.critical,
buttonSize: ButtonSize.large,
shouldStickToDarkTheme: true,
buttonAction: ButtonAction.first,
isInAlert: true,
),
ButtonWidget(
labelText: S.of(context).no,
buttonType: ButtonType.secondary,
buttonSize: ButtonSize.large,
buttonAction: ButtonAction.second,
shouldStickToDarkTheme: true,
isInAlert: true,
),
],
body: S.of(context).doYouWantToDiscardTheEditsYouHaveMade,
actionSheetType: ActionSheetType.defaultActionSheet,
);
if (actionResult?.action != null &&
actionResult!.action == ButtonAction.first) {
replacePage(context, DetailPage(widget.detailPageConfig));
}
}
@override
Widget build(BuildContext context) {
final isLightMode = Theme.of(context).brightness == Brightness.light;
final colorScheme = getEnteColorScheme(context);
final textTheme = getEnteTextTheme(context);
return Scaffold(
resizeToAvoidBottomInset: false,
backgroundColor: colorScheme.backgroundBase,
body: PopScope(
canPop: false,
onPopInvoked: (didPop) {
if (didPop) return;
editorKey.currentState?.disablePopScope = true;
_showExitConfirmationDialog(context);
},
child: ProImageEditor.file(
key: editorKey,
widget.file,
callbacks: ProImageEditorCallbacks(
onCloseEditor: () {
editorKey.currentState?.disablePopScope = true;
_showExitConfirmationDialog(context);
},
mainEditorCallbacks: MainEditorCallbacks(
onStartCloseSubEditor: (value) {
_mainEditorBarKey.currentState?.setState(() {});
},
onPopInvoked: (didPop, result) {
editorKey.currentState?.disablePopScope = false;
},
),
),
configs: ProImageEditorConfigs(
imageEditorTheme: ImageEditorTheme(
appBarBackgroundColor: colorScheme.backgroundBase,
background: colorScheme.backgroundBase,
bottomBarBackgroundColor: colorScheme.backgroundBase,
filterEditor: FilterEditorTheme(
background: colorScheme.backgroundBase,
),
paintingEditor: PaintingEditorTheme(
background: colorScheme.backgroundBase,
),
textEditor: const TextEditorTheme(
background: Colors.transparent,
textFieldMargin: EdgeInsets.only(top: kToolbarHeight),
),
cropRotateEditor: CropRotateEditorTheme(
background: colorScheme.backgroundBase,
cropCornerColor:
Theme.of(context).colorScheme.imageEditorPrimaryColor,
),
tuneEditor: TuneEditorTheme(
background: colorScheme.backgroundBase,
),
emojiEditor: EmojiEditorTheme(
backgroundColor: colorScheme.backgroundBase,
),
),
imageGenerationConfigs: const ImageGenerationConfigs(
jpegQuality: 100,
generateInsideSeparateThread: true,
pngLevel: 0,
),
layerInteraction: const LayerInteraction(
hideToolbarOnInteraction: false,
),
theme: ThemeData(
scaffoldBackgroundColor: colorScheme.backgroundBase,
appBarTheme: AppBarTheme(
titleTextStyle: textTheme.body,
backgroundColor: colorScheme.backgroundBase,
),
bottomAppBarTheme: BottomAppBarTheme(
color: colorScheme.backgroundBase,
),
brightness: isLightMode ? Brightness.light : Brightness.dark,
),
customWidgets: ImageEditorCustomWidgets(
filterEditor: CustomWidgetsFilterEditor(
slider: (
editorState,
rebuildStream,
value,
onChanged,
onChangeEnd,
) =>
ReactiveCustomWidget(
builder: (context) {
return const SizedBox.shrink();
},
stream: rebuildStream,
),
filterButton: (
filter,
isSelected,
scaleFactor,
onSelectFilter,
editorImage,
filterKey,
) {
return ImageEditorFilterBar(
filterModel: filter,
isSelected: isSelected,
onSelectFilter: () {
onSelectFilter.call();
editorKey.currentState?.setState(() {});
},
editorImage: editorImage,
filterKey: filterKey,
);
},
appBar: (editor, rebuildStream) {
return ReactiveCustomAppbar(
builder: (context) {
return ImageEditorAppBar(
key: const Key('image_editor_app_bar'),
configs: editor.configs,
done: () => editor.done(),
close: () => editor.close(),
);
},
stream: rebuildStream,
);
},
),
tuneEditor: CustomWidgetsTuneEditor(
appBar: (editor, rebuildStream) {
return ReactiveCustomAppbar(
builder: (context) {
return ImageEditorAppBar(
enableRedo: editor.canRedo,
enableUndo: editor.canUndo,
key: const Key('image_editor_app_bar'),
redo: () => editor.redo(),
undo: () => editor.undo(),
configs: editor.configs,
done: () => editor.done(),
close: () => editor.close(),
);
},
stream: rebuildStream,
);
},
bottomBar: (editorState, rebuildStream) {
return ReactiveCustomWidget(
builder: (context) {
return ImageEditorTuneBar(
configs: editorState.configs,
callbacks: editorState.callbacks,
editor: editorState,
);
},
stream: rebuildStream,
);
},
),
mainEditor: CustomWidgetsMainEditor(
removeLayerArea: (key, rebuildStream) {
return ReactiveCustomWidget(
key: key,
builder: (context) {
return Align(
alignment: Alignment.bottomCenter,
child: StreamBuilder(
stream: rebuildStream,
builder: (context, snapshot) {
final isHovered = editorKey.currentState!
.layerInteractionManager.hoverRemoveBtn;
return AnimatedContainer(
key: key,
duration: const Duration(milliseconds: 150),
height: 56,
width: 56,
margin: const EdgeInsets.only(bottom: 24),
decoration: BoxDecoration(
color: isHovered
? colorScheme.warning400.withOpacity(0.8)
: Colors.white,
shape: BoxShape.circle,
),
padding: const EdgeInsets.all(12),
child: Center(
child: SvgPicture.asset(
"assets/image-editor/image-editor-delete.svg",
colorFilter: ColorFilter.mode(
isHovered
? Colors.white
: colorScheme.warning400
.withOpacity(0.8),
BlendMode.srcIn,
),
),
),
);
},
),
);
},
stream: rebuildStream,
);
},
appBar: (editor, rebuildStream) {
return ReactiveCustomAppbar(
builder: (context) {
return ImageEditorAppBar(
enableRedo: editor.canRedo,
enableUndo: editor.canUndo,
key: const Key('image_editor_app_bar'),
redo: () => editor.redoAction(),
undo: () => editor.undoAction(),
configs: editor.configs,
done: () async {
final Uint8List bytes = await editorKey.currentState!
.captureEditorImage();
await saveImage(bytes);
},
close: () {
_showExitConfirmationDialog(context);
},
isMainEditor: true,
);
},
stream: rebuildStream,
);
},
bottomBar: (editor, rebuildStream, key) => ReactiveCustomWidget(
key: key,
builder: (context) {
return ImageEditorMainBottomBar(
key: _mainEditorBarKey,
editor: editor,
configs: editor.configs,
callbacks: editor.callbacks,
);
},
stream: rebuildStream,
),
),
paintEditor: CustomWidgetsPaintEditor(
appBar: (editor, rebuildStream) {
return ReactiveCustomAppbar(
builder: (context) {
return ImageEditorAppBar(
enableRedo: editor.canRedo,
enableUndo: editor.canUndo,
key: const Key('image_editor_app_bar'),
redo: () => editor.redoAction(),
undo: () => editor.undoAction(),
configs: editor.configs,
done: () => editor.done(),
close: () => editor.close(),
);
},
stream: rebuildStream,
);
},
colorPicker:
(paintEditor, rebuildStream, currentColor, setColor) =>
null,
bottomBar: (editorState, rebuildStream) {
return ReactiveCustomWidget(
builder: (context) {
return ImageEditorPaintBar(
configs: editorState.configs,
callbacks: editorState.callbacks,
editor: editorState,
i18nColor: 'Color',
);
},
stream: rebuildStream,
);
},
),
textEditor: CustomWidgetsTextEditor(
appBar: (textEditor, rebuildStream) => ReactiveCustomAppbar(
builder: (context) {
return ImageEditorAppBar(
key: const Key('image_editor_app_bar'),
configs: textEditor.configs,
done: () => textEditor.done(),
close: () => textEditor.close(),
);
},
stream: rebuildStream,
),
bodyItems: (editor, rebuildStream) {
return [
ReactiveCustomWidget(
builder: (context) {
return Positioned.fill(
child: GestureDetector(
onTap: () {},
child: Container(
color: Colors.transparent,
),
),
);
},
stream: rebuildStream,
),
];
},
colorPicker:
(textEditor, rebuildStream, currentColor, setColor) => null,
bottomBar: (editorState, rebuildStream) {
return ReactiveCustomWidget(
builder: (context) {
return ImageEditorTextBar(
configs: editorState.configs,
callbacks: editorState.callbacks,
editor: editorState,
);
},
stream: rebuildStream,
);
},
),
cropRotateEditor: CustomWidgetsCropRotateEditor(
appBar: (editor, rebuildStream) {
return ReactiveCustomAppbar(
builder: (context) {
return ImageEditorAppBar(
key: const Key('image_editor_app_bar'),
configs: editor.configs,
done: () => editor.done(),
close: () => editor.close(),
enableRedo: editor.canRedo,
enableUndo: editor.canUndo,
redo: () => editor.redoAction(),
undo: () => editor.undoAction(),
);
},
stream: rebuildStream,
);
},
bottomBar: (cropRotateEditor, rebuildStream) =>
ReactiveCustomWidget(
stream: rebuildStream,
builder: (_) => ImageEditorCropRotateBar(
configs: cropRotateEditor.configs,
callbacks: cropRotateEditor.callbacks,
editor: cropRotateEditor,
),
),
),
),
mainEditorConfigs: const MainEditorConfigs(enableZoom: true),
paintEditorConfigs: const PaintEditorConfigs(enabled: true),
textEditorConfigs: const TextEditorConfigs(
enabled: false,
canToggleBackgroundMode: true,
canToggleTextAlign: true,
),
cropRotateEditorConfigs: const CropRotateEditorConfigs(
canChangeAspectRatio: true,
canFlip: true,
canRotate: true,
canReset: true,
enabled: true,
),
filterEditorConfigs: FilterEditorConfigs(
enabled: true,
fadeInUpDuration: fadeInDuration,
fadeInUpStaggerDelayDuration: fadeInDelay,
filterList: filterList,
),
tuneEditorConfigs: const TuneEditorConfigs(enabled: true),
blurEditorConfigs: const BlurEditorConfigs(
enabled: false,
),
emojiEditorConfigs: const EmojiEditorConfigs(
enabled: true,
checkPlatformCompatibility: true,
),
stickerEditorConfigs: StickerEditorConfigs(
enabled: false,
buildStickers: (setLayer, scrollController) {
return const SizedBox.shrink();
},
),
),
),
),
);
}
}

View File

@@ -0,0 +1,88 @@
import 'package:flutter/material.dart';
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_color_picker.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_configs_mixin.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_constants.dart";
import "package:pro_image_editor/mixins/converted_configs.dart";
import "package:pro_image_editor/models/editor_callbacks/pro_image_editor_callbacks.dart";
import "package:pro_image_editor/models/editor_configs/pro_image_editor_configs.dart";
import "package:pro_image_editor/modules/paint_editor/paint_editor.dart";
import "package:pro_image_editor/widgets/animated/fade_in_up.dart";
class ImageEditorPaintBar extends StatefulWidget with SimpleConfigsAccess {
const ImageEditorPaintBar({
super.key,
required this.configs,
required this.callbacks,
required this.editor,
required this.i18nColor,
});
final PaintingEditorState editor;
@override
final ProImageEditorConfigs configs;
@override
final ProImageEditorCallbacks callbacks;
final String i18nColor;
@override
State<ImageEditorPaintBar> createState() => _ImageEditorPaintBarState();
}
class _ImageEditorPaintBarState extends State<ImageEditorPaintBar>
with ImageEditorConvertedConfigs, SimpleConfigsAccessState {
double colorSliderValue = 0.5;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildFunctions(constraints),
],
);
},
);
}
Widget _buildFunctions(BoxConstraints constraints) {
return BottomAppBar(
height: editorBottomBarHeight,
padding: EdgeInsets.zero,
child: Align(
alignment: Alignment.center,
child: FadeInUp(
duration: fadeInDuration,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(left: 20.0),
child: Text(
"Brush Color",
style: getEnteTextTheme(context).body,
),
),
const SizedBox(height: 24),
ImageEditorColorPicker(
value: colorSliderValue,
onChanged: (value) {
setState(() {
colorSliderValue = value;
});
final hue = value * 360;
final color = HSVColor.fromAHSV(1.0, hue, 1.0, 1.0).toColor();
widget.editor.colorChanged(color);
},
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,380 @@
import 'package:flutter/material.dart';
import "package:flutter_svg/svg.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/tools/editor/image_editor/circular_icon_button.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_color_picker.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_configs_mixin.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_constants.dart";
import "package:pro_image_editor/mixins/converted_configs.dart";
import 'package:pro_image_editor/pro_image_editor.dart';
class ImageEditorTextBar extends StatefulWidget with SimpleConfigsAccess {
const ImageEditorTextBar({
super.key,
required this.configs,
required this.callbacks,
required this.editor,
});
final TextEditorState editor;
@override
final ProImageEditorConfigs configs;
@override
final ProImageEditorCallbacks callbacks;
@override
State<ImageEditorTextBar> createState() => _ImageEditorTextBarState();
}
class _ImageEditorTextBarState extends State<ImageEditorTextBar>
with ImageEditorConvertedConfigs, SimpleConfigsAccessState {
int selectedActionIndex = -1;
double colorSliderValue = 0.5;
void _selectAction(int index) {
setState(() {
selectedActionIndex = selectedActionIndex == index ? -1 : index;
});
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildFunctions(constraints),
],
);
},
);
}
Widget _buildFunctions(BoxConstraints constraints) {
return BottomAppBar(
padding: EdgeInsets.zero,
height: editorBottomBarHeight,
child: FadeInUp(
duration: fadeInDuration,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
_buildMainActionButtons(),
_buildHelperWidget(),
],
),
),
);
}
Widget _buildMainActionButtons() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
CircularIconButton(
svgPath: "assets/image-editor/image-editor-text-color.svg",
label: "Color",
isSelected: selectedActionIndex == 0,
onTap: () {
_selectAction(0);
},
),
CircularIconButton(
svgPath: "assets/image-editor/image-editor-text-font.svg",
label: "Font",
isSelected: selectedActionIndex == 1,
onTap: () {
_selectAction(1);
},
),
CircularIconButton(
svgPath: "assets/image-editor/image-editor-text-background.svg",
label: "Background",
isSelected: selectedActionIndex == 2,
onTap: () {
setState(() {
selectedActionIndex = 2;
});
},
),
CircularIconButton(
svgPath: "assets/image-editor/image-editor-text-align-left.svg",
label: "Align",
isSelected: selectedActionIndex == 3,
onTap: () {
setState(() {
selectedActionIndex = 3;
});
},
),
],
);
}
Widget _buildHelperWidget() {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
switchInCurve: Curves.easeInOut,
switchOutCurve: Curves.easeInOut,
child: switch (selectedActionIndex) {
0 => ImageEditorColorPicker(
value: colorSliderValue,
onChanged: (value) {
setState(() {
colorSliderValue = value;
});
final hue = value * 360;
final color = HSVColor.fromAHSV(1.0, hue, 1.0, 1.0).toColor();
widget.editor.primaryColor = color;
},
),
1 => _FontPickerWidget(editor: widget.editor),
2 => _BackgroundPickerWidget(editor: widget.editor),
3 => _AlignPickerWidget(editor: widget.editor),
_ => const SizedBox.shrink(),
},
);
}
}
class _FontPickerWidget extends StatelessWidget {
final TextEditorState editor;
const _FontPickerWidget({required this.editor});
@override
Widget build(BuildContext context) {
final colorScheme = getEnteColorScheme(context);
if (editor.textEditorConfigs.customTextStyles == null) {
return const SizedBox.shrink();
}
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: editor.textEditorConfigs.customTextStyles!
.asMap()
.entries
.map((entry) {
final item = entry.value;
final selected = editor.selectedTextStyle;
final bool isSelected = selected.hashCode == item.hashCode;
return Padding(
padding: const EdgeInsets.only(right: 12),
child: GestureDetector(
onTap: () {
editor.setTextStyle(item);
},
child: Container(
height: 40,
width: 48,
decoration: BoxDecoration(
color: isSelected
? colorScheme.fillBasePressed
: colorScheme.backgroundElevated2,
borderRadius: BorderRadius.circular(25),
),
child: Center(
child: Text(
'Aa',
style: item.copyWith(
color: isSelected
? colorScheme.backdropBase
: colorScheme.tabIcon,
),
),
),
),
),
);
}).toList(),
);
}
}
class _BackgroundPickerWidget extends StatelessWidget {
final TextEditorState editor;
const _BackgroundPickerWidget({required this.editor});
@override
Widget build(BuildContext context) {
final colorScheme = getEnteColorScheme(context);
final isLightMode = Theme.of(context).brightness == Brightness.light;
final backgroundStyles = {
LayerBackgroundMode.background: {
'text': 'Aa',
'selectedBackgroundColor':
isLightMode ? colorScheme.fillFaint : Colors.white,
'backgroundColor': colorScheme.backgroundElevated2,
'border': null,
'textColor': Colors.white,
'selectedInnerBackgroundColor': Colors.black,
'innerBackgroundColor': Colors.transparent,
},
LayerBackgroundMode.backgroundAndColor: {
'text': 'Aa',
'selectedBackgroundColor':
isLightMode ? colorScheme.fillFaint : Colors.white,
'backgroundColor': colorScheme.backgroundElevated2,
'border': null,
'textColor': Colors.black,
'selectedInnerBackgroundColor': Colors.transparent,
'innerBackgroundColor': Colors.transparent,
},
LayerBackgroundMode.backgroundAndColorWithOpacity: {
'text': 'Aa',
'selectedBackgroundColor':
isLightMode ? colorScheme.fillFaint : Colors.white,
'backgroundColor': colorScheme.backgroundElevated2,
'border': null,
'textColor': Colors.black,
'selectedInnerBackgroundColor': Colors.black.withOpacity(0.11),
'innerBackgroundColor': isLightMode
? Colors.black.withOpacity(0.11)
: Colors.white.withOpacity(0.11),
},
LayerBackgroundMode.onlyColor: {
'text': 'Aa',
'selectedBackgroundColor':
isLightMode ? colorScheme.fillFaint : Colors.black,
'backgroundColor': colorScheme.backgroundElevated2,
'border':
isLightMode ? null : Border.all(color: Colors.white, width: 2),
'textColor': Colors.black,
'selectedInnerBackgroundColor': Colors.white,
'innerBackgroundColor': Colors.white.withOpacity(0.6),
},
};
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: backgroundStyles.entries.map((entry) {
final mode = entry.key;
final style = entry.value;
final isSelected = editor.backgroundColorMode == mode;
return Padding(
padding: const EdgeInsets.only(right: 12),
child: GestureDetector(
onTap: () {
editor.setState(() {
editor.backgroundColorMode = mode;
});
},
child: SizedBox(
height: 40,
width: 48,
child: Container(
decoration: BoxDecoration(
color: isSelected
? style['selectedBackgroundColor'] as Color
: style['backgroundColor'] as Color,
borderRadius: BorderRadius.circular(25),
border: isSelected ? style['border'] as Border? : null,
),
child: Stack(
alignment: Alignment.center,
children: [
Center(
child: Container(
height: 20,
width: 22,
decoration: ShapeDecoration(
color: isSelected
? style['selectedInnerBackgroundColor'] as Color
: style['innerBackgroundColor'] as Color,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
),
),
),
Center(
child: Text(
style['text'] as String,
style: TextStyle(
color: isSelected
? style['textColor'] as Color
: colorScheme.tabIcon,
),
),
),
],
),
),
),
),
);
}).toList(),
);
}
}
class _AlignPickerWidget extends StatelessWidget {
final TextEditorState editor;
const _AlignPickerWidget({required this.editor});
@override
Widget build(BuildContext context) {
final colorScheme = getEnteColorScheme(context);
final alignments = [
(TextAlign.left, "assets/image-editor/image-editor-text-align-left.svg"),
(
TextAlign.center,
"assets/image-editor/image-editor-text-align-center.svg"
),
(
TextAlign.right,
"assets/image-editor/image-editor-text-align-right.svg"
),
];
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: alignments.map((alignmentData) {
final (alignment, svgPath) = alignmentData;
final isSelected = editor.align == alignment;
return Padding(
padding: const EdgeInsets.only(right: 12.0),
child: GestureDetector(
onTap: () {
editor.setState(() {
editor.align = alignment;
});
},
child: Container(
height: 40,
width: 48,
decoration: BoxDecoration(
color: isSelected
? colorScheme.fillBasePressed
: colorScheme.backgroundElevated2,
borderRadius: BorderRadius.circular(25),
border: isSelected
? Border.all(color: Colors.black, width: 2)
: null,
),
child: Center(
child: SvgPicture.asset(
svgPath,
width: 22,
height: 22,
fit: BoxFit.scaleDown,
colorFilter: ColorFilter.mode(
isSelected ? colorScheme.backdropBase : colorScheme.tabIcon,
BlendMode.srcIn,
),
),
),
),
),
);
}).toList(),
);
}
}

View File

@@ -0,0 +1,651 @@
import 'dart:math';
import 'package:flutter/material.dart';
import "package:flutter_svg/svg.dart";
import "package:photos/ente_theme_data.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_configs_mixin.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_constants.dart";
import "package:pro_image_editor/mixins/converted_configs.dart";
import "package:pro_image_editor/models/editor_callbacks/pro_image_editor_callbacks.dart";
import "package:pro_image_editor/models/editor_configs/pro_image_editor_configs.dart";
import "package:pro_image_editor/modules/tune_editor/tune_editor.dart";
import "package:pro_image_editor/widgets/animated/fade_in_up.dart";
class ImageEditorTuneBar extends StatefulWidget with SimpleConfigsAccess {
const ImageEditorTuneBar({
super.key,
required this.configs,
required this.callbacks,
required this.editor,
});
final TuneEditorState editor;
@override
final ProImageEditorConfigs configs;
@override
final ProImageEditorCallbacks callbacks;
@override
State<ImageEditorTuneBar> createState() => _ImageEditorTuneBarState();
}
class _ImageEditorTuneBarState extends State<ImageEditorTuneBar>
with ImageEditorConvertedConfigs, SimpleConfigsAccessState {
TuneEditorState get tuneEditor => widget.editor;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildFunctions(constraints),
],
);
},
);
}
Widget _buildFunctions(BoxConstraints constraints) {
return SizedBox(
width: double.infinity,
height: editorBottomBarHeight,
child: FadeInUp(
duration: fadeInDuration,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
SizedBox(
height: 90,
child: SingleChildScrollView(
controller: tuneEditor.bottomBarScrollCtrl,
scrollDirection: Axis.horizontal,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
children: List.generate(
tuneEditor.tuneAdjustmentMatrix.length, (index) {
final item = tuneEditor.tuneAdjustmentList[index];
return TuneItem(
icon: item.icon,
label: item.label,
isSelected: tuneEditor.selectedIndex == index,
value: tuneEditor.tuneAdjustmentMatrix[index].value,
max: item.max,
min: item.min,
onTap: () {
tuneEditor.setState(() {
tuneEditor.selectedIndex = index;
});
},
);
}),
),
),
),
RepaintBoundary(
child: StreamBuilder(
stream: tuneEditor.uiStream.stream,
builder: (context, snapshot) {
final activeOption =
tuneEditor.tuneAdjustmentList[tuneEditor.selectedIndex];
final activeMatrix =
tuneEditor.tuneAdjustmentMatrix[tuneEditor.selectedIndex];
return _TuneAdjustWidget(
min: activeOption.min,
max: activeOption.max,
value: activeMatrix.value,
onChanged: tuneEditor.onChanged,
);
},
),
),
],
),
),
);
}
}
class TuneItem extends StatelessWidget {
final IconData icon;
final String label;
final bool isSelected;
final double value;
final double min;
final double max;
final VoidCallback onTap;
const TuneItem({
super.key,
required this.icon,
required this.label,
required this.isSelected,
required this.value,
required this.min,
required this.max,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: SizedBox(
width: 90,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressWithValue(
value: value,
min: min,
max: max,
size: 60,
icon: icon,
isSelected: isSelected,
progressColor:
Theme.of(context).colorScheme.imageEditorPrimaryColor,
svgPath:
"assets/image-editor/image-editor-${label.toLowerCase()}.svg",
),
const SizedBox(height: 8),
Text(
label,
style: getEnteTextTheme(context).small,
textAlign: TextAlign.center,
),
],
),
),
);
}
}
class CircularProgressWithValue extends StatefulWidget {
final double value;
final double min;
final double max;
final IconData icon;
final bool isSelected;
final double size;
final Color progressColor;
final String? svgPath;
const CircularProgressWithValue({
super.key,
required this.value,
required this.min,
required this.max,
required this.icon,
required this.progressColor,
this.isSelected = false,
this.size = 60,
this.svgPath,
});
@override
State<CircularProgressWithValue> createState() =>
_CircularProgressWithValueState();
}
class _CircularProgressWithValueState extends State<CircularProgressWithValue>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _progressAnimation;
double _previousValue = 0.0;
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
animationBehavior: AnimationBehavior.preserve,
);
_progressAnimation = Tween<double>(
begin: 0.0,
end: widget.value,
).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
),
);
_animationController.forward();
}
@override
void didUpdateWidget(CircularProgressWithValue oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.value != widget.value) {
_previousValue = oldWidget.value;
_progressAnimation = Tween<double>(
begin: _previousValue,
end: widget.value,
).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
),
);
_animationController.forward(from: 0.0);
}
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
int _normalizeValueForDisplay(double value, double min, double max) {
if (min == -0.5 && max == 0.5) {
return (value * 200).round();
} else if (min == 0 && max == 1) {
return (value * 100).round();
} else if (min == -0.25 && max == 0.25) {
return (value * 400).round();
} else {
return (value * 100).round();
}
}
double _normalizeValueForProgress(double value, double min, double max) {
if (min == -0.5 && max == 0.5) {
return (value.abs() / 0.5).clamp(0.0, 1.0);
} else if (min == 0 && max == 1) {
return (value / 1.0).clamp(0.0, 1.0);
} else if (min == -0.25 && max == 0.25) {
return (value.abs() / 0.25).clamp(0.0, 1.0);
} else {
return (value.abs() / 1.0).clamp(0.0, 1.0);
}
}
bool _isClockwise(double value, double min, double max) {
if (min >= 0) {
return true;
} else {
return value >= 0;
}
}
@override
Widget build(BuildContext context) {
final colorTheme = getEnteColorScheme(context);
final textTheme = getEnteTextTheme(context);
final displayValue =
_normalizeValueForDisplay(widget.value, widget.min, widget.max);
final displayText = displayValue.toString();
final prefix = displayValue > 0 ? "+" : "";
final progressColor = widget.progressColor;
final showValue = displayValue != 0 || widget.isSelected;
return SizedBox(
width: widget.size,
height: widget.size,
child: Stack(
alignment: Alignment.center,
children: [
Container(
width: widget.size,
height: widget.size,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: showValue || widget.isSelected
? progressColor.withOpacity(0.2)
: colorTheme.backgroundElevated2,
border: Border.all(
color: widget.isSelected
? progressColor.withOpacity(0.4)
: colorTheme.backgroundElevated2,
width: 2,
),
),
),
AnimatedBuilder(
animation: _progressAnimation,
builder: (context, child) {
final animatedValue =
displayValue == 0 ? 0.0 : _progressAnimation.value;
final isClockwise =
_isClockwise(animatedValue, widget.min, widget.max);
final progressValue = _normalizeValueForProgress(
animatedValue,
widget.min,
widget.max,
);
return SizedBox(
width: widget.size,
height: widget.size,
child: CustomPaint(
painter: CircularProgressPainter(
progress: progressValue,
isClockwise: isClockwise,
color: progressColor,
),
),
);
},
),
Align(
alignment: Alignment.center,
child: showValue
? Text(
"$prefix$displayText",
style: textTheme.smallBold,
)
: widget.svgPath != null
? SvgPicture.asset(
widget.svgPath!,
width: 22,
height: 22,
fit: BoxFit.scaleDown,
colorFilter: ColorFilter.mode(
colorTheme.tabIcon,
BlendMode.srcIn,
),
)
: Icon(
widget.icon,
color: colorTheme.tabIcon,
size: 20,
),
),
],
),
);
}
}
class _TuneAdjustWidget extends StatelessWidget {
final double min;
final double max;
final double value;
final ValueChanged<double> onChanged;
const _TuneAdjustWidget({
required this.min,
required this.max,
required this.value,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
final colorScheme = getEnteColorScheme(context);
return SizedBox(
height: 40,
child: Stack(
alignment: Alignment.center,
children: [
Positioned.fill(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25),
color: colorScheme.backgroundElevated2,
),
),
),
Container(
margin: const EdgeInsets.symmetric(horizontal: 20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25),
),
child: SliderTheme(
data: SliderTheme.of(context).copyWith(
thumbShape: const _ColorPickerThumbShape(),
overlayShape: const RoundSliderOverlayShape(overlayRadius: 0),
activeTrackColor:
Theme.of(context).colorScheme.imageEditorPrimaryColor,
inactiveTrackColor: colorScheme.backgroundElevated2,
trackShape: const _CenterBasedTrackShape(),
trackHeight: 24,
),
child: Slider(
value: value,
onChanged: onChanged,
min: min,
max: max,
),
),
),
Container(
margin: const EdgeInsets.symmetric(horizontal: 38),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
width: 6,
height: 6,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: colorScheme.fillBase.withAlpha(30),
),
),
Container(
width: 6,
height: 6,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: colorScheme.fillBase.withAlpha(30),
),
),
Container(
width: 6,
height: 6,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: colorScheme.fillBase.withAlpha(30),
),
),
],
),
),
],
),
);
}
}
class _ColorPickerThumbShape extends SliderComponentShape {
const _ColorPickerThumbShape();
@override
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
return const Size(20, 20);
}
@override
void paint(
PaintingContext context,
Offset center, {
required Animation<double> activationAnimation,
required Animation<double> enableAnimation,
required bool isDiscrete,
required TextPainter labelPainter,
required RenderBox parentBox,
required Size sizeWithOverflow,
required SliderThemeData sliderTheme,
required TextDirection textDirection,
required double textScaleFactor,
required double value,
}) {
final canvas = context.canvas;
final trackRect = sliderTheme.trackShape!.getPreferredRect(
parentBox: parentBox,
offset: Offset.zero,
sliderTheme: sliderTheme,
isEnabled: true,
isDiscrete: isDiscrete,
);
final constrainedCenter = Offset(
center.dx.clamp(trackRect.left + 15, trackRect.right - 15),
center.dy,
);
final paint = Paint()
..color = Colors.white
..style = PaintingStyle.fill;
canvas.drawCircle(constrainedCenter, 15, paint);
final innerPaint = Paint()
..color = const Color.fromRGBO(8, 194, 37, 1)
..style = PaintingStyle.fill;
canvas.drawCircle(constrainedCenter, 12.5, innerPaint);
}
}
class _CenterBasedTrackShape extends SliderTrackShape {
const _CenterBasedTrackShape();
static const double horizontalPadding = 6.0;
@override
Rect getPreferredRect({
required RenderBox parentBox,
Offset offset = Offset.zero,
required SliderThemeData sliderTheme,
bool isEnabled = false,
bool isDiscrete = false,
}) {
final double trackHeight = sliderTheme.trackHeight ?? 8;
final double trackLeft = offset.dx + horizontalPadding;
final double trackTop =
offset.dy + (parentBox.size.height - trackHeight) / 2;
final double trackWidth = parentBox.size.width - (horizontalPadding * 2);
return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
}
@override
void paint(
PaintingContext context,
Offset offset, {
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required Animation<double> enableAnimation,
required TextDirection textDirection,
required Offset thumbCenter,
Offset? secondaryOffset,
bool isEnabled = false,
bool isDiscrete = false,
double? additionalActiveTrackHeight,
}) {
final Canvas canvas = context.canvas;
final Rect trackRect = getPreferredRect(
parentBox: parentBox,
offset: offset,
sliderTheme: sliderTheme,
isEnabled: isEnabled,
isDiscrete: isDiscrete,
);
final double centerX = trackRect.left + trackRect.width / 2;
final double clampedThumbDx = thumbCenter.dx.clamp(
trackRect.left,
trackRect.right,
);
final Paint inactivePaint = Paint()
..color = sliderTheme.inactiveTrackColor!
..style = PaintingStyle.fill;
final RRect inactiveRRect = RRect.fromRectAndRadius(
trackRect,
Radius.circular(trackRect.height / 2),
);
canvas.drawRRect(inactiveRRect, inactivePaint);
if (clampedThumbDx != centerX) {
final Paint activePaint = Paint()
..color = sliderTheme.activeTrackColor!
..style = PaintingStyle.fill;
final Rect activeRect = clampedThumbDx >= centerX
? Rect.fromLTWH(
centerX,
trackRect.top,
clampedThumbDx - centerX,
trackRect.height,
)
: Rect.fromLTWH(
clampedThumbDx,
trackRect.top,
centerX - clampedThumbDx,
trackRect.height,
);
final RRect activeRRect = RRect.fromRectAndRadius(
activeRect,
Radius.circular(trackRect.height / 2),
);
canvas.drawRRect(activeRRect, activePaint);
}
}
}
class CircularProgressPainter extends CustomPainter {
final double progress;
final bool isClockwise;
final Color color;
CircularProgressPainter({
required this.progress,
required this.isClockwise,
required this.color,
});
@override
void paint(Canvas canvas, Size size) {
const strokeWidth = 2.5;
final center = Offset(size.width / 2, size.height / 2);
final radius = (size.width - strokeWidth) / 2;
final backgroundPaint = Paint()
..color = Colors.transparent
..strokeWidth = strokeWidth
..style = PaintingStyle.stroke;
final foregroundPaint = Paint()
..color = color
..strokeWidth = strokeWidth
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
canvas.drawCircle(center, radius, backgroundPaint);
if (progress > 0) {
const startAngle = -pi / 2;
final sweepAngle = 2 * pi * progress * (isClockwise ? 1 : -1);
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
startAngle,
sweepAngle,
false,
foregroundPaint,
);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

View File

@@ -19,6 +19,7 @@ import "package:photos/services/local_authentication_service.dart";
import "package:photos/states/detail_page_state.dart";
import "package:photos/ui/common/fast_scroll_physics.dart";
import 'package:photos/ui/notification/toast.dart';
import "package:photos/ui/tools/editor/image_editor/image_editor_page_new.dart";
import 'package:photos/ui/tools/editor/image_editor_page.dart';
import "package:photos/ui/tools/editor/video_editor_page.dart";
import "package:photos/ui/viewer/file/file_app_bar.dart";
@@ -175,7 +176,7 @@ class _DetailPageState extends State<DetailPage> {
builder: (BuildContext context, int selectedIndex, _) {
return FileBottomBar(
_files![selectedIndex],
_onEditFileRequested,
_onNewImageEditor,
widget.config.mode == DetailPageMode.minimalistic &&
!isGuestView,
onFileRemoved: _onFileRemoved,
@@ -357,6 +358,68 @@ class _DetailPageState extends State<DetailPage> {
}
}
Future<void> _onNewImageEditor(EnteFile file) async {
if (file.uploadedFileID != null &&
file.ownerID != Configuration.instance.getUserID()) {
_logger.severe(
"Attempt to edit unowned file",
UnauthorizedEditError(),
StackTrace.current,
);
// ignore: unawaited_futures
showErrorDialog(
context,
S.of(context).sorry,
S.of(context).weDontSupportEditingPhotosAndAlbumsThatYouDont,
);
return;
}
final dialog = createProgressDialog(context, S.of(context).pleaseWait);
await dialog.show();
try {
final ioFile = await getFile(file);
if (ioFile == null) {
showShortToast(context, S.of(context).failedToFetchOriginalForEdit);
await dialog.hide();
return;
}
if (file.fileType == FileType.video) {
await dialog.hide();
replacePage(
context,
VideoEditorPage(
file: file,
ioFile: ioFile,
detailPageConfig: widget.config.copyWith(
files: _files,
selectedIndex: _selectedIndexNotifier.value,
),
),
);
return;
}
final imageProvider =
ExtendedFileImageProvider(ioFile, cacheRawData: true);
await precacheImage(imageProvider, context);
await dialog.hide();
replacePage(
context,
NewImageEditor(
originalFile: file,
file: ioFile,
detailPageConfig: widget.config.copyWith(
files: _files,
selectedIndex: _selectedIndexNotifier.value,
),
),
);
} catch (e) {
await dialog.hide();
_logger.warning("Failed to initiate edit", e);
}
}
Future<void> _onEditFileRequested(EnteFile file) async {
if (file.uploadedFileID != null &&
file.ownerID != Configuration.instance.getUserID()) {

View File

@@ -5,10 +5,10 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834
url: "https://pub.dev"
source: hosted
version: "76.0.0"
version: "72.0.0"
_flutterfire_internals:
dependency: transitive
description:
@@ -21,7 +21,7 @@ packages:
dependency: transitive
description: dart
source: sdk
version: "0.3.3"
version: "0.3.2"
adaptive_theme:
dependency: "direct main"
description:
@@ -34,10 +34,10 @@ packages:
dependency: transitive
description:
name: analyzer
sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139
url: "https://pub.dev"
source: hosted
version: "6.11.0"
version: "6.7.0"
android_intent_plus:
dependency: "direct main"
description:
@@ -317,10 +317,10 @@ packages:
dependency: "direct main"
description:
name: collection
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
url: "https://pub.dev"
source: hosted
version: "1.19.0"
version: "1.18.0"
computer:
dependency: "direct main"
description:
@@ -514,6 +514,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.17"
emoji_picker_flutter:
dependency: transitive
description:
name: emoji_picker_flutter
sha256: "08567e6f914d36c32091a96cf2f51d2558c47aa2bd47a590dc4f50e42e0965f6"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
encrypt:
dependency: "direct main"
description:
@@ -1416,18 +1424,18 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
url: "https://pub.dev"
source: hosted
version: "10.0.7"
version: "10.0.5"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
url: "https://pub.dev"
source: hosted
version: "3.0.8"
version: "3.0.5"
leak_tracker_testing:
dependency: transitive
description:
@@ -1536,10 +1544,10 @@ packages:
dependency: transitive
description:
name: macros
sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
url: "https://pub.dev"
source: hosted
version: "0.1.3-main.0"
version: "0.1.2-main.4"
maps_launcher:
dependency: "direct main"
description:
@@ -2064,6 +2072,14 @@ packages:
url: "https://github.com/eddyuan/privacy_screen.git"
source: git
version: "0.0.6"
pro_image_editor:
dependency: "direct main"
description:
name: pro_image_editor
sha256: ee86d144ec76957578fb3dc7dee3d5e9cd03383cb153eb58f531be16ac528c63
url: "https://pub.dev"
source: hosted
version: "6.0.0"
process:
dependency: transitive
description:
@@ -2309,7 +2325,7 @@ packages:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
version: "0.0.99"
source_gen:
dependency: transitive
description:
@@ -2434,10 +2450,10 @@ packages:
dependency: transitive
description:
name: stack_trace
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
url: "https://pub.dev"
source: hosted
version: "1.12.0"
version: "1.11.1"
step_progress_indicator:
dependency: "direct main"
description:
@@ -2466,10 +2482,10 @@ packages:
dependency: transitive
description:
name: string_scanner
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
version: "1.2.0"
styled_text:
dependency: "direct main"
description:
@@ -2530,26 +2546,26 @@ packages:
dependency: "direct dev"
description:
name: test
sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f"
sha256: "7ee44229615f8f642b68120165ae4c2a75fe77ae2065b1e55ae4711f6cf0899e"
url: "https://pub.dev"
source: hosted
version: "1.25.8"
version: "1.25.7"
test_api:
dependency: transitive
description:
name: test_api
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
url: "https://pub.dev"
source: hosted
version: "0.7.3"
version: "0.7.2"
test_core:
dependency: transitive
description:
name: test_core
sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d"
sha256: "55ea5a652e38a1dfb32943a7973f3681a60f872f8c3a05a14664ad54ef9c6696"
url: "https://pub.dev"
source: hosted
version: "0.6.5"
version: "0.6.4"
thermal:
dependency: "direct main"
description:
@@ -2742,6 +2758,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
vibration:
dependency: transitive
description:
name: vibration
sha256: "3b08a0579c2f9c18d5d78cb5c74f1005f731e02eeca6d72561a2e8059bf98ec3"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
vibration_platform_interface:
dependency: transitive
description:
name: vibration_platform_interface
sha256: "6ffeee63547562a6fef53c05a41d4fdcae2c0595b83ef59a4813b0612cd2bc36"
url: "https://pub.dev"
source: hosted
version: "0.0.3"
video_editor:
dependency: "direct main"
description:
@@ -2813,10 +2845,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
url: "https://pub.dev"
source: hosted
version: "14.3.0"
version: "14.2.5"
volume_controller:
dependency: transitive
description:
@@ -2877,10 +2909,10 @@ packages:
dependency: transitive
description:
name: webdriver
sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8"
sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e"
url: "https://pub.dev"
source: hosted
version: "3.0.4"
version: "3.0.3"
webkit_inspection_protocol:
dependency: transitive
description:

View File

@@ -173,6 +173,7 @@ dependencies:
git:
url: https://github.com/eddyuan/privacy_screen.git
ref: 855418e
pro_image_editor: 6.0.0
receive_sharing_intent: # pub.dev is behind
git:
url: https://github.com/KasemJaffer/receive_sharing_intent.git
@@ -342,6 +343,7 @@ flutter:
assets:
- assets/
- assets/video-editor/
- assets/image-editor/
- assets/icons/
- assets/launcher_icon/
fonts: