mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Compare commits
72 Commits
v5.4.206
...
restore-ve
Author | SHA1 | Date | |
---|---|---|---|
![]() |
8c817a4674 | ||
![]() |
cacd81f233 | ||
![]() |
061da1f02d | ||
![]() |
5202a61b0f | ||
![]() |
3e32f7278e | ||
![]() |
3a70315388 | ||
![]() |
5b5db025eb | ||
![]() |
9abad68125 | ||
![]() |
256ce50d98 | ||
![]() |
5ce68cd257 | ||
![]() |
e122c38c19 | ||
![]() |
05a8f084dc | ||
![]() |
930316e121 | ||
![]() |
90681c75cd | ||
![]() |
1140b1cd37 | ||
![]() |
f992c14893 | ||
![]() |
093df26ba5 | ||
![]() |
5731fe8d2e | ||
![]() |
93b2c7cc5f | ||
![]() |
225a6e199a | ||
![]() |
c3e587b09e | ||
![]() |
aa300b2441 | ||
![]() |
fd8eb03de3 | ||
![]() |
07d45c6be0 | ||
![]() |
4cb08642b6 | ||
![]() |
7685aaf90c | ||
![]() |
79983841b7 | ||
![]() |
b69e9df277 | ||
![]() |
5945289a2d | ||
![]() |
d4170e324a | ||
![]() |
6ff69014fc | ||
![]() |
18898c9a73 | ||
![]() |
7f347be5df | ||
![]() |
fd9390e30f | ||
![]() |
48143cc8b3 | ||
![]() |
d07a463513 | ||
![]() |
ad209ad035 | ||
![]() |
38466874e7 | ||
![]() |
148f6fc092 | ||
![]() |
6e5fad024c | ||
![]() |
7be4e7d462 | ||
![]() |
3b78dafa86 | ||
![]() |
add039e90c | ||
![]() |
100fb7c862 | ||
![]() |
0547fbf360 | ||
![]() |
49d19f1ef7 | ||
![]() |
aa7bc0cf6c | ||
![]() |
ba51819c91 | ||
![]() |
43fdc6a872 | ||
![]() |
33b442d7e3 | ||
![]() |
232dab4488 | ||
![]() |
e3daeaead5 | ||
![]() |
6526b92e88 | ||
![]() |
755b5cc87e | ||
![]() |
daf0852f52 | ||
![]() |
b540d9e0bd | ||
![]() |
cddd20103c | ||
![]() |
84256c3364 | ||
![]() |
57134a36c1 | ||
![]() |
62577c9a9a | ||
![]() |
f354de912f | ||
![]() |
a1e701829d | ||
![]() |
68141bfd87 | ||
![]() |
193a743a3f | ||
![]() |
6915f41080 | ||
![]() |
0c4c46a529 | ||
![]() |
3f3da17c72 | ||
![]() |
352ccf00e4 | ||
![]() |
f6b8768e07 | ||
![]() |
c76b49bdc2 | ||
![]() |
bdca1add48 | ||
![]() |
d00a93dedc |
143
newIDE/app/public/res/avatar/ghost.svg
Normal file
143
newIDE/app/public/res/avatar/ghost.svg
Normal file
@@ -0,0 +1,143 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="12" fill="#BF63D6" />
|
||||
<mask id="mask0_802_3803" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24">
|
||||
<circle cx="12" cy="12" r="12" fill="#D9D9D9" />
|
||||
</mask>
|
||||
<g mask="url(#mask0_802_3803)">
|
||||
<path d="M-30.3049 29.896C-33.0903 32.3379 -30.883 34.5237 -31.067 35.1913H28.0303C26.769 35.0816 30.3426 34.9444 29.4492 31.1855C28.5558 27.4266 24.2201 25.6981 24.1675 24.9299C24.115 24.1616 26.6376 21.3082 24.7193 19.4151C22.8011 17.5219 22.9325 18.0706 22.8274 15.08C22.7223 12.0894 21.0668 6.163 14.3924 5.1204C7.71805 4.0778 6.24654 6.82149 4.19692 8.98901C2.1473 11.1565 2.09475 12.6656 0.754616 13.0222C-0.585516 13.3789 -2.16214 12.3363 -3.89643 13.0222C-5.63072 13.7082 -5.57817 14.6685 -6.60298 14.6685C-7.62779 14.6685 -8.9942 13.324 -12.0949 14.6685C-14.5755 15.744 -15.2657 20.037 -15.3007 22.049C-16.3781 21.171 -17.9021 20.5125 -19.7678 20.4028C-21.6335 20.293 -23.5254 21.4728 -25.1809 24.3811C-26.8364 27.2895 -26.2845 28.4967 -26.9678 28.9357C-27.651 29.3747 -29.26 28.9799 -30.3049 29.896Z" fill="url(#paint0_radial_802_3803)" />
|
||||
<path d="M-33.5825 30.7926C-36.5733 33.3853 -34.2032 35.7061 -34.4007 36.4149H34.3205C32.9661 36.2984 36.8035 36.1528 35.8442 32.1618C34.8848 28.1708 24.966 26.3355 24.9095 25.5198C24.8531 24.7041 27.5618 21.6745 25.5021 19.6644C23.4423 17.6543 23.5834 18.237 23.4705 15.0616C23.3576 11.8863 21.58 5.59393 14.4131 4.48693C7.24625 3.37994 5.66615 6.29308 3.4653 8.59446C1.26444 10.8958 1.20801 12.4981 -0.23101 12.8768C-1.67003 13.2555 -3.363 12.1485 -5.22526 12.8768C-7.08753 13.6051 -7.0311 14.6247 -8.13153 14.6247C-9.23196 14.6247 -10.6992 13.1972 -14.0287 14.6247C-16.6923 15.7666 -17.4334 20.3247 -17.4711 22.461C-18.6279 21.5288 -20.2645 20.8296 -22.2678 20.7131C-24.2711 20.5966 -26.3027 21.8492 -28.0803 24.9372C-29.8579 28.0251 -29.2654 29.3069 -29.999 29.773C-30.7326 30.2391 -32.4604 29.82 -33.5825 30.7926Z" fill="url(#paint1_linear_802_3803)" />
|
||||
<path d="M-40 37.4944C-39.8026 36.7907 -40.4379 33.5912 -37.5546 30.8752C-35.4248 28.8688 -31.5283 29.3315 -30.7953 28.8688C-30.0623 28.4062 -30.6544 27.1338 -28.8782 24.0684C-27.1021 21.0031 -25.0722 19.7596 -23.0705 19.8753C-21.0688 19.991 -19.4336 20.685 -18.2777 21.6104C-18.2401 19.4897 -17.4996 14.965 -14.8382 13.8314C-11.5114 12.4144 -10.9924 13.0496 -9.8929 13.0496C-8.79338 13.0496 -8.96642 11.5952 -7.10569 10.8723C-5.24496 10.1493 -3.86312 11.6837 -2.42529 11.3077C-0.987453 10.9318 0.442387 10.1299 2.64143 7.84536C4.84048 5.56082 7.82952 4.31776 13.2725 5.15301C20.4335 6.2519 21.7054 9.92662 21.8181 13.0787C21.9309 16.2308 23.8174 15.9569 24.6601 18.8342C25.4731 21.6104 24.0116 23.8371 24.068 24.6468C24.1244 25.4565 30.4571 27.1166 31.4156 31.0784C32.0769 33.8117 30.8495 36.1041 30.6982 36.8625L31.4156 36.8561C30.7796 37.2343 30.6302 37.2034 30.6982 36.8625L-40 37.4944Z" fill="url(#paint2_linear_802_3803)" fill-opacity="0.4" />
|
||||
<path d="M14.018 7.16476C14.0279 7.12581 14.0578 7.0998 14.0849 7.10665C14.1119 7.11351 14.1259 7.15063 14.116 7.18958C14.1061 7.22853 14.0762 7.25455 14.0491 7.24769C14.0221 7.24084 14.0081 7.20371 14.018 7.16476Z" fill="#D1FEF9" />
|
||||
<path d="M15.6621 6.75192C15.6729 6.70939 15.7056 6.68098 15.7351 6.68846C15.7647 6.69595 15.7799 6.7365 15.7691 6.77903C15.7584 6.82156 15.7257 6.84997 15.6961 6.84248C15.6666 6.835 15.6514 6.79445 15.6621 6.75192Z" fill="#D1FEF9" />
|
||||
<path d="M15.9428 7.05576C15.9574 6.99844 16.0014 6.96014 16.0412 6.97023C16.0811 6.98032 16.1016 7.03497 16.0871 7.09229C16.0726 7.14961 16.0285 7.1879 15.9887 7.17781C15.9488 7.16772 15.9283 7.11308 15.9428 7.05576Z" fill="#D1FEF9" />
|
||||
<path d="M16.8543 6.70996C16.8609 6.684 16.8808 6.66665 16.8989 6.67122C16.9169 6.67579 16.9262 6.70054 16.9196 6.7265C16.913 6.75247 16.8931 6.76981 16.875 6.76524C16.857 6.76067 16.8477 6.73592 16.8543 6.70996Z" fill="#D1FEF9" />
|
||||
<path d="M17.8287 6.05721C17.8497 5.97442 17.9133 5.91912 17.9708 5.93369C18.0283 5.94826 18.058 6.02718 18.037 6.10996C18.016 6.19275 17.9524 6.24805 17.8949 6.23348C17.8374 6.21891 17.8077 6.13999 17.8287 6.05721Z" fill="#D1FEF9" />
|
||||
<path d="M18.0933 6.34284C18.1003 6.31502 18.1217 6.29643 18.141 6.30133C18.1604 6.30622 18.1703 6.33275 18.1633 6.36057C18.1562 6.38838 18.1348 6.40697 18.1155 6.40207C18.0962 6.39717 18.0862 6.37066 18.0933 6.34284Z" fill="#D1FEF9" />
|
||||
<path d="M16.5398 7.79474C16.5508 7.75136 16.5841 7.72238 16.6142 7.73001C16.6444 7.73765 16.6599 7.779 16.6489 7.82239C16.6379 7.86577 16.6046 7.89475 16.5744 7.88712C16.5443 7.87948 16.5288 7.83812 16.5398 7.79474Z" fill="#D1FEF9" />
|
||||
<path d="M14.5752 7.71331C14.5781 7.70188 14.5869 7.69424 14.5949 7.69625C14.6028 7.69827 14.6069 7.70916 14.604 7.72059C14.6011 7.73202 14.5923 7.73966 14.5844 7.73764C14.5764 7.73563 14.5723 7.72474 14.5752 7.71331Z" fill="#D1FEF9" />
|
||||
<path d="M13.8323 7.61426C13.8441 7.56789 13.8797 7.53692 13.9119 7.54508C13.9441 7.55324 13.9607 7.59744 13.949 7.64381C13.9373 7.69018 13.9016 7.72115 13.8694 7.71299C13.8372 7.70483 13.8206 7.66063 13.8323 7.61426Z" fill="#D1FEF9" />
|
||||
<path d="M14.7478 7.15098C14.7641 7.08669 14.8135 7.04375 14.8582 7.05506C14.9029 7.06637 14.9259 7.12766 14.9096 7.19195C14.8933 7.25625 14.8439 7.29919 14.7992 7.28788C14.7546 7.27656 14.7316 7.21527 14.7478 7.15098Z" fill="#D1FEF9" />
|
||||
<path d="M15.274 7.44532C15.279 7.42592 15.2939 7.41296 15.3073 7.41637C15.3208 7.41978 15.3278 7.43828 15.3229 7.45768C15.3179 7.47708 15.303 7.49005 15.2896 7.48663C15.2761 7.48322 15.2691 7.46472 15.274 7.44532Z" fill="#D1FEF9" />
|
||||
<path d="M15.7663 5.28356C15.7832 5.2167 15.8346 5.17204 15.881 5.18381C15.9275 5.19557 15.9514 5.25931 15.9345 5.32616C15.9175 5.39301 15.8662 5.43767 15.8197 5.4259C15.7733 5.41414 15.7493 5.35041 15.7663 5.28356Z" fill="#D1FEF9" />
|
||||
<path d="M21.5237 9.65164C21.5476 9.55734 21.6201 9.49435 21.6856 9.51094C21.7511 9.52754 21.7849 9.61744 21.761 9.71174C21.7371 9.80604 21.6647 9.86904 21.5991 9.85244C21.5336 9.83585 21.4998 9.74595 21.5237 9.65164Z" fill="#D1FEF9" />
|
||||
<path d="M16.7476 5.59271C16.7555 5.56133 16.7796 5.54037 16.8014 5.54589C16.8232 5.55141 16.8345 5.58133 16.8265 5.61271C16.8186 5.6441 16.7945 5.66506 16.7726 5.65954C16.7508 5.65402 16.7396 5.6241 16.7476 5.59271Z" fill="#D1FEF9" />
|
||||
<path d="M15.6258 5.52825C15.6332 5.49872 15.6559 5.479 15.6765 5.48419C15.697 5.48939 15.7075 5.51754 15.7001 5.54707C15.6926 5.5766 15.6699 5.59633 15.6494 5.59113C15.6289 5.58593 15.6183 5.55778 15.6258 5.52825Z" fill="#D1FEF9" />
|
||||
<path d="M16.3517 5.76635C16.3588 5.7381 16.3805 5.71924 16.4002 5.72421C16.4198 5.72918 16.4299 5.75611 16.4227 5.78435C16.4156 5.8126 16.3939 5.83147 16.3743 5.82649C16.3546 5.82152 16.3445 5.7946 16.3517 5.76635Z" fill="#D1FEF9" />
|
||||
<path d="M15.0458 5.85657C15.0568 5.81305 15.0903 5.78397 15.1205 5.79163C15.1508 5.79929 15.1664 5.84078 15.1553 5.88431C15.1443 5.92783 15.1109 5.95691 15.0806 5.94925C15.0504 5.94159 15.0348 5.9001 15.0458 5.85657Z" fill="#D1FEF9" />
|
||||
<path d="M19.9803 6.58241C19.9937 6.52921 20.0346 6.49367 20.0716 6.50303C20.1085 6.51239 20.1276 6.56311 20.1141 6.61631C20.1006 6.66951 20.0598 6.70504 20.0228 6.69568C19.9858 6.68632 19.9668 6.6356 19.9803 6.58241Z" fill="#D1FEF9" />
|
||||
<path d="M20.6122 6.79541C20.6191 6.76788 20.6403 6.74948 20.6594 6.75433C20.6786 6.75917 20.6884 6.78542 20.6814 6.81296C20.6745 6.84049 20.6533 6.85888 20.6342 6.85403C20.6151 6.84919 20.6052 6.82294 20.6122 6.79541Z" fill="#D1FEF9" />
|
||||
<path d="M21.1527 7.68672C21.1608 7.65448 21.1856 7.63294 21.208 7.63862C21.2304 7.64429 21.2419 7.67502 21.2338 7.70726C21.2256 7.7395 21.2008 7.76104 21.1784 7.75537C21.156 7.74969 21.1445 7.71896 21.1527 7.68672Z" fill="#D1FEF9" />
|
||||
<path d="M20.0877 7.40599C20.0974 7.36776 20.1268 7.34222 20.1533 7.34894C20.1799 7.35567 20.1936 7.39212 20.1839 7.43035C20.1742 7.46858 20.1448 7.49412 20.1183 7.48739C20.0917 7.48066 20.078 7.44422 20.0877 7.40599Z" fill="#D1FEF9" />
|
||||
<path d="M19.1399 7.48206C19.1489 7.44654 19.1762 7.42282 19.2009 7.42907C19.2256 7.43532 19.2383 7.46918 19.2293 7.5047C19.2203 7.54022 19.193 7.56395 19.1683 7.5577C19.1436 7.55145 19.1309 7.51758 19.1399 7.48206Z" fill="#D1FEF9" />
|
||||
<path d="M20.9697 9.98819C20.9787 9.95252 21.0062 9.9287 21.0309 9.93497C21.0557 9.94125 21.0685 9.97525 21.0594 10.0109C21.0504 10.0466 21.023 10.0704 20.9982 10.0641C20.9734 10.0579 20.9607 10.0239 20.9697 9.98819Z" fill="#D1FEF9" />
|
||||
<path d="M22.2198 9.91137C22.2307 9.86827 22.2638 9.83948 22.2938 9.84707C22.3237 9.85465 22.3392 9.89574 22.3282 9.93884C22.3173 9.98194 22.2842 10.0107 22.2542 10.0031C22.2243 9.99556 22.2089 9.95447 22.2198 9.91137Z" fill="#D1FEF9" />
|
||||
<path d="M19.3923 9.71207C19.4076 9.65148 19.4542 9.611 19.4963 9.62167C19.5384 9.63233 19.5601 9.69009 19.5448 9.75068C19.5294 9.81128 19.4828 9.85175 19.4407 9.84109C19.3986 9.83043 19.3769 9.77266 19.3923 9.71207Z" fill="#D1FEF9" />
|
||||
<path d="M19.4306 9.07896C19.4363 9.05671 19.4534 9.04184 19.4689 9.04576C19.4843 9.04967 19.4923 9.07089 19.4866 9.09314C19.481 9.1154 19.4639 9.13026 19.4484 9.12635C19.433 9.12243 19.425 9.10122 19.4306 9.07896Z" fill="#D1FEF9" />
|
||||
<path d="M20.6629 9.79212C20.6694 9.76645 20.6891 9.74929 20.7069 9.75381C20.7248 9.75833 20.734 9.78281 20.7275 9.80849C20.721 9.83417 20.7012 9.85132 20.6834 9.8468C20.6655 9.84228 20.6564 9.8178 20.6629 9.79212Z" fill="#D1FEF9" />
|
||||
<path d="M21.1683 10.2899C21.1762 10.2587 21.2002 10.2378 21.2219 10.2433C21.2436 10.2488 21.2548 10.2786 21.2469 10.3098C21.239 10.3411 21.2149 10.3619 21.1932 10.3564C21.1715 10.3509 21.1603 10.3211 21.1683 10.2899Z" fill="#D1FEF9" />
|
||||
<path d="M18.7116 6.96087C18.7224 7.00236 18.7001 7.0364 18.6619 7.03691C18.6236 7.03742 18.5839 7.0042 18.5732 6.96272C18.5624 6.92124 18.5847 6.88719 18.6229 6.88668C18.6612 6.88617 18.7009 6.91939 18.7116 6.96087Z" fill="#D1FEF9" />
|
||||
<path d="M16.8827 7.81859C16.8944 7.86389 16.8701 7.90107 16.8283 7.90163C16.7865 7.90219 16.7432 7.86591 16.7315 7.82061C16.7197 7.77531 16.7441 7.73813 16.7858 7.73758C16.8276 7.73702 16.8709 7.77329 16.8827 7.81859Z" fill="#D1FEF9" />
|
||||
<path d="M16.3465 7.59161C16.3623 7.65267 16.3295 7.70277 16.2732 7.70352C16.2169 7.70428 16.1585 7.65539 16.1426 7.59433C16.1268 7.53327 16.1596 7.48317 16.2159 7.48242C16.2722 7.48167 16.3306 7.53055 16.3465 7.59161Z" fill="#D1FEF9" />
|
||||
<path d="M15.4022 8.18338C15.4093 8.21103 15.3945 8.23373 15.369 8.23407C15.3435 8.23441 15.317 8.21226 15.3098 8.18461C15.3027 8.15695 15.3175 8.13426 15.343 8.13392C15.3685 8.13358 15.395 8.15572 15.4022 8.18338Z" fill="#D1FEF9" />
|
||||
<path d="M14.561 9.09841C14.5839 9.18658 14.5365 9.25895 14.4552 9.26003C14.3739 9.26112 14.2895 9.19051 14.2667 9.10234C14.2439 9.01416 14.2912 8.94179 14.3725 8.94071C14.4538 8.93962 14.5382 9.01023 14.561 9.09841Z" fill="#D1FEF9" />
|
||||
<path d="M14.0568 8.88541C14.0645 8.91505 14.0485 8.93937 14.0212 8.93973C13.9939 8.94009 13.9655 8.91637 13.9579 8.88674C13.9502 8.8571 13.9661 8.83279 13.9934 8.83242C14.0207 8.83206 14.0491 8.85578 14.0568 8.88541Z" fill="#D1FEF9" />
|
||||
<path d="M15.1535 7.01746C15.1654 7.06367 15.1406 7.1016 15.098 7.10216C15.0554 7.10273 15.0112 7.06573 14.9992 7.01952C14.9872 6.97331 15.0121 6.93539 15.0547 6.93482C15.0973 6.93425 15.1415 6.97125 15.1535 7.01746Z" fill="#D1FEF9" />
|
||||
<path d="M17.681 6.56592C17.6842 6.57809 17.6776 6.58809 17.6664 6.58824C17.6552 6.58839 17.6435 6.57864 17.6404 6.56646C17.6372 6.55429 17.6437 6.5443 17.655 6.54415C17.6662 6.544 17.6778 6.55374 17.681 6.56592Z" fill="#D1FEF9" />
|
||||
<path d="M18.6786 6.46253C18.6914 6.51192 18.6649 6.55245 18.6194 6.55306C18.5738 6.55367 18.5266 6.51412 18.5138 6.46473C18.501 6.41534 18.5275 6.37481 18.573 6.3742C18.6186 6.3736 18.6658 6.41314 18.6786 6.46253Z" fill="#D1FEF9" />
|
||||
<path d="M17.7979 7.1727C17.8157 7.24118 17.7789 7.29738 17.7158 7.29822C17.6526 7.29906 17.5871 7.24424 17.5693 7.17575C17.5516 7.10727 17.5884 7.05107 17.6515 7.05023C17.7147 7.04939 17.7802 7.10422 17.7979 7.1727Z" fill="#D1FEF9" />
|
||||
<path d="M16.9593 7.02196C16.9646 7.04262 16.9535 7.05958 16.9345 7.05984C16.9154 7.06009 16.8957 7.04355 16.8903 7.02288C16.885 7.00221 16.8961 6.98525 16.9151 6.985C16.9342 6.98474 16.9539 7.00129 16.9593 7.02196Z" fill="#D1FEF9" />
|
||||
<path d="M17.6252 9.3097C17.6437 9.38091 17.6054 9.43935 17.5398 9.44023C17.4741 9.4411 17.406 9.38409 17.3875 9.31288C17.3691 9.24167 17.4073 9.18323 17.473 9.18236C17.5386 9.18148 17.6068 9.2385 17.6252 9.3097Z" fill="#D1FEF9" />
|
||||
<path d="M7.75656 6.51843C7.78258 6.61888 7.72861 6.70131 7.63602 6.70255C7.54342 6.70379 7.44727 6.62336 7.42125 6.52291C7.39524 6.42246 7.44921 6.34003 7.5418 6.3388C7.63439 6.33756 7.73054 6.41799 7.75656 6.51843Z" fill="#D1FEF9" />
|
||||
<path d="M16.2025 9.26824C16.2111 9.30167 16.1932 9.3291 16.1623 9.32951C16.1315 9.32992 16.0995 9.30316 16.0909 9.26973C16.0822 9.2363 16.1002 9.20886 16.131 9.20845C16.1618 9.20804 16.1938 9.23481 16.2025 9.26824Z" fill="#D1FEF9" />
|
||||
<path d="M17.657 9.02753C17.6651 9.05899 17.6482 9.0848 17.6192 9.08519C17.5902 9.08558 17.5601 9.06039 17.552 9.02894C17.5438 8.99748 17.5607 8.97167 17.5897 8.97128C17.6187 8.9709 17.6488 8.99608 17.657 9.02753Z" fill="#D1FEF9" />
|
||||
<path d="M16.5993 8.98679C16.6071 9.01688 16.5909 9.04156 16.5632 9.04194C16.5354 9.04231 16.5066 9.01822 16.4989 8.98813C16.4911 8.95804 16.5072 8.93335 16.535 8.93298C16.5627 8.93261 16.5915 8.9567 16.5993 8.98679Z" fill="#D1FEF9" />
|
||||
<path d="M18.1936 8.54337C18.2056 8.58973 18.1807 8.62777 18.138 8.62834C18.0952 8.62891 18.0508 8.59179 18.0388 8.54543C18.0268 8.49907 18.0517 8.46102 18.0945 8.46045C18.1372 8.45988 18.1816 8.497 18.1936 8.54337Z" fill="#D1FEF9" />
|
||||
<path d="M11.5322 9.15865C11.5469 9.21532 11.5165 9.26182 11.4643 9.26252C11.412 9.26322 11.3578 9.21784 11.3431 9.16118C11.3284 9.10451 11.3589 9.05801 11.4111 9.05732C11.4633 9.05662 11.5176 9.10199 11.5322 9.15865Z" fill="#D1FEF9" />
|
||||
<path d="M10.6084 9.11803C10.616 9.14736 10.6002 9.17143 10.5732 9.17179C10.5461 9.17215 10.5181 9.14867 10.5105 9.11934C10.5029 9.09001 10.5186 9.06595 10.5457 9.06558C10.5727 9.06522 10.6008 9.0887 10.6084 9.11803Z" fill="#D1FEF9" />
|
||||
<path d="M9.39454 8.37602C9.40343 8.41036 9.38498 8.43854 9.35332 8.43896C9.32167 8.43938 9.28879 8.41189 9.2799 8.37755C9.271 8.34321 9.28946 8.31503 9.32111 8.3146C9.35277 8.31418 9.38564 8.34168 9.39454 8.37602Z" fill="#D1FEF9" />
|
||||
<path d="M10.907 8.36699C10.9175 8.40771 10.8957 8.44113 10.8581 8.44163C10.8206 8.44213 10.7816 8.40953 10.7711 8.36881C10.7605 8.32808 10.7824 8.29467 10.8199 8.29417C10.8575 8.29367 10.8964 8.32627 10.907 8.36699Z" fill="#D1FEF9" />
|
||||
<path d="M12.0577 8.0336C12.0675 8.07143 12.0472 8.10248 12.0123 8.10295C11.9775 8.10341 11.9412 8.07312 11.9314 8.03529C11.9216 7.99745 11.942 7.9664 11.9768 7.96594C12.0117 7.96547 12.0479 7.99577 12.0577 8.0336Z" fill="#D1FEF9" />
|
||||
<path d="M8.25571 6.03292C8.26555 6.07091 8.24514 6.10209 8.21012 6.10255C8.17511 6.10302 8.13874 6.0726 8.1289 6.03461C8.11906 5.99662 8.13947 5.96544 8.17449 5.96497C8.20951 5.96451 8.24587 5.99493 8.25571 6.03292Z" fill="#D1FEF9" />
|
||||
<path d="M6.72274 6.44871C6.73463 6.49462 6.70997 6.53229 6.66765 6.53285C6.62534 6.53342 6.58139 6.49666 6.5695 6.45075C6.55761 6.40485 6.58228 6.36717 6.62459 6.36661C6.66691 6.36604 6.71085 6.4028 6.72274 6.44871Z" fill="#D1FEF9" />
|
||||
<path d="M10.4114 5.88048C10.4281 5.94502 10.3934 5.99799 10.3339 5.99878C10.2744 5.99957 10.2126 5.9479 10.1959 5.88336C10.1792 5.81882 10.2139 5.76585 10.2734 5.76506C10.3329 5.76426 10.3946 5.81594 10.4114 5.88048Z" fill="#D1FEF9" />
|
||||
<path d="M10.7394 6.5218C10.7456 6.5455 10.7328 6.56496 10.711 6.56525C10.6891 6.56554 10.6664 6.54656 10.6603 6.52285C10.6541 6.49915 10.6669 6.4797 10.6887 6.47941C10.7106 6.47911 10.7333 6.49809 10.7394 6.5218Z" fill="#D1FEF9" />
|
||||
<path d="M8.76001 6.14491C8.7671 6.17226 8.7524 6.1947 8.72719 6.19504C8.70198 6.19538 8.67579 6.17348 8.66871 6.14612C8.66163 6.11877 8.67632 6.09633 8.70154 6.09599C8.72675 6.09566 8.75293 6.11755 8.76001 6.14491Z" fill="#D1FEF9" />
|
||||
<path d="M7.82555 5.78648C7.83416 5.81975 7.81628 5.84706 7.78561 5.84747C7.75493 5.84788 7.72308 5.82124 7.71446 5.78796C7.70584 5.75468 7.72372 5.72737 7.7544 5.72697C7.78507 5.72656 7.81693 5.7532 7.82555 5.78648Z" fill="#D1FEF9" />
|
||||
<path d="M8.68592 5.7983C8.69894 5.83386 8.73671 5.85273 8.77029 5.84043C8.80386 5.82814 8.82053 5.78935 8.80751 5.75379C8.79449 5.71822 8.75672 5.69936 8.72314 5.71165C8.68956 5.72394 8.6729 5.76274 8.68592 5.7983Z" fill="#D1FEF9" />
|
||||
<path d="M9.94793 5.27233C9.97231 5.33892 10.043 5.37424 10.1059 5.35122C10.1688 5.3282 10.2 5.25556 10.1756 5.18898C10.1512 5.12239 10.0805 5.08706 10.0176 5.11008C9.95476 5.1331 9.92355 5.20574 9.94793 5.27233Z" fill="#D1FEF9" />
|
||||
<path d="M9.57434 5.69829C9.59238 5.74758 9.64474 5.77372 9.69128 5.75668C9.73782 5.73965 9.76092 5.68588 9.74288 5.63658C9.72483 5.58729 9.67248 5.56114 9.62594 5.57818C9.57939 5.59522 9.55629 5.64899 9.57434 5.69829Z" fill="#D1FEF9" />
|
||||
<path d="M9.89041 4.5091C9.89978 4.53469 9.92696 4.54826 9.95112 4.53942C9.97528 4.53057 9.98727 4.50266 9.9779 4.47707C9.96853 4.45148 9.94135 4.43791 9.91719 4.44675C9.89304 4.4556 9.88104 4.48351 9.89041 4.5091Z" fill="#D1FEF9" />
|
||||
<path d="M10.8269 4.34204C10.8347 4.36329 10.8573 4.37456 10.8773 4.36721C10.8974 4.35986 10.9074 4.33669 10.8996 4.31544C10.8918 4.29419 10.8692 4.28292 10.8492 4.29026C10.8291 4.29761 10.8191 4.32079 10.8269 4.34204Z" fill="#D1FEF9" />
|
||||
<path d="M11.2846 6.72449C11.3113 6.79727 11.3886 6.83589 11.4573 6.81073C11.526 6.78557 11.5601 6.70617 11.5335 6.63338C11.5068 6.56059 11.4295 6.52198 11.3608 6.54714C11.2921 6.5723 11.258 6.6517 11.2846 6.72449Z" fill="#D1FEF9" />
|
||||
<path d="M10.79 6.58391C10.8029 6.61919 10.8404 6.6379 10.8737 6.62571C10.907 6.61352 10.9235 6.57503 10.9106 6.53976C10.8977 6.50449 10.8602 6.48578 10.8269 6.49797C10.7936 6.51016 10.7771 6.54864 10.79 6.58391Z" fill="#D1FEF9" />
|
||||
<path d="M11.7042 6.77033C11.7131 6.79461 11.7389 6.8075 11.7618 6.7991C11.7847 6.79071 11.7961 6.76421 11.7872 6.73993C11.7783 6.71564 11.7525 6.70276 11.7296 6.71115C11.7067 6.71955 11.6953 6.74604 11.7042 6.77033Z" fill="#D1FEF9" />
|
||||
<path d="M9.27454 7.15176C9.31 7.24862 9.41288 7.3 9.50433 7.26652C9.59578 7.23304 9.64117 7.12738 9.60571 7.03053C9.57025 6.93367 9.46737 6.88229 9.37592 6.91577C9.28447 6.94925 9.23908 7.05491 9.27454 7.15176Z" fill="#D1FEF9" />
|
||||
<path d="M5.98094 8.90307C6.00295 8.96318 6.06679 8.99506 6.12354 8.97428C6.18028 8.95351 6.20845 8.88794 6.18645 8.82784C6.16444 8.76773 6.1006 8.73585 6.04385 8.75663C5.98711 8.7774 5.95894 8.84297 5.98094 8.90307Z" fill="#D1FEF9" />
|
||||
<path d="M7.03975 8.92855C7.04944 8.955 7.07754 8.96903 7.10251 8.95989C7.12749 8.95075 7.13989 8.92189 7.13021 8.89543C7.12052 8.86898 7.09242 8.85495 7.06744 8.86409C7.04247 8.87324 7.03007 8.90209 7.03975 8.92855Z" fill="#D1FEF9" />
|
||||
<path d="M6.58248 7.36299C6.60702 7.43001 6.6782 7.46557 6.74149 7.4424C6.80477 7.41923 6.83617 7.34612 6.81164 7.2791C6.7871 7.21208 6.71591 7.17653 6.65263 7.19969C6.58935 7.22286 6.55794 7.29597 6.58248 7.36299Z" fill="#D1FEF9" />
|
||||
<path d="M5.01724 8.75554C5.04457 8.83021 5.12388 8.86981 5.19437 8.84401C5.26486 8.8182 5.29985 8.73675 5.27251 8.66209C5.24518 8.58743 5.16588 8.54782 5.09538 8.57363C5.02489 8.59944 4.9899 8.68088 5.01724 8.75554Z" fill="#D1FEF9" />
|
||||
<path d="M6.9081 6.89962C6.91789 6.92636 6.9463 6.94055 6.97155 6.93131C6.9968 6.92206 7.00933 6.89289 6.99954 6.86614C6.98975 6.8394 6.96134 6.82521 6.93609 6.83446C6.91084 6.8437 6.89831 6.87287 6.9081 6.89962Z" fill="#D1FEF9" />
|
||||
<path d="M14.7915 4.89137C14.7989 4.91176 14.8206 4.92257 14.8398 4.91552C14.8591 4.90847 14.8686 4.88624 14.8612 4.86586C14.8537 4.84547 14.8321 4.83466 14.8128 4.84171C14.7936 4.84875 14.784 4.87099 14.7915 4.89137Z" fill="#D1FEF9" />
|
||||
<path d="M13.3571 5.89699C13.3786 5.9558 13.4411 5.987 13.4966 5.96667C13.5521 5.94634 13.5797 5.88219 13.5581 5.82339C13.5366 5.76458 13.4742 5.73338 13.4186 5.75371C13.3631 5.77403 13.3356 5.83819 13.3571 5.89699Z" fill="#D1FEF9" />
|
||||
<path d="M12.4154 5.44996C12.4351 5.50387 12.4924 5.53246 12.5433 5.51383C12.5942 5.49519 12.6194 5.43639 12.5997 5.38249C12.58 5.32858 12.5227 5.29999 12.4718 5.31862C12.4209 5.33725 12.3957 5.39606 12.4154 5.44996Z" fill="#D1FEF9" />
|
||||
<path d="M12.9364 4.88366C12.944 4.90463 12.9663 4.91575 12.9861 4.9085C13.0059 4.90126 13.0157 4.87839 13.008 4.85743C13.0004 4.83647 12.9781 4.82535 12.9583 4.83259C12.9385 4.83984 12.9287 4.8627 12.9364 4.88366Z" fill="#D1FEF9" />
|
||||
<path d="M12.264 4.21598C12.2794 4.25807 12.3241 4.2804 12.3639 4.26585C12.4036 4.25131 12.4233 4.20539 12.4079 4.1633C12.3925 4.12121 12.3478 4.09889 12.3081 4.11344C12.2683 4.12799 12.2486 4.1739 12.264 4.21598Z" fill="#D1FEF9" />
|
||||
<path d="M11.4433 5.17329C11.4718 5.25097 11.5543 5.29218 11.6277 5.26533C11.701 5.23848 11.7374 5.15373 11.709 5.07604C11.6805 4.99836 11.598 4.95715 11.5247 4.984C11.4513 5.01085 11.4149 5.0956 11.4433 5.17329Z" fill="#D1FEF9" />
|
||||
<path d="M16.1661 5.69449C16.1877 5.75344 16.2503 5.78471 16.306 5.76433C16.3616 5.74396 16.3893 5.67965 16.3677 5.6207C16.3461 5.56175 16.2835 5.53048 16.2278 5.55086C16.1722 5.57123 16.1445 5.63554 16.1661 5.69449Z" fill="#D1FEF9" />
|
||||
<path d="M15.4353 7.42838C15.4623 7.50218 15.5407 7.54133 15.6103 7.51582C15.68 7.49031 15.7146 7.40981 15.6876 7.33601C15.6606 7.26222 15.5822 7.22307 15.5125 7.24858C15.4428 7.27409 15.4083 7.35459 15.4353 7.42838Z" fill="#D1FEF9" />
|
||||
<path d="M13.3636 7.58869C13.3763 7.62353 13.4133 7.64201 13.4462 7.62997C13.4791 7.61793 13.4954 7.57992 13.4827 7.54508C13.4699 7.51024 13.4329 7.49176 13.4 7.5038C13.3671 7.51584 13.3508 7.55385 13.3636 7.58869Z" fill="#D1FEF9" />
|
||||
<path d="M14.1174 7.01527C14.1317 7.05444 14.1733 7.07523 14.2103 7.06169C14.2473 7.04815 14.2657 7.00541 14.2513 6.96623C14.237 6.92705 14.1954 6.90627 14.1584 6.91981C14.1214 6.93335 14.103 6.97609 14.1174 7.01527Z" fill="#D1FEF9" />
|
||||
<path d="M8.74511 4.36372C8.75554 4.3922 8.78579 4.4073 8.81268 4.39746C8.83957 4.38761 8.85291 4.35655 8.84249 4.32807C8.83206 4.29959 8.80181 4.28448 8.77492 4.29433C8.74803 4.30417 8.73469 4.33524 8.74511 4.36372Z" fill="#D1FEF9" />
|
||||
<path d="M15.1843 6.07016C15.2009 6.11556 15.2492 6.13965 15.292 6.12395C15.3349 6.10826 15.3562 6.05873 15.3395 6.01333C15.3229 5.96793 15.2747 5.94384 15.2318 5.95954C15.189 5.97523 15.1677 6.02476 15.1843 6.07016Z" fill="#D1FEF9" />
|
||||
<path d="M8.65056 4.76258C8.6733 4.8247 8.73928 4.85766 8.79794 4.83619C8.85659 4.81471 8.8857 4.74695 8.86296 4.68482C8.84021 4.6227 8.77423 4.58975 8.71558 4.61122C8.65692 4.63269 8.62781 4.70046 8.65056 4.76258Z" fill="#D1FEF9" />
|
||||
<path d="M24.0627 20.161C24.0887 20.1443 24.1004 20.1161 24.0887 20.098C24.0771 20.08 24.0467 20.0789 24.0207 20.0956C23.9947 20.1123 23.983 20.1404 23.9946 20.1585C24.0062 20.1766 24.0367 20.1777 24.0627 20.161Z" fill="#D1FEF9" />
|
||||
<path d="M21.7612 17.3149C21.8175 17.2787 21.8427 17.2176 21.8176 17.1785C21.7924 17.1394 21.7264 17.137 21.6701 17.1732C21.6138 17.2094 21.5885 17.2704 21.6137 17.3096C21.6388 17.3487 21.7048 17.3511 21.7612 17.3149Z" fill="#D1FEF9" />
|
||||
<path d="M24.1018 17.6507C24.1336 17.6302 24.1479 17.5957 24.1337 17.5735C24.1195 17.5514 24.0821 17.55 24.0502 17.5705C24.0184 17.591 24.0041 17.6255 24.0183 17.6477C24.0325 17.6698 24.0699 17.6712 24.1018 17.6507Z" fill="#D1FEF9" />
|
||||
<path d="M23.8243 17.2853C23.8408 17.2747 23.8481 17.2569 23.8408 17.2455C23.8334 17.2341 23.8142 17.2334 23.7977 17.2439C23.7813 17.2545 23.7739 17.2723 23.7812 17.2837C23.7886 17.2952 23.8079 17.2959 23.8243 17.2853Z" fill="#D1FEF9" />
|
||||
<path d="M23.1251 17.1272C23.1443 17.1148 23.153 17.0939 23.1444 17.0806C23.1358 17.0672 23.1132 17.0664 23.0939 17.0787C23.0747 17.0911 23.0661 17.112 23.0747 17.1254C23.0832 17.1388 23.1058 17.1396 23.1251 17.1272Z" fill="#D1FEF9" />
|
||||
<path d="M23.5414 17.7611C23.5642 17.7465 23.5745 17.7217 23.5643 17.7058C23.5541 17.69 23.5273 17.689 23.5045 17.7037C23.4816 17.7184 23.4714 17.7431 23.4816 17.759C23.4918 17.7748 23.5186 17.7758 23.5414 17.7611Z" fill="#D1FEF9" />
|
||||
<path d="M23.6982 18.3962C23.7194 18.3825 23.7289 18.3595 23.7195 18.3448C23.71 18.3301 23.6851 18.3292 23.6639 18.3428C23.6427 18.3564 23.6332 18.3794 23.6426 18.3942C23.6521 18.4089 23.677 18.4098 23.6982 18.3962Z" fill="#D1FEF9" />
|
||||
<path d="M21.6627 17.75C21.684 17.7364 21.6936 17.7133 21.6841 17.6985C21.6746 17.6837 21.6496 17.6828 21.6283 17.6964C21.607 17.7101 21.5974 17.7332 21.6069 17.748C21.6165 17.7628 21.6414 17.7637 21.6627 17.75Z" fill="#D1FEF9" />
|
||||
<path d="M21.4402 16.9173C21.466 16.9008 21.4776 16.8728 21.466 16.8548C21.4545 16.8369 21.4242 16.8358 21.3984 16.8524C21.3726 16.869 21.361 16.897 21.3726 16.9149C21.3841 16.9328 21.4144 16.9339 21.4402 16.9173Z" fill="#D1FEF9" />
|
||||
<path d="M22.1882 18.7186C22.2244 18.6953 22.2407 18.6561 22.2245 18.6309C22.2083 18.6058 22.1659 18.6043 22.1297 18.6275C22.0935 18.6508 22.0773 18.69 22.0935 18.7152C22.1096 18.7403 22.1521 18.7418 22.1882 18.7186Z" fill="#D1FEF9" />
|
||||
<path d="M22.5923 18.5555C22.6056 18.5469 22.6116 18.5325 22.6056 18.5233C22.5997 18.514 22.5841 18.5135 22.5708 18.522C22.5575 18.5306 22.5516 18.545 22.5575 18.5542C22.5634 18.5634 22.579 18.564 22.5923 18.5555Z" fill="#D1FEF9" />
|
||||
<path d="M21.8579 17.9075C21.8732 17.8976 21.8801 17.881 21.8732 17.8704C21.8664 17.8597 21.8484 17.859 21.8331 17.8689C21.8177 17.8788 21.8108 17.8954 21.8177 17.906C21.8245 17.9167 21.8425 17.9173 21.8579 17.9075Z" fill="#D1FEF9" />
|
||||
<path d="M21.4228 17.6865C21.4415 17.6745 21.4499 17.6543 21.4415 17.6413C21.4332 17.6283 21.4113 17.6276 21.3927 17.6395C21.374 17.6515 21.3656 17.6718 21.374 17.6847C21.3823 17.6977 21.4042 17.6985 21.4228 17.6865Z" fill="#D1FEF9" />
|
||||
<path d="M0.689831 14.2559C0.729755 14.2302 0.747667 14.1869 0.729838 14.1592C0.712009 14.1315 0.66519 14.1298 0.625266 14.1554C0.585342 14.1811 0.567432 14.2244 0.585261 14.2521C0.60309 14.2799 0.649907 14.2815 0.689831 14.2559Z" fill="#D1FEF9" />
|
||||
<path d="M0.2736 13.6829C0.292342 13.6709 0.300749 13.6506 0.292378 13.6375C0.284008 13.6245 0.262029 13.6237 0.243287 13.6358C0.224545 13.6478 0.216136 13.6681 0.224507 13.6812C0.232877 13.6942 0.254857 13.695 0.2736 13.6829Z" fill="#D1FEF9" />
|
||||
<path d="M0.561061 14.4012C0.578696 14.3898 0.586609 14.3707 0.578733 14.3585C0.570857 14.3462 0.550176 14.3455 0.532541 14.3568C0.514906 14.3681 0.506995 14.3873 0.514871 14.3995C0.522746 14.4118 0.543426 14.4125 0.561061 14.4012Z" fill="#D1FEF9" />
|
||||
<path d="M0.247276 13.9798C0.264144 13.969 0.271711 13.9507 0.264178 13.939C0.256644 13.9273 0.236864 13.9265 0.219995 13.9374C0.203127 13.9482 0.19556 13.9665 0.203093 13.9782C0.210627 13.99 0.230408 13.9907 0.247276 13.9798Z" fill="#D1FEF9" />
|
||||
<path d="M0.473643 14.8514C0.499713 14.8346 0.511408 14.8064 0.499766 14.7882C0.488123 14.7701 0.457552 14.769 0.431482 14.7858C0.405412 14.8025 0.393715 14.8308 0.405358 14.8489C0.417 14.867 0.447573 14.8681 0.473643 14.8514Z" fill="#D1FEF9" />
|
||||
<path d="M-31.0664 35.7671C-30.8823 35.0598 -33.0915 32.7442 -30.3037 30.1573C-29.2578 29.1868 -27.6473 29.605 -26.9635 29.14C-26.2797 28.6749 -26.832 27.396 -25.1751 24.315C-23.5181 21.2339 -21.6245 19.9841 -19.7571 20.1004C-17.8898 20.2166 -16.3644 20.9142 -15.286 21.8443C-15.251 19.7128 -14.5601 15.1649 -12.0774 14.0255C-8.9739 12.6013 -7.60626 14.0255 -6.58054 14.0255C-5.55482 14.0255 -5.60741 13.0082 -3.87158 12.2815C-2.13574 11.5549 -0.557701 12.6594 0.78363 12.2815C2.12496 11.9037 2.17756 10.305 4.22901 8.00882C6.28045 5.71259 7.75329 2.80596 14.4336 3.91048C21.114 5.015 23.5862 10.305 23.6914 13.4733C23.7966 16.6415 23.7703 16.3508 25.6903 18.3564C27.6102 20.362 25.6377 23.5011 25.6903 24.315C25.7429 25.1288 33.2649 25.7973 33.2649 35.7089" stroke="#5EE1EA" stroke-width="0.0719697" />
|
||||
<path d="M19.7091 13.744C15.8048 13.744 14.2198 15.7564 13.4383 17.668C13.7899 18.0347 15.0035 18.7127 17.0907 18.2685C19.1779 17.8243 19.7098 15.0485 19.7091 13.744Z" fill="#D9D9D9" />
|
||||
<mask id="mask1_802_3803" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="14" y="15" width="4" height="4">
|
||||
<path d="M14.2304 16.9102C14.1334 17.9627 14.7088 18.8764 15.6627 18.9643C16.6167 19.0523 17.3495 18.2593 17.4466 17.2067C17.5436 16.1541 16.9076 15.235 16.0142 15.1526C15.1209 15.0702 14.3275 15.8576 14.2304 16.9102Z" fill="black" />
|
||||
</mask>
|
||||
<g mask="url(#mask1_802_3803)">
|
||||
<path d="M19.7644 13.804C16.1945 12.4692 14.0964 15.8638 13.4935 17.728C13.8451 18.0947 15.0701 18.717 17.1573 18.2728C19.2445 17.8286 19.765 15.1085 19.7644 13.804Z" fill="black" />
|
||||
</g>
|
||||
<path d="M5.73062 10.3736C8.59696 11.8091 9.58844 15.009 9.16976 16.9229C8.6813 17.0625 7.30963 16.9827 5.73062 15.5473C4.15161 14.1118 5.0727 11.5 5.73062 10.3736Z" fill="#D9D9D9" />
|
||||
<mask id="mask2_802_3803" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="5" y="13" width="5" height="5">
|
||||
<path d="M9.10492 15.1133C9.10492 16.1704 8.44797 17.0273 7.49002 17.0273C6.53207 17.0273 5.87512 16.1704 5.87512 15.1133C5.87512 14.0563 6.59286 13.1994 7.49002 13.1994C8.38719 13.1994 9.10492 14.0563 9.10492 15.1133Z" fill="#4A36C3" />
|
||||
</mask>
|
||||
<g mask="url(#mask2_802_3803)">
|
||||
<path d="M5.78555 10.4331C9.54169 11.0791 9.64337 15.0685 9.22469 16.9824C8.73624 17.122 7.36457 17.0423 5.78555 15.6068C4.20654 14.1713 5.12763 11.5596 5.78555 10.4331Z" fill="#4A36C3" />
|
||||
</g>
|
||||
<path d="M7.77164 14.1144C7.7835 14.1026 7.80367 14.1082 7.80777 14.1244L7.97244 14.7747C7.97401 14.7809 7.97822 14.786 7.98395 14.7888L8.60374 15.0894C8.61931 15.097 8.62012 15.1189 8.60516 15.1275L8.02188 15.4661C8.01614 15.4694 8.01226 15.4752 8.01133 15.4817L7.91912 16.1384C7.91676 16.1552 7.89689 16.1628 7.88389 16.1519L7.35699 15.7115C7.35222 15.7075 7.34596 15.7058 7.33982 15.7067L6.66129 15.8126C6.64455 15.8152 6.63146 15.7984 6.63809 15.7828L6.89754 15.1726C6.90014 15.1665 6.89979 15.1595 6.89659 15.1537L6.57125 14.563C6.56291 14.5478 6.57488 14.5295 6.59211 14.531L7.27818 14.5929C7.28452 14.5935 7.2908 14.5912 7.29532 14.5867L7.77164 14.1144Z" fill="#2D1D8E" />
|
||||
</g>
|
||||
<defs>
|
||||
<radialGradient id="paint0_radial_802_3803" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(12.2114 4.21498) rotate(92.148) scale(30.147 61.0177)">
|
||||
<stop stop-color="#15C4E9" stop-opacity="0.79" />
|
||||
<stop offset="0.760998" stop-color="#1906EE" stop-opacity="0.5" />
|
||||
<stop offset="1" stop-color="#59DFFC" stop-opacity="0" />
|
||||
</radialGradient>
|
||||
<linearGradient id="paint1_linear_802_3803" x1="13.3682" y1="2.16805" x2="16.196" y2="43.34" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#8CFFEA" stop-opacity="0.36" />
|
||||
<stop offset="0.78125" stop-color="#CDFF8C" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_802_3803" x1="12.5361" y1="1.46598" x2="20.7414" y2="27.7465" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#CDFF8C" stop-opacity="0.36" />
|
||||
<stop offset="0.78125" stop-color="#CDFF8C" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 30 KiB |
50
newIDE/app/public/res/avatar/green-hero.svg
Normal file
50
newIDE/app/public/res/avatar/green-hero.svg
Normal file
@@ -0,0 +1,50 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="12" fill="#F076FA" />
|
||||
<mask id="mask0_802_3805" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24">
|
||||
<circle cx="12" cy="12" r="12" fill="#D9D9D9" />
|
||||
</mask>
|
||||
<g mask="url(#mask0_802_3805)">
|
||||
<path d="M0.888631 22.0665C0.888631 23.1711 1.58404 24.0665 2.68861 24.0665C3.79318 24.0665 4.12285 23.5979 4.68853 22.4665C4.88853 22.0666 5.48852 20.4666 5.48852 20.4666C5.48852 20.4666 4.72282 19.2971 4.28854 18.2666C3.77023 17.0367 3.08855 14.2666 3.08855 14.2666C2.48856 15.4666 0.888631 20.962 0.888631 22.0665Z" fill="#16CF89" />
|
||||
<path d="M0.888631 22.0664C0.888631 23.1709 1.58404 24.0663 2.68861 24.0663C3.79318 24.0663 4.12285 23.5977 4.68853 22.4663C4.88853 22.0664 5.48852 20.4664 5.48852 20.4664C5.48852 20.4664 4.72282 19.2969 4.28854 18.2664C3.77023 17.0365 3.08855 14.2664 3.08855 14.2664C2.48856 15.4664 0.888631 20.9618 0.888631 22.0664Z" fill="url(#paint0_linear_802_3805)" />
|
||||
<path d="M2.68781 15.2664C2.68781 15.2664 2.68855 19.2664 5.02614 21.6664C5.25177 21.0977 5.48852 20.4664 5.48852 20.4664C5.48852 20.4664 4.72282 19.2969 4.28854 18.2664C3.77023 17.0365 3.08855 14.2664 3.08855 14.2664C2.98176 14.48 2.84329 14.8297 2.68781 15.2664Z" fill="#278050" />
|
||||
<path d="M18.4884 19.4666C18.4884 19.4666 17.3677 20.3373 16.4884 20.8666C15.4816 21.4726 13.4884 21.6666 13.4884 21.6666C13.4884 21.6666 13.3939 24.043 13.4884 24.9548C13.583 25.8665 13.6884 26.8665 13.6884 26.8665C13.6884 26.8665 13.1653 27.63 12.8884 28.0665C12.5017 28.6762 12.2884 29.6665 12.2884 29.6665L19.0884 29.6665C19.0884 29.6665 19.0839 28.4924 18.8884 27.7801C18.7721 27.3564 18.4884 26.7321 18.4884 26.7321C18.2884 25.4744 18.0884 24.8891 18.0884 23.8665C18.0884 22.8579 18.2884 20.6666 18.4884 19.4666Z" fill="url(#paint1_linear_802_3805)" />
|
||||
<path d="M18.4885 19.6668C18.4885 19.6668 16.9678 21.7375 16.0886 22.2668C15.0817 22.8728 13.4886 21.6668 13.4886 21.6668C13.4886 21.6668 13.4394 22.9032 13.4474 23.9346C13.4489 24.1262 13.449 24.3029 13.4532 24.4668C13.4532 24.4668 15.8886 24.0668 18.2261 21.7107C18.2326 21.6463 18.2392 21.5818 18.2459 21.5173C18.3161 20.8482 18.4023 20.184 18.4885 19.6668Z" fill="#278050" />
|
||||
<path d="M10.2883 22.0672C10.2883 22.0672 8.48835 21.7753 7.68836 21.2672C6.88837 20.7591 6.08837 20.0672 6.08837 20.0672C6.08837 20.0672 5.17264 22.0774 4.88839 24.0672C4.68839 25.4672 4.88839 26.8671 4.88839 26.8671C4.88839 26.8671 4.36528 27.6307 4.08839 28.0671C3.70163 28.6768 3.4884 29.6671 3.4884 29.6671L10.2883 29.6671C10.2883 29.6671 10.2838 28.493 10.0883 27.7807C9.97207 27.3571 9.68834 26.7327 9.68834 26.7327C9.28834 25.6672 9.68834 24.0672 9.68834 24.0672C9.68834 24.0672 10.0883 22.4672 10.2883 22.0672Z" fill="url(#paint2_linear_802_3805)" />
|
||||
<path d="M10.2883 22.0672C10.2883 22.0672 8.48835 21.7753 7.68836 21.2672C6.88837 20.7591 6.08837 20.0672 6.08837 20.0672C6.08837 20.0672 5.80308 20.6935 5.50154 21.5929C5.50154 21.5929 5.48838 21.8672 6.68837 22.8672C7.88835 23.8672 9.60838 24.4672 9.60838 24.4672C9.64965 24.2219 9.68834 24.0672 9.68834 24.0672C9.68834 24.0672 10.0883 22.4672 10.2883 22.0672Z" fill="#278050" />
|
||||
<circle cx="9.1999" cy="9.1999" r="9.1999" transform="matrix(-1 -8.74228e-08 -8.74228e-08 1 21.0879 5.66664)" fill="url(#paint3_linear_802_3805)" />
|
||||
<rect width="5.80281" height="5.9876" rx="0.629083" transform="matrix(-0.686366 -0.727256 -0.727256 0.686366 12.2881 14.287)" fill="black" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.3113 13.2521L10.1312 12.0017C9.99873 11.9751 9.8595 11.9609 9.71585 11.9609C8.83221 11.9609 8.11587 12.4981 8.11587 13.1609C8.11587 13.8236 8.8322 14.3609 9.71585 14.3609C10.5586 14.3609 11.2491 13.8722 11.3113 13.2521Z" fill="white" />
|
||||
<circle cx="9.1999" cy="9.1999" r="9.1999" transform="matrix(-1 -8.74228e-08 -8.74228e-08 1 21.0879 5.66664)" fill="url(#paint4_linear_802_3805)" />
|
||||
<rect width="5.80281" height="5.9876" rx="0.629083" transform="matrix(-0.686366 -0.727256 -0.727256 0.686366 11.4883 14.287)" fill="black" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.5112 13.252L9.33128 12.0017C9.19878 11.9751 9.0595 11.9609 8.91579 11.9609C8.03214 11.9609 7.31581 12.4981 7.31581 13.1609C7.31581 13.8236 8.03214 14.3609 8.91579 14.3609C9.75856 14.3609 10.4491 13.8722 10.5112 13.252Z" fill="white" />
|
||||
<path d="M18.4886 21.2665C18.3286 21.2665 17.5553 17.2665 17.2886 15.6665C17.2886 15.6665 18.8219 19.1332 19.2886 20.4665L18.4886 21.2665Z" fill="#007156" />
|
||||
<path d="M19.4883 23.2664C20.0539 24.3977 21.2883 24.6664 21.8883 24.4664C22.4883 24.2664 23.2883 23.8664 23.4882 22.6664C23.6882 21.4664 23.1209 19.6385 22.4883 17.2664C21.4883 14.4665 21.2883 13.4665 20.6883 12.2665L17.0883 14.8665C17.4883 16.2665 19.0883 22.4664 19.4883 23.2664Z" fill="url(#paint5_linear_802_3805)" />
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_802_3805" x1="2.48856" y1="16.2664" x2="1.88856" y2="24.0663" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#1EB67C" />
|
||||
<stop offset="1" stop-color="#1FF073" />
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_802_3805" x1="15.8884" y1="23.6665" x2="15.8884" y2="29.6665" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#14B678" />
|
||||
<stop offset="0.8125" stop-color="#1FF073" />
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_802_3805" x1="6.88837" y1="22.6672" x2="6.88836" y2="29.6671" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#00AB6A" />
|
||||
<stop offset="1" stop-color="#1FF073" />
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_802_3805" x1="14.1999" y1="0.799991" x2="3.79996" y2="15.5998" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.234375" stop-color="#16CF89" />
|
||||
<stop offset="1" stop-color="#14B578" />
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_802_3805" x1="15.5998" y1="2.39998" x2="7.59992" y2="18.1998" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#1FF073" />
|
||||
<stop offset="1" stop-color="#04795D" />
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_802_3805" x1="18.2883" y1="16.6664" x2="22.4882" y2="24.4664" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0FA866" />
|
||||
<stop offset="0.994297" stop-color="#1FF073" />
|
||||
<stop offset="0.994397" stop-color="#1EB57C" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 6.2 KiB |
18
newIDE/app/public/res/avatar/pink-cloud.svg
Normal file
18
newIDE/app/public/res/avatar/pink-cloud.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="12" fill="#98DE24" />
|
||||
<mask id="mask0_802_3806" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24">
|
||||
<circle cx="12" cy="12" r="12" fill="#D9D9D9" />
|
||||
</mask>
|
||||
<g mask="url(#mask0_802_3806)">
|
||||
<path d="M11.2419 5.72486C17.5737 5.64156 18.696 13.7122 18.3934 15.1801C22.8125 16.2933 21.4953 20.141 20.2299 21.2151L20.2118 21.2304C19.3473 21.9643 17.3107 23.6933 13.5 24C9.66253 24.3089 5.76283 23.21 4.72142 22.8134C3.68001 22.4168 2.25934 22.1932 1.8536 19.5397C1.52901 17.417 3.31222 16.2371 4.11815 15.8806C4.25119 12.1545 4.91003 5.80817 11.2419 5.72486Z" fill="url(#paint0_linear_802_3806)" />
|
||||
<ellipse cx="0.846336" cy="0.911439" rx="0.846336" ry="0.911439" transform="matrix(-0.914359 0.404904 0.404904 0.914359 16.0637 13.1708)" fill="#3B314F" />
|
||||
<ellipse cx="0.846336" cy="0.911439" rx="0.846336" ry="0.911439" transform="matrix(-0.914359 0.404904 0.404904 0.914359 8.98706 13.1708)" fill="#3B314F" />
|
||||
<path d="M20.0001 5.00003C20.3655 6.93178 20.8772 12.2833 20.0001 18.2351" stroke="#CF0A80" stroke-width="0.918557" stroke-linecap="round" />
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_802_3806" x1="14.7489" y1="6.37278" x2="10.2511" y2="24.2265" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F783F2" />
|
||||
<stop offset="1" stop-color="#E040DA" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
32
newIDE/app/public/res/avatar/red-hero.svg
Normal file
32
newIDE/app/public/res/avatar/red-hero.svg
Normal file
@@ -0,0 +1,32 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="12" fill="#7953F5" />
|
||||
<mask id="mask0_802_3804" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24">
|
||||
<circle cx="12" cy="12" r="12" fill="#D9D9D9" />
|
||||
</mask>
|
||||
<g mask="url(#mask0_802_3804)">
|
||||
<path d="M22.289 13.9245C24.569 15.2838 19.6003 21.2452 18.8884 21.8363C16.5763 21.5498 17.8171 18.1535 17.8464 17.0701C20.6304 14.8783 20.6681 12.9582 22.289 13.9245Z" fill="url(#paint0_linear_802_3804)" />
|
||||
<path d="M1.71126 13.9245C-0.568805 15.2838 4.39996 21.2452 5.11188 21.8363C7.4239 21.5498 6.18319 18.1535 6.15385 17.0701C3.36985 14.8783 3.33216 12.9582 1.71126 13.9245Z" fill="url(#paint1_linear_802_3804)" />
|
||||
<rect x="4.81934" y="4.76291" width="14.2048" height="24.5704" rx="7.10239" fill="url(#paint2_linear_802_3804)" />
|
||||
<path d="M12.1138 10.1089C15.3704 10.1089 17.8247 13.2146 17.8247 13.2146C17.8247 13.2146 15.3053 16.3202 12.1138 16.3202C8.92224 16.3202 6.40282 13.2146 6.40282 13.2146C6.40282 13.2146 8.85711 10.109 12.1138 10.1089Z" fill="white" />
|
||||
<mask id="mask1_802_3804" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="6" y="10" width="12" height="7">
|
||||
<path d="M12.1138 10.1088C15.3704 10.1088 17.8247 13.2145 17.8247 13.2145C17.8247 13.2145 15.3053 16.3201 12.1138 16.3201C8.92224 16.3201 6.40282 13.2144 6.40282 13.2144C6.40282 13.2144 8.85711 10.1089 12.1138 10.1088Z" fill="white" />
|
||||
</mask>
|
||||
<g mask="url(#mask1_802_3804)">
|
||||
<circle cx="12.113" cy="13.2306" r="3.45873" fill="#240521" />
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_802_3804" x1="22.9707" y1="14.709" x2="17.3558" y2="20.9165" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E04D40" />
|
||||
<stop offset="1" stop-color="#F15F56" />
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_802_3804" x1="1.02957" y1="14.709" x2="6.64449" y2="20.9165" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E04D40" />
|
||||
<stop offset="1" stop-color="#F15F56" />
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_802_3804" x1="11.9217" y1="4.76291" x2="11.9217" y2="29.3333" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF7A77" />
|
||||
<stop offset="1" stop-color="#DE5043" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 2.3 KiB |
@@ -71,7 +71,7 @@ export type RenderEditorContainerProps = {|
|
||||
// Project opening
|
||||
canOpen: boolean,
|
||||
onChooseProject: () => void,
|
||||
onOpenRecentFile: (file: FileMetadataAndStorageProviderName) => void,
|
||||
onOpenRecentFile: (file: FileMetadataAndStorageProviderName) => Promise<void>,
|
||||
onOpenProjectManager: () => void,
|
||||
onCloseProject: () => Promise<boolean>,
|
||||
|
||||
|
@@ -203,7 +203,7 @@ type ProjectFileListItemProps = {|
|
||||
currentFileMetadata: ?FileMetadata,
|
||||
lastModifiedInfo?: LastModifiedInfo | null,
|
||||
storageProviders: Array<StorageProvider>,
|
||||
onOpenRecentFile: (file: FileMetadataAndStorageProviderName) => void,
|
||||
onOpenRecentFile: (file: FileMetadataAndStorageProviderName) => Promise<void>,
|
||||
isWindowWidthMediumOrLarger: boolean,
|
||||
hideDeleteContextMenuAction?: boolean,
|
||||
onManageGame?: ({ gameId: string }) => void,
|
||||
|
@@ -69,7 +69,7 @@ type Props = {|
|
||||
currentFileMetadata: ?FileMetadata,
|
||||
canOpen: boolean,
|
||||
onChooseProject: () => void,
|
||||
onOpenRecentFile: (file: FileMetadataAndStorageProviderName) => void,
|
||||
onOpenRecentFile: (file: FileMetadataAndStorageProviderName) => Promise<void>,
|
||||
onOpenNewProjectSetupDialog: () => void,
|
||||
onShowAllExamples: () => void,
|
||||
onSelectExampleShortHeader: (exampleShortHeader: ExampleShortHeader) => void,
|
||||
|
@@ -41,7 +41,7 @@ type Props = {|
|
||||
user: User,
|
||||
currentFileMetadata: ?FileMetadata,
|
||||
onClickBack: () => void,
|
||||
onOpenRecentFile: (file: FileMetadataAndStorageProviderName) => void,
|
||||
onOpenRecentFile: (file: FileMetadataAndStorageProviderName) => Promise<void>,
|
||||
storageProviders: Array<StorageProvider>,
|
||||
projects: ?Array<CloudProjectWithUserAccessInfo>,
|
||||
onRefreshProjects: (user: User) => Promise<void>,
|
||||
|
@@ -55,7 +55,7 @@ const DropTarget = makeDropTarget('team-groups');
|
||||
type Props = {|
|
||||
project: ?gdProject,
|
||||
currentFileMetadata: ?FileMetadata,
|
||||
onOpenRecentFile: (file: FileMetadataAndStorageProviderName) => void,
|
||||
onOpenRecentFile: (file: FileMetadataAndStorageProviderName) => Promise<void>,
|
||||
storageProviders: Array<StorageProvider>,
|
||||
|};
|
||||
|
||||
|
@@ -96,7 +96,7 @@ type Props = {|
|
||||
// Project opening
|
||||
canOpen: boolean,
|
||||
onChooseProject: () => void,
|
||||
onOpenRecentFile: (file: FileMetadataAndStorageProviderName) => void,
|
||||
onOpenRecentFile: (file: FileMetadataAndStorageProviderName) => Promise<void>,
|
||||
onOpenExampleStore: () => void,
|
||||
onOpenExampleStoreWithExampleShortHeader: ExampleShortHeader => void,
|
||||
onOpenExampleStoreWithPrivateGameTemplateListingData: (
|
||||
|
@@ -138,6 +138,11 @@ const ElectronMainMenu = ({
|
||||
callback: callbacks.onSaveProjectAs,
|
||||
shouldApply: isFocusedOnMainWindow,
|
||||
});
|
||||
useIPCEventListener({
|
||||
ipcEvent: 'main-menu-show-version-history',
|
||||
callback: callbacks.onShowVersionHistory,
|
||||
shouldApply: isFocusedOnMainWindow,
|
||||
});
|
||||
useIPCEventListener({
|
||||
ipcEvent: 'main-menu-close',
|
||||
callback:
|
||||
|
@@ -28,9 +28,10 @@ export type MainMenuCallbacks = {|
|
||||
onChooseProject: () => void,
|
||||
onOpenRecentFile: (
|
||||
fileMetadataAndStorageProviderName: FileMetadataAndStorageProviderName
|
||||
) => void,
|
||||
) => Promise<void>,
|
||||
onSaveProject: () => Promise<void>,
|
||||
onSaveProjectAs: () => void,
|
||||
onShowVersionHistory: () => void,
|
||||
onCloseProject: () => Promise<boolean>,
|
||||
onCloseApp: () => void,
|
||||
onExportProject: () => void,
|
||||
@@ -57,6 +58,7 @@ export type MainMenuEvent =
|
||||
| 'main-menu-open-recent'
|
||||
| 'main-menu-save'
|
||||
| 'main-menu-save-as'
|
||||
| 'main-menu-show-version-history'
|
||||
| 'main-menu-close'
|
||||
| 'main-menu-close-app'
|
||||
| 'main-menu-export'
|
||||
@@ -82,6 +84,7 @@ const getMainMenuEventCallback = (
|
||||
'main-menu-open-recent': callbacks.onOpenRecentFile,
|
||||
'main-menu-save': callbacks.onSaveProject,
|
||||
'main-menu-save-as': callbacks.onSaveProjectAs,
|
||||
'main-menu-show-version-history': callbacks.onShowVersionHistory,
|
||||
'main-menu-close': callbacks.onCloseProject,
|
||||
'main-menu-close-app': callbacks.onCloseApp,
|
||||
'main-menu-export': callbacks.onExportProject,
|
||||
@@ -168,6 +171,11 @@ export const buildMainMenuDeclarativeTemplate = ({
|
||||
onClickSendEvent: 'main-menu-save-as',
|
||||
enabled: !!project,
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Show version history`),
|
||||
onClickSendEvent: 'main-menu-show-version-history',
|
||||
enabled: !!project,
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: i18n._(t`Invite collaborators`),
|
||||
|
@@ -26,6 +26,7 @@ export type PreviewAndShareButtonsProps = {|
|
||||
hasPreviewsRunning: boolean,
|
||||
previewState: PreviewState,
|
||||
openShareDialog: () => void,
|
||||
isSharingEnabled: boolean,
|
||||
|};
|
||||
|
||||
const PreviewAndShareButtons = React.memo<PreviewAndShareButtonsProps>(
|
||||
@@ -40,6 +41,7 @@ const PreviewAndShareButtons = React.memo<PreviewAndShareButtonsProps>(
|
||||
previewState,
|
||||
setPreviewOverride,
|
||||
openShareDialog,
|
||||
isSharingEnabled,
|
||||
}: PreviewAndShareButtonsProps) {
|
||||
const windowWidth = useResponsiveWindowWidth();
|
||||
const isMobileScreen = windowWidth === 'small';
|
||||
@@ -152,6 +154,7 @@ const PreviewAndShareButtons = React.memo<PreviewAndShareButtonsProps>(
|
||||
<ResponsiveRaisedButton
|
||||
primary
|
||||
onClick={onShareClick}
|
||||
disabled={!isSharingEnabled}
|
||||
icon={<PublishIcon />}
|
||||
label={<Trans>Share</Trans>}
|
||||
// This ID is used for guided lessons, let's keep it stable.
|
||||
|
@@ -9,6 +9,12 @@ import ProjectManagerIcon from '../../UI/CustomSvgIcons/ProjectManager';
|
||||
import FloppyIcon from '../../UI/CustomSvgIcons/Floppy';
|
||||
import IconButton from '../../UI/IconButton';
|
||||
import { Spacer } from '../../UI/Grid';
|
||||
import HistoryIcon from '../../UI/CustomSvgIcons/History';
|
||||
import OpenedVersionStatusChip from '../../VersionHistory/OpenedVersionStatusChip';
|
||||
import type { OpenedVersionStatus } from '../../VersionHistory';
|
||||
import GDevelopThemeContext from '../../UI/Theme/GDevelopThemeContext';
|
||||
import { getStatusColor } from '../../VersionHistory/Utils';
|
||||
import { useResponsiveWindowWidth } from '../../UI/Reponsive/ResponsiveWindowMeasurer';
|
||||
|
||||
export type MainFrameToolbarProps = {|
|
||||
showProjectButtons: boolean,
|
||||
@@ -16,6 +22,10 @@ export type MainFrameToolbarProps = {|
|
||||
openShareDialog: () => void,
|
||||
onSave: () => Promise<void>,
|
||||
canSave: boolean,
|
||||
showVersionHistoryButton: boolean,
|
||||
onOpenVersionHistory: () => void,
|
||||
checkedOutVersionStatus?: ?OpenedVersionStatus,
|
||||
onQuitVersionHistory?: () => Promise<void>,
|
||||
|
||||
...PreviewAndShareButtonsProps,
|
||||
|};
|
||||
@@ -27,11 +37,17 @@ export type ToolbarInterface = {|
|
||||
type LeftButtonsToolbarGroupProps = {|
|
||||
toggleProjectManager: () => void,
|
||||
onSave: () => Promise<void>,
|
||||
showVersionHistoryButton: boolean,
|
||||
onOpenVersionHistory: () => void,
|
||||
checkedOutVersionStatus?: ?OpenedVersionStatus,
|
||||
onQuitVersionHistory?: () => Promise<void>,
|
||||
canSave: boolean,
|
||||
|};
|
||||
|
||||
const LeftButtonsToolbarGroup = React.memo<LeftButtonsToolbarGroupProps>(
|
||||
function LeftButtonsToolbarGroup(props) {
|
||||
const windowWidth = useResponsiveWindowWidth();
|
||||
const isMobile = windowWidth === 'small';
|
||||
return (
|
||||
<ToolbarGroup firstChild>
|
||||
<IconButton
|
||||
@@ -43,6 +59,18 @@ const LeftButtonsToolbarGroup = React.memo<LeftButtonsToolbarGroupProps>(
|
||||
>
|
||||
<ProjectManagerIcon />
|
||||
</IconButton>
|
||||
{props.showVersionHistoryButton && (
|
||||
<IconButton
|
||||
size="small"
|
||||
id="toolbar-history-button"
|
||||
onClick={props.onOpenVersionHistory}
|
||||
tooltip={t`Open version history`}
|
||||
color="default"
|
||||
disabled={false}
|
||||
>
|
||||
<HistoryIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
<IconButton
|
||||
size="small"
|
||||
id="toolbar-save-button"
|
||||
@@ -53,6 +81,23 @@ const LeftButtonsToolbarGroup = React.memo<LeftButtonsToolbarGroupProps>(
|
||||
>
|
||||
<FloppyIcon />
|
||||
</IconButton>
|
||||
{!isMobile &&
|
||||
props.checkedOutVersionStatus &&
|
||||
props.onQuitVersionHistory && (
|
||||
<div
|
||||
style={{
|
||||
// Leave margin between the chip that has a Cross icon to click and the
|
||||
// Play icon to preview the project. It's to avoid a mis-click that would
|
||||
// quit the version history instead of previewing the game.
|
||||
marginRight: 20,
|
||||
}}
|
||||
>
|
||||
<OpenedVersionStatusChip
|
||||
onClickClose={props.onQuitVersionHistory}
|
||||
openedVersionStatus={props.checkedOutVersionStatus}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</ToolbarGroup>
|
||||
);
|
||||
}
|
||||
@@ -60,19 +105,35 @@ const LeftButtonsToolbarGroup = React.memo<LeftButtonsToolbarGroupProps>(
|
||||
|
||||
export default React.forwardRef<MainFrameToolbarProps, ToolbarInterface>(
|
||||
function MainframeToolbar(props: MainFrameToolbarProps, ref) {
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
const [editorToolbar, setEditorToolbar] = React.useState<?React.Node>(null);
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
setEditorToolbar,
|
||||
}));
|
||||
|
||||
const borderBottomColor = React.useMemo(
|
||||
() => {
|
||||
if (!props.checkedOutVersionStatus) return null;
|
||||
return getStatusColor(
|
||||
gdevelopTheme,
|
||||
props.checkedOutVersionStatus.status
|
||||
);
|
||||
},
|
||||
[props.checkedOutVersionStatus, gdevelopTheme]
|
||||
);
|
||||
|
||||
return (
|
||||
<Toolbar>
|
||||
<Toolbar borderBottomColor={borderBottomColor}>
|
||||
{props.showProjectButtons ? (
|
||||
<>
|
||||
<LeftButtonsToolbarGroup
|
||||
toggleProjectManager={props.toggleProjectManager}
|
||||
onSave={props.onSave}
|
||||
canSave={props.canSave}
|
||||
showVersionHistoryButton={props.showVersionHistoryButton}
|
||||
onOpenVersionHistory={props.onOpenVersionHistory}
|
||||
checkedOutVersionStatus={props.checkedOutVersionStatus}
|
||||
onQuitVersionHistory={props.onQuitVersionHistory}
|
||||
/>
|
||||
<ToolbarGroup>
|
||||
<Spacer />
|
||||
@@ -87,6 +148,7 @@ export default React.forwardRef<MainFrameToolbarProps, ToolbarInterface>(
|
||||
previewState={props.previewState}
|
||||
hasPreviewsRunning={props.hasPreviewsRunning}
|
||||
openShareDialog={props.openShareDialog}
|
||||
isSharingEnabled={props.isSharingEnabled}
|
||||
/>
|
||||
<Spacer />
|
||||
</ToolbarGroup>
|
||||
|
@@ -185,6 +185,7 @@ import { type PrivateGameTemplateListingData } from '../Utils/GDevelopServices/S
|
||||
import PixiResourcesLoader from '../ObjectsRendering/PixiResourcesLoader';
|
||||
import useResourcesWatcher from './ResourcesWatcher';
|
||||
import { extractGDevelopApiErrorStatusAndCode } from '../Utils/GDevelopServices/Errors';
|
||||
import useVersionHistory from '../VersionHistory/useVersionHistory';
|
||||
const GD_STARTUP_TIMES = global.GD_STARTUP_TIMES || [];
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
@@ -2081,11 +2082,14 @@ const MainFrame = (props: Props) => {
|
||||
);
|
||||
|
||||
const openFromFileMetadataWithStorageProvider = React.useCallback(
|
||||
(
|
||||
async (
|
||||
fileMetadataAndStorageProviderName: FileMetadataAndStorageProviderName,
|
||||
options: ?{ openAllScenes: boolean }
|
||||
) => {
|
||||
if (unsavedChanges.hasUnsavedChanges) {
|
||||
options: ?{| openAllScenes?: boolean, ignoreUnsavedChanges?: boolean |}
|
||||
): Promise<void> => {
|
||||
if (
|
||||
unsavedChanges.hasUnsavedChanges &&
|
||||
!(options && options.ignoreUnsavedChanges)
|
||||
) {
|
||||
const answer = Window.showConfirmDialog(
|
||||
i18n._(
|
||||
t`Open a new project? Any changes that have not been saved will be lost.`
|
||||
@@ -2105,7 +2109,7 @@ const MainFrame = (props: Props) => {
|
||||
if (!storageProvider) return;
|
||||
|
||||
getStorageProviderOperations(storageProvider);
|
||||
openFromFileMetadata(fileMetadata)
|
||||
await openFromFileMetadata(fileMetadata)
|
||||
.then(state => {
|
||||
if (state) {
|
||||
const { currentProject } = state;
|
||||
@@ -2162,6 +2166,44 @@ const MainFrame = (props: Props) => {
|
||||
]
|
||||
);
|
||||
|
||||
const onOpenCloudProjectOnSpecificVersion = React.useCallback(
|
||||
({
|
||||
fileMetadata,
|
||||
versionId,
|
||||
ignoreUnsavedChanges,
|
||||
}: {|
|
||||
fileMetadata: FileMetadata,
|
||||
versionId: string,
|
||||
ignoreUnsavedChanges: boolean,
|
||||
|}): Promise<void> => {
|
||||
return openFromFileMetadataWithStorageProvider(
|
||||
{
|
||||
storageProviderName: 'Cloud',
|
||||
fileMetadata: {
|
||||
...fileMetadata,
|
||||
version: versionId,
|
||||
},
|
||||
},
|
||||
{ ignoreUnsavedChanges }
|
||||
);
|
||||
},
|
||||
[openFromFileMetadataWithStorageProvider]
|
||||
);
|
||||
|
||||
const {
|
||||
renderVersionHistoryPanel,
|
||||
renderMobileTopBarStatus,
|
||||
showVersionHistoryButton,
|
||||
openVersionHistoryPanel,
|
||||
checkedOutVersionStatus,
|
||||
onQuitVersionHistory,
|
||||
} = useVersionHistory({
|
||||
getStorageProvider,
|
||||
isSavingProject,
|
||||
fileMetadata: currentFileMetadata,
|
||||
onOpenCloudProjectOnSpecificVersion,
|
||||
});
|
||||
|
||||
const openSaveToStorageProviderDialog = React.useCallback(
|
||||
(open: boolean = true) => {
|
||||
if (open) {
|
||||
@@ -2353,7 +2395,11 @@ const MainFrame = (props: Props) => {
|
||||
|
||||
const saveProjectAs = React.useCallback(
|
||||
() => {
|
||||
if (!currentProject) return;
|
||||
if (!currentProject || !!checkedOutVersionStatus) {
|
||||
// Prevent "save project as" when no current project or when the opened project
|
||||
// is a previous version (cloud project only) of the current project.
|
||||
return;
|
||||
}
|
||||
|
||||
if (cloudProjectRecoveryOpenedVersionId && !cloudProjectSaveChoiceOpen) {
|
||||
setCloudProjectSaveChoiceOpen(true);
|
||||
@@ -2380,6 +2426,7 @@ const MainFrame = (props: Props) => {
|
||||
saveProjectAsWithStorageProvider,
|
||||
cloudProjectRecoveryOpenedVersionId,
|
||||
cloudProjectSaveChoiceOpen,
|
||||
checkedOutVersionStatus,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -2407,7 +2454,6 @@ const MainFrame = (props: Props) => {
|
||||
}
|
||||
|
||||
saveUiSettings(state.editorTabs);
|
||||
_showSnackMessage(i18n._(t`Saving...`), null);
|
||||
|
||||
// Protect against concurrent saves, which can trigger issues with the
|
||||
// file system.
|
||||
@@ -2415,6 +2461,25 @@ const MainFrame = (props: Props) => {
|
||||
console.info('Project is already being saved, not triggering save.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkedOutVersionStatus) {
|
||||
const shouldRestoreCheckedOutVersion = await showConfirmation({
|
||||
title: t`Restore this version`,
|
||||
message: t`You're trying to save changes made to a previous version of your project. If you continue, it will be used as the new latest version.`,
|
||||
});
|
||||
if (!shouldRestoreCheckedOutVersion) return;
|
||||
} else if (canFileMetadataBeSafelySaved) {
|
||||
const canProjectBeSafelySaved = await canFileMetadataBeSafelySaved(
|
||||
currentFileMetadata,
|
||||
{
|
||||
showAlert,
|
||||
showConfirmation,
|
||||
}
|
||||
);
|
||||
if (!canProjectBeSafelySaved) return;
|
||||
}
|
||||
|
||||
_showSnackMessage(i18n._(t`Saving...`), null);
|
||||
setIsSavingProject(true);
|
||||
|
||||
try {
|
||||
@@ -2425,27 +2490,18 @@ const MainFrame = (props: Props) => {
|
||||
// store their values in variables now.
|
||||
const storageProviderInternalName = getStorageProvider().internalName;
|
||||
|
||||
if (canFileMetadataBeSafelySaved) {
|
||||
const canProjectBeSafelySaved = await canFileMetadataBeSafelySaved(
|
||||
currentFileMetadata,
|
||||
{
|
||||
showAlert,
|
||||
showConfirmation,
|
||||
}
|
||||
);
|
||||
if (!canProjectBeSafelySaved) return;
|
||||
|
||||
// Ensure snackbar is shown again, in case the user stayed on the previous alert dialog
|
||||
// for too long.
|
||||
_replaceSnackMessage(i18n._(t`Saving...`), null);
|
||||
const saveOptions = {};
|
||||
if (cloudProjectRecoveryOpenedVersionId) {
|
||||
saveOptions.previousVersion = cloudProjectRecoveryOpenedVersionId;
|
||||
}
|
||||
if (checkedOutVersionStatus) {
|
||||
saveOptions.restoredFromVersionId =
|
||||
checkedOutVersionStatus.version.id;
|
||||
}
|
||||
|
||||
const { wasSaved, fileMetadata } = await onSaveProject(
|
||||
currentProject,
|
||||
currentFileMetadata,
|
||||
cloudProjectRecoveryOpenedVersionId
|
||||
? { previousVersion: cloudProjectRecoveryOpenedVersionId }
|
||||
: undefined
|
||||
saveOptions
|
||||
);
|
||||
|
||||
if (wasSaved) {
|
||||
@@ -2528,6 +2584,7 @@ const MainFrame = (props: Props) => {
|
||||
cloudProjectSaveChoiceOpen,
|
||||
showAlert,
|
||||
showConfirmation,
|
||||
checkedOutVersionStatus,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -2670,20 +2727,18 @@ const MainFrame = (props: Props) => {
|
||||
return true;
|
||||
};
|
||||
|
||||
const onOpenCloudProjectOnSpecificVersion = React.useCallback(
|
||||
const onOpenCloudProjectOnSpecificVersionForRecovery = React.useCallback(
|
||||
(versionId: string) => {
|
||||
if (!cloudProjectFileMetadataToRecover) return;
|
||||
openFromFileMetadataWithStorageProvider({
|
||||
storageProviderName: 'Cloud',
|
||||
fileMetadata: {
|
||||
...cloudProjectFileMetadataToRecover,
|
||||
version: versionId,
|
||||
},
|
||||
onOpenCloudProjectOnSpecificVersion({
|
||||
fileMetadata: cloudProjectFileMetadataToRecover,
|
||||
versionId,
|
||||
ignoreUnsavedChanges: false,
|
||||
});
|
||||
setCloudProjectFileMetadataToRecover(null);
|
||||
setCloudProjectRecoveryOpenedVersionId(versionId);
|
||||
},
|
||||
[openFromFileMetadataWithStorageProvider, cloudProjectFileMetadataToRecover]
|
||||
[cloudProjectFileMetadataToRecover, onOpenCloudProjectOnSpecificVersion]
|
||||
);
|
||||
|
||||
const canInstallPrivateAsset = React.useCallback(
|
||||
@@ -2946,6 +3001,7 @@ const MainFrame = (props: Props) => {
|
||||
onOpenRecentFile: openFromFileMetadataWithStorageProvider,
|
||||
onSaveProject: saveProject,
|
||||
onSaveProjectAs: saveProjectAs,
|
||||
onShowVersionHistory: openVersionHistoryPanel,
|
||||
onCloseProject: askToCloseProject,
|
||||
onCloseApp: closeApp,
|
||||
onExportProject: () => openShareDialog('publish'),
|
||||
@@ -3087,6 +3143,7 @@ const MainFrame = (props: Props) => {
|
||||
onDropTab={onDropEditorTab}
|
||||
/>
|
||||
</TabsTitlebar>
|
||||
{renderMobileTopBarStatus()}
|
||||
<Toolbar
|
||||
ref={toolbar}
|
||||
showProjectButtons={
|
||||
@@ -3102,6 +3159,9 @@ const MainFrame = (props: Props) => {
|
||||
openShareDialog={() =>
|
||||
openShareDialog(/* leave the dialog decide which tab to open */)
|
||||
}
|
||||
isSharingEnabled={
|
||||
!checkedOutVersionStatus && !cloudProjectRecoveryOpenedVersionId
|
||||
}
|
||||
onOpenDebugger={launchDebuggerAndPreview}
|
||||
hasPreviewsRunning={hasPreviewsRunning}
|
||||
onPreviewWithoutHotReload={launchNewPreview}
|
||||
@@ -3116,6 +3176,10 @@ const MainFrame = (props: Props) => {
|
||||
!!currentProject && currentProject.getLayoutsCount() > 0
|
||||
}
|
||||
previewState={previewState}
|
||||
showVersionHistoryButton={showVersionHistoryButton}
|
||||
onOpenVersionHistory={openVersionHistoryPanel}
|
||||
checkedOutVersionStatus={checkedOutVersionStatus}
|
||||
onQuitVersionHistory={onQuitVersionHistory}
|
||||
/>
|
||||
<LeaderboardProvider
|
||||
gameId={
|
||||
@@ -3354,7 +3418,7 @@ const MainFrame = (props: Props) => {
|
||||
<CloudProjectRecoveryDialog
|
||||
cloudProjectId={cloudProjectFileMetadataToRecover.fileIdentifier}
|
||||
onClose={() => setCloudProjectFileMetadataToRecover(null)}
|
||||
onOpenPreviousVersion={onOpenCloudProjectOnSpecificVersion}
|
||||
onOpenPreviousVersion={onOpenCloudProjectOnSpecificVersionForRecovery}
|
||||
/>
|
||||
)}
|
||||
{cloudProjectSaveChoiceOpen && (
|
||||
@@ -3415,6 +3479,7 @@ const MainFrame = (props: Props) => {
|
||||
{renderLeaderboardReplacerDialog()}
|
||||
{renderResourceMoverDialog()}
|
||||
{renderResourceFetcherDialog()}
|
||||
{renderVersionHistoryPanel()}
|
||||
<CloseConfirmDialog
|
||||
shouldPrompt={!!state.currentProject}
|
||||
i18n={props.i18n}
|
||||
|
@@ -13,7 +13,7 @@ import Text from '../../UI/Text';
|
||||
import {
|
||||
getLastVersionsOfProject,
|
||||
isCloudProjectVersionSane,
|
||||
type CloudProjectVersion,
|
||||
type FilledCloudProjectVersion,
|
||||
} from '../../Utils/GDevelopServices/Project';
|
||||
import FlatButton from '../../UI/FlatButton';
|
||||
import { sendCloudProjectCouldNotBeOpened } from '../../Utils/Analytics/EventSender';
|
||||
@@ -36,7 +36,7 @@ const CloudProjectRecoveryDialog = ({
|
||||
const [
|
||||
lastSaneVersion,
|
||||
setLastSaneVersion,
|
||||
] = React.useState<?CloudProjectVersion>(null);
|
||||
] = React.useState<?FilledCloudProjectVersion>(null);
|
||||
const [isErrored, setIsErrored] = React.useState<boolean>(false);
|
||||
const [
|
||||
saneVersionHasNotBeenFound,
|
||||
|
@@ -53,7 +53,7 @@ const zipProjectAndCommitVersion = async ({
|
||||
authenticatedUser: AuthenticatedUser,
|
||||
project: gdProject,
|
||||
cloudProjectId: string,
|
||||
options?: {| previousVersion: string |},
|
||||
options?: {| previousVersion?: string, restoredFromVersionId?: string |},
|
||||
|}): Promise<?string> => {
|
||||
const [zippedProject, projectJson] = await zipProject(project);
|
||||
const archiveIsSane = await checkZipContent(zippedProject, projectJson);
|
||||
@@ -65,6 +65,7 @@ const zipProjectAndCommitVersion = async ({
|
||||
cloudProjectId,
|
||||
zippedProject,
|
||||
previousVersion: options ? options.previousVersion : null,
|
||||
restoredFromVersionId: options ? options.restoredFromVersionId : null,
|
||||
});
|
||||
return newVersion;
|
||||
};
|
||||
@@ -74,10 +75,12 @@ export const generateOnSaveProject = (
|
||||
) => async (
|
||||
project: gdProject,
|
||||
fileMetadata: FileMetadata,
|
||||
options?: {| previousVersion: string |}
|
||||
options?: {| previousVersion?: string, restoredFromVersionId?: string |}
|
||||
) => {
|
||||
const cloudProjectId = fileMetadata.fileIdentifier;
|
||||
const gameId = project.getProjectUuid();
|
||||
const now = Date.now();
|
||||
|
||||
if (!fileMetadata.gameId) {
|
||||
console.info('Game id was never set, updating the cloud project.');
|
||||
try {
|
||||
@@ -99,9 +102,12 @@ export const generateOnSaveProject = (
|
||||
const newFileMetadata: FileMetadata = {
|
||||
...fileMetadata,
|
||||
gameId,
|
||||
// lastModifiedDate is not set since it will be set by backend services
|
||||
// and then frontend will use it to transform the list of cloud project
|
||||
// items into a list of FileMetadata.
|
||||
// lastModifiedDate is set here even though it will be set by backend services.
|
||||
// Regarding the list of cloud projects in the build section, it should not have
|
||||
// an impact since the 2 dates are not used for the same purpose.
|
||||
// But it's better to have an up-to-date current file metadata (used by the version
|
||||
// history to know when to refresh the most recent version).
|
||||
lastModifiedDate: now,
|
||||
};
|
||||
if (!newVersion) return { wasSaved: false, fileMetadata: newFileMetadata };
|
||||
|
||||
@@ -121,7 +127,7 @@ export const generateOnChangeProjectProperty = (
|
||||
project: gdProject,
|
||||
fileMetadata: FileMetadata,
|
||||
properties: {| name?: string, gameId?: string |}
|
||||
): Promise<null | {| version: string |}> => {
|
||||
): Promise<null | {| version: string, lastModifiedDate: number |}> => {
|
||||
if (!authenticatedUser.authenticated) return null;
|
||||
try {
|
||||
await updateCloudProject(
|
||||
@@ -138,7 +144,7 @@ export const generateOnChangeProjectProperty = (
|
||||
throw new Error("Couldn't save project following property update.");
|
||||
}
|
||||
|
||||
return { version: newVersion };
|
||||
return { version: newVersion, lastModifiedDate: Date.now() };
|
||||
} catch (error) {
|
||||
// TODO: Determine if a feedback should be given to user so that they can try again if necessary.
|
||||
console.warn(
|
||||
|
@@ -101,7 +101,7 @@ export type StorageProviderOperations = {|
|
||||
onSaveProject?: (
|
||||
project: gdProject,
|
||||
fileMetadata: FileMetadata,
|
||||
options?: {| previousVersion: string |}
|
||||
options?: {| previousVersion?: string, restoredFromVersionId?: string |}
|
||||
) => Promise<{|
|
||||
wasSaved: boolean,
|
||||
fileMetadata: FileMetadata,
|
||||
@@ -146,7 +146,7 @@ export type StorageProviderOperations = {|
|
||||
project: gdProject,
|
||||
fileMetadata: FileMetadata,
|
||||
properties: {| name?: string, gameId?: string |} // In order to synchronize project and cloud project names.
|
||||
) => Promise<null | {| version: string |}>,
|
||||
) => Promise<null | {| version: string, lastModifiedDate: number |}>,
|
||||
|
||||
// Project auto saving:
|
||||
onAutoSaveProject?: (
|
||||
|
15
newIDE/app/src/UI/CustomSvgIcons/History.js
Normal file
15
newIDE/app/src/UI/CustomSvgIcons/History.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import SvgIcon from '@material-ui/core/SvgIcon';
|
||||
|
||||
export default React.memo(props => (
|
||||
<SvgIcon {...props} width="16" height="16" viewBox="-1 -1 18 18" fill="none">
|
||||
<path
|
||||
d="M0.833328 8C0.833328 4.04196 4.04195 0.833336 7.99999 0.833336C11.5931 0.833336 14.5678 3.47711 15.0866 6.92562C15.1277 7.19869 14.9396 7.45336 14.6666 7.49444C14.3935 7.53552 14.1388 7.34745 14.0978 7.07438C13.6515 4.10788 11.0909 1.83334 7.99999 1.83334C4.59424 1.83334 1.83333 4.59424 1.83333 8C1.83333 11.4057 4.59424 14.1667 7.99999 14.1667C10.2473 14.1667 12.2147 12.9645 13.2927 11.1667H11.3333C11.0572 11.1667 10.8333 10.9428 10.8333 10.6667C10.8333 10.3905 11.0572 10.1667 11.3333 10.1667H14.101C14.1084 10.1665 14.1159 10.1665 14.1234 10.1667H14.2667C14.7637 10.1667 15.1667 10.5696 15.1667 11.0667V14C15.1667 14.2761 14.9428 14.5 14.6667 14.5C14.3905 14.5 14.1667 14.2761 14.1667 14V11.6535C12.9182 13.7562 10.6243 15.1667 7.99999 15.1667C4.04195 15.1667 0.833328 11.958 0.833328 8Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M7.99999 3.5C8.27614 3.5 8.49999 3.72386 8.49999 4V7.5H12C12.2761 7.5 12.5 7.72386 12.5 8C12.5 8.27614 12.2761 8.5 12 8.5H7.99999C7.72385 8.5 7.49999 8.27614 7.49999 8V4C7.49999 3.72386 7.72385 3.5 7.99999 3.5Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</SvgIcon>
|
||||
));
|
@@ -5,23 +5,33 @@ import GDevelopThemeContext from './Theme/GDevelopThemeContext';
|
||||
type ToolbarProps = {|
|
||||
children: React.Node,
|
||||
height?: number,
|
||||
borderBottomColor?: ?string,
|
||||
|};
|
||||
|
||||
const styles = {
|
||||
toolbar: {
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
overflowX: 'overlay',
|
||||
overflowY: 'hidden',
|
||||
paddingLeft: 8,
|
||||
paddingRight: 8,
|
||||
},
|
||||
};
|
||||
|
||||
export const Toolbar = React.memo<ToolbarProps>(
|
||||
({ children, height }: ToolbarProps) => {
|
||||
({ children, borderBottomColor, height = 40 }: ToolbarProps) => {
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
return (
|
||||
<div
|
||||
className="almost-invisible-scrollbar"
|
||||
style={{
|
||||
...styles.toolbar,
|
||||
backgroundColor: gdevelopTheme.toolbar.backgroundColor,
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
overflowX: 'overlay',
|
||||
overflowY: 'hidden',
|
||||
height: height || 40,
|
||||
paddingLeft: 8,
|
||||
paddingRight: 8,
|
||||
height,
|
||||
borderBottom: borderBottomColor
|
||||
? `2px solid ${borderBottomColor}`
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
@@ -347,6 +347,7 @@ export type SubscriptionDialogDisplayReason =
|
||||
| 'Leaderboard customization'
|
||||
| 'Extend redeemed subscription'
|
||||
| 'Generate project from prompt'
|
||||
| 'Version history'
|
||||
| 'Add collaborators on project';
|
||||
|
||||
export const sendSubscriptionDialogShown = (metadata: {|
|
||||
|
@@ -10,8 +10,11 @@ import { getFileSha512TruncatedTo256 } from '../FileHasher';
|
||||
import { isNativeMobileApp } from '../Platform';
|
||||
import { unzipFirstEntryOfBlob } from '../Zip.js/Utils';
|
||||
import { extractGDevelopApiErrorStatusAndCode } from './Errors';
|
||||
import { extractNextPageUriFromLinkHeader } from './Play';
|
||||
import { User as FirebaseUser } from 'firebase/auth';
|
||||
|
||||
export const CLOUD_PROJECT_NAME_MAX_LENGTH = 50;
|
||||
export const CLOUD_PROJECT_VERSION_LABEL_MAX_LENGTH = 50;
|
||||
export const PROJECT_RESOURCE_MAX_SIZE_IN_BYTES = 15 * 1000 * 1000;
|
||||
|
||||
export const projectResourcesClient = axios.create({
|
||||
@@ -83,11 +86,27 @@ type CloudProject = {|
|
||||
export type CloudProjectVersion = {|
|
||||
projectId: string,
|
||||
id: string,
|
||||
label?: string,
|
||||
createdAt: string,
|
||||
/** Was not always recorded so can be undefined. Represents the user who created this version. */
|
||||
userId?: string,
|
||||
/** previousVersion is null when the entity represents the initial version of a project. */
|
||||
previousVersion: null | string,
|
||||
/** If the version is a restoration from a previous one, this attribute is set. */
|
||||
restoredFromVersionId?: string,
|
||||
|};
|
||||
|
||||
export type FilledCloudProjectVersion = {|
|
||||
projectId: string,
|
||||
id: string,
|
||||
label?: string,
|
||||
createdAt: string,
|
||||
/** Was not always recorded so can be undefined. Represents the user who created this version. */
|
||||
userId?: string,
|
||||
/** previousVersion is null when the entity represents the initial version of a project. */
|
||||
previousVersion: null | string,
|
||||
/** If the version is a restoration from a previous one, this attribute is set. */
|
||||
restoredFromVersion?: CloudProjectVersion,
|
||||
|};
|
||||
|
||||
export type CloudProjectWithUserAccessInfo = {|
|
||||
@@ -182,7 +201,7 @@ const getVersionIdFromPath = (path: string): string => {
|
||||
export const getLastVersionsOfProject = async (
|
||||
authenticatedUser: AuthenticatedUser,
|
||||
cloudProjectId: string
|
||||
): Promise<?Array<CloudProjectVersion>> => {
|
||||
): Promise<?Array<FilledCloudProjectVersion>> => {
|
||||
const { getAuthorizationHeader, firebaseUser } = authenticatedUser;
|
||||
if (!firebaseUser) return;
|
||||
|
||||
@@ -273,11 +292,13 @@ export const commitVersion = async ({
|
||||
cloudProjectId,
|
||||
zippedProject,
|
||||
previousVersion,
|
||||
restoredFromVersionId,
|
||||
}: {
|
||||
authenticatedUser: AuthenticatedUser,
|
||||
cloudProjectId: string,
|
||||
zippedProject: Blob,
|
||||
previousVersion?: ?string,
|
||||
restoredFromVersionId?: ?string,
|
||||
}): Promise<?string> => {
|
||||
const { getAuthorizationHeader, firebaseUser } = authenticatedUser;
|
||||
if (!firebaseUser) return;
|
||||
@@ -304,19 +325,26 @@ export const commitVersion = async ({
|
||||
}
|
||||
)
|
||||
);
|
||||
const body: {|
|
||||
newVersion: string,
|
||||
previousVersion?: string,
|
||||
restoredFromVersionId?: string,
|
||||
|} = { newVersion };
|
||||
if (previousVersion) {
|
||||
body.previousVersion = previousVersion;
|
||||
}
|
||||
if (restoredFromVersionId) {
|
||||
body.restoredFromVersionId = restoredFromVersionId;
|
||||
}
|
||||
// Inform backend a new version has been uploaded.
|
||||
try {
|
||||
// Backend only returns "OK".
|
||||
await apiClient.post(
|
||||
`/project/${cloudProjectId}/action/commit`,
|
||||
{ newVersion, ...(previousVersion ? { previousVersion } : undefined) },
|
||||
{
|
||||
headers: {
|
||||
Authorization: authorizationHeader,
|
||||
},
|
||||
params: { userId },
|
||||
}
|
||||
);
|
||||
await apiClient.post(`/project/${cloudProjectId}/action/commit`, body, {
|
||||
headers: {
|
||||
Authorization: authorizationHeader,
|
||||
},
|
||||
params: { userId },
|
||||
});
|
||||
return newVersion;
|
||||
} catch (error) {
|
||||
console.error('Error while committing version', error);
|
||||
@@ -693,3 +721,80 @@ export const listProjectUserAcls = async (
|
||||
|
||||
return projectUserAcls;
|
||||
};
|
||||
|
||||
export const updateCloudProjectVersion = async (
|
||||
authenticatedUser: AuthenticatedUser,
|
||||
cloudProjectId: string,
|
||||
versionId: string,
|
||||
attributes: {| label: string |}
|
||||
): Promise<?CloudProjectVersion> => {
|
||||
const { getAuthorizationHeader, firebaseUser } = authenticatedUser;
|
||||
if (!firebaseUser) return;
|
||||
|
||||
const trimmedLabel = attributes.label.trim();
|
||||
|
||||
const cleanedAttributes = {
|
||||
label: trimmedLabel
|
||||
? trimmedLabel.slice(0, CLOUD_PROJECT_VERSION_LABEL_MAX_LENGTH)
|
||||
: '',
|
||||
};
|
||||
|
||||
const { uid: userId } = firebaseUser;
|
||||
const authorizationHeader = await getAuthorizationHeader();
|
||||
const response = await apiClient.patch(
|
||||
`/project/${cloudProjectId}/version/${versionId}`,
|
||||
cleanedAttributes,
|
||||
{
|
||||
headers: {
|
||||
Authorization: authorizationHeader,
|
||||
},
|
||||
params: { userId },
|
||||
}
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* List versions of a cloud project.
|
||||
* This method does not directly use the authenticatedUser object to enable
|
||||
* listing versions in React effects. Using authenticatedUser as a dependency
|
||||
* of an effect triggers the effect on each change of the profile (any update
|
||||
* of badges, extensions, purchases, etc.).
|
||||
*/
|
||||
export const listVersionsOfProject = async (
|
||||
getAuthorizationHeader: () => Promise<string>,
|
||||
firebaseUser: ?FirebaseUser,
|
||||
cloudProjectId: string,
|
||||
options: {| forceUri: ?string |}
|
||||
): Promise<?{|
|
||||
versions: Array<FilledCloudProjectVersion>,
|
||||
nextPageUri: ?string,
|
||||
|}> => {
|
||||
if (!firebaseUser) return;
|
||||
|
||||
const { uid: userId } = firebaseUser;
|
||||
const authorizationHeader = await getAuthorizationHeader();
|
||||
const uri = options.forceUri || `/project/${cloudProjectId}/version`;
|
||||
|
||||
// $FlowFixMe
|
||||
const response = await apiClient.get(uri, {
|
||||
headers: {
|
||||
Authorization: authorizationHeader,
|
||||
},
|
||||
params: options.forceUri
|
||||
? { userId }
|
||||
: { userId, goal: 'history', perPage: 15 },
|
||||
});
|
||||
const nextPageUri = response.headers.link
|
||||
? extractNextPageUriFromLinkHeader(response.headers.link)
|
||||
: null;
|
||||
const projectVersions = response.data;
|
||||
|
||||
if (!Array.isArray(projectVersions)) {
|
||||
throw new Error('Invalid response from the project versions API');
|
||||
}
|
||||
return {
|
||||
versions: projectVersions,
|
||||
nextPageUri,
|
||||
};
|
||||
};
|
||||
|
@@ -445,6 +445,17 @@ export const getRedirectToCheckoutUrl = ({
|
||||
return url.toString();
|
||||
};
|
||||
|
||||
export const canSeeCloudProjectHistory = (
|
||||
subscription: ?Subscription
|
||||
): boolean => {
|
||||
if (!subscription) return false;
|
||||
return [
|
||||
'gdevelop_business',
|
||||
'gdevelop_startup',
|
||||
'gdevelop_education',
|
||||
].includes(subscription.planId);
|
||||
};
|
||||
|
||||
export const redeemCode = async (
|
||||
getAuthorizationHeader: () => Promise<string>,
|
||||
userId: string,
|
||||
|
@@ -141,11 +141,15 @@ export type TeamMembership = {|
|
||||
groups?: ?Array<string>,
|
||||
|};
|
||||
|
||||
export const apiClient = axios.create({
|
||||
baseURL: GDevelopUserApi.baseUrl,
|
||||
});
|
||||
|
||||
export const searchCreatorPublicProfilesByUsername = (
|
||||
searchString: string
|
||||
): Promise<Array<UserPublicProfile>> => {
|
||||
return axios
|
||||
.get(`${GDevelopUserApi.baseUrl}/user-public-profile/search`, {
|
||||
return apiClient
|
||||
.get(`/user-public-profile/search`, {
|
||||
params: {
|
||||
username: searchString,
|
||||
type: 'creator',
|
||||
@@ -155,11 +159,9 @@ export const searchCreatorPublicProfilesByUsername = (
|
||||
};
|
||||
|
||||
export const getUserBadges = async (id: string): Promise<Array<Badge>> => {
|
||||
const response = await axios.get(
|
||||
`${GDevelopUserApi.baseUrl}/user/${id}/badge`
|
||||
);
|
||||
|
||||
const response = await apiClient.get(`/user/${id}/badge`);
|
||||
const badges = response.data;
|
||||
|
||||
if (!Array.isArray(badges)) {
|
||||
throw new Error('Invalid response from the badges API');
|
||||
}
|
||||
@@ -172,7 +174,7 @@ export const listUserTeams = async (
|
||||
userId: string
|
||||
): Promise<Array<Team>> => {
|
||||
const authorizationHeader = await getAuthorizationHeader();
|
||||
const response = await axios.get(`${GDevelopUserApi.baseUrl}/team`, {
|
||||
const response = await apiClient.get(`/team`, {
|
||||
headers: { Authorization: authorizationHeader },
|
||||
params: { userId, role: 'admin' },
|
||||
});
|
||||
@@ -185,7 +187,7 @@ export const listTeamMembers = async (
|
||||
teamId: string
|
||||
): Promise<Array<User>> => {
|
||||
const authorizationHeader = await getAuthorizationHeader();
|
||||
const response = await axios.get(`${GDevelopUserApi.baseUrl}/user`, {
|
||||
const response = await apiClient.get(`/user`, {
|
||||
headers: { Authorization: authorizationHeader },
|
||||
params: { userId, teamId },
|
||||
});
|
||||
@@ -198,13 +200,10 @@ export const listTeamMemberships = async (
|
||||
teamId: string
|
||||
): Promise<Array<TeamMembership>> => {
|
||||
const authorizationHeader = await getAuthorizationHeader();
|
||||
const response = await axios.get(
|
||||
`${GDevelopUserApi.baseUrl}/team-membership`,
|
||||
{
|
||||
headers: { Authorization: authorizationHeader },
|
||||
params: { userId, teamId },
|
||||
}
|
||||
);
|
||||
const response = await apiClient.get(`/team-membership`, {
|
||||
headers: { Authorization: authorizationHeader },
|
||||
params: { userId, teamId },
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
@@ -214,13 +213,10 @@ export const listTeamGroups = async (
|
||||
teamId: string
|
||||
): Promise<Array<TeamGroup>> => {
|
||||
const authorizationHeader = await getAuthorizationHeader();
|
||||
const response = await axios.get(
|
||||
`${GDevelopUserApi.baseUrl}/team/${teamId}/group`,
|
||||
{
|
||||
headers: { Authorization: authorizationHeader },
|
||||
params: { userId },
|
||||
}
|
||||
);
|
||||
const response = await apiClient.get(`/team/${teamId}/group`, {
|
||||
headers: { Authorization: authorizationHeader },
|
||||
params: { userId },
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
@@ -232,8 +228,8 @@ export const updateGroup = async (
|
||||
attributes: {| name: string |}
|
||||
): Promise<TeamGroup> => {
|
||||
const authorizationHeader = await getAuthorizationHeader();
|
||||
const response = await axios.patch(
|
||||
`${GDevelopUserApi.baseUrl}/team/${teamId}/group/${groupId}`,
|
||||
const response = await apiClient.patch(
|
||||
`/team/${teamId}/group/${groupId}`,
|
||||
attributes,
|
||||
{
|
||||
headers: { Authorization: authorizationHeader },
|
||||
@@ -250,14 +246,10 @@ export const createGroup = async (
|
||||
attributes: {| name: string |}
|
||||
): Promise<TeamGroup> => {
|
||||
const authorizationHeader = await getAuthorizationHeader();
|
||||
const response = await axios.post(
|
||||
`${GDevelopUserApi.baseUrl}/team/${teamId}/group`,
|
||||
attributes,
|
||||
{
|
||||
headers: { Authorization: authorizationHeader },
|
||||
params: { userId },
|
||||
}
|
||||
);
|
||||
const response = await apiClient.post(`/team/${teamId}/group`, attributes, {
|
||||
headers: { Authorization: authorizationHeader },
|
||||
params: { userId },
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
@@ -268,13 +260,10 @@ export const deleteGroup = async (
|
||||
groupId: string
|
||||
): Promise<Array<TeamGroup>> => {
|
||||
const authorizationHeader = await getAuthorizationHeader();
|
||||
const response = await axios.delete(
|
||||
`${GDevelopUserApi.baseUrl}/team/${teamId}/group/${groupId}`,
|
||||
{
|
||||
headers: { Authorization: authorizationHeader },
|
||||
params: { userId },
|
||||
}
|
||||
);
|
||||
const response = await apiClient.delete(`/team/${teamId}/group/${groupId}`, {
|
||||
headers: { Authorization: authorizationHeader },
|
||||
params: { userId },
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
@@ -283,13 +272,10 @@ export const listRecommendations = async (
|
||||
{ userId }: {| userId: string |}
|
||||
): Promise<Array<Recommendation>> => {
|
||||
const authorizationHeader = await getAuthorizationHeader();
|
||||
const response = await axios.get(
|
||||
`${GDevelopUserApi.baseUrl}/recommendation`,
|
||||
{
|
||||
headers: { Authorization: authorizationHeader },
|
||||
params: { userId },
|
||||
}
|
||||
);
|
||||
const response = await apiClient.get(`/recommendation`, {
|
||||
headers: { Authorization: authorizationHeader },
|
||||
params: { userId },
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
@@ -301,8 +287,8 @@ export const updateUserGroup = async (
|
||||
userId: string
|
||||
): Promise<Array<TeamGroup>> => {
|
||||
const authorizationHeader = await getAuthorizationHeader();
|
||||
const response = await axios.post(
|
||||
`${GDevelopUserApi.baseUrl}/team/${teamId}/action/update-members`,
|
||||
const response = await apiClient.post(
|
||||
`/team/${teamId}/action/update-members`,
|
||||
[{ groupId, userId }],
|
||||
{
|
||||
headers: { Authorization: authorizationHeader },
|
||||
@@ -317,31 +303,26 @@ export const getUserPublicProfilesByIds = async (
|
||||
): Promise<UserPublicProfileByIds> => {
|
||||
// Ensure we don't send an empty list of ids, as the request would fail.
|
||||
if (ids.length === 0) return {};
|
||||
const response = await axios.get(
|
||||
`${GDevelopUserApi.baseUrl}/user-public-profile`,
|
||||
{
|
||||
params: {
|
||||
id: ids.join(','),
|
||||
},
|
||||
}
|
||||
);
|
||||
const response = await apiClient.get(`/user-public-profile`, {
|
||||
params: {
|
||||
id: ids.join(','),
|
||||
},
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getUserPublicProfile = (
|
||||
id: string
|
||||
): Promise<UserPublicProfile> => {
|
||||
return axios
|
||||
.get(`${GDevelopUserApi.baseUrl}/user-public-profile/${id}`)
|
||||
return apiClient
|
||||
.get(`/user-public-profile/${id}`)
|
||||
.then(response => response.data);
|
||||
};
|
||||
|
||||
export const getUsernameAvailability = async (
|
||||
username: string
|
||||
): Promise<UsernameAvailability> => {
|
||||
const response = await axios.get(
|
||||
`${GDevelopUserApi.baseUrl}/username-availability/${username}`
|
||||
);
|
||||
const response = await apiClient.get(`/username-availability/${username}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
@@ -350,8 +331,8 @@ export const syncDiscordUsername = async (
|
||||
userId: string
|
||||
): Promise<void> => {
|
||||
const authorizationHeader = await getAuthorizationHeader();
|
||||
await axios.post(
|
||||
`${GDevelopUserApi.baseUrl}/user/${userId}/action/update-discord-role`,
|
||||
await apiClient.post(
|
||||
`/user/${userId}/action/update-discord-role`,
|
||||
{},
|
||||
{
|
||||
headers: { Authorization: authorizationHeader },
|
||||
|
81
newIDE/app/src/VersionHistory/OpenedVersionStatusChip.js
Normal file
81
newIDE/app/src/VersionHistory/OpenedVersionStatusChip.js
Normal file
@@ -0,0 +1,81 @@
|
||||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import { I18n } from '@lingui/react';
|
||||
|
||||
import Text from '../UI/Text';
|
||||
import GDevelopThemeContext from '../UI/Theme/GDevelopThemeContext';
|
||||
import ButtonBase from '@material-ui/core/ButtonBase';
|
||||
import { createStyles, makeStyles } from '@material-ui/core/styles';
|
||||
import Cross from '../UI/CustomSvgIcons/Cross';
|
||||
import History from '../UI/CustomSvgIcons/History';
|
||||
import { Spacer } from '../UI/Grid';
|
||||
import { textEllipsisStyle } from '../UI/TextEllipsis';
|
||||
import { type OpenedVersionStatus } from '.';
|
||||
import { getStatusColor } from './Utils';
|
||||
|
||||
const styles = {
|
||||
chip: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: 4,
|
||||
borderRadius: 6,
|
||||
color: '#111111',
|
||||
...textEllipsisStyle,
|
||||
},
|
||||
};
|
||||
|
||||
const useStylesCloseIconButton = makeStyles(theme =>
|
||||
createStyles({
|
||||
root: {
|
||||
borderRadius: 3,
|
||||
'&:hover': {
|
||||
backdropFilter: 'brightness(0.8)',
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
type Props = {|
|
||||
openedVersionStatus: ?OpenedVersionStatus,
|
||||
onClickClose: () => Promise<void>,
|
||||
|};
|
||||
|
||||
const OpenedVersionStatusChip = ({
|
||||
openedVersionStatus,
|
||||
onClickClose,
|
||||
}: Props) => {
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
const classes = useStylesCloseIconButton();
|
||||
if (!openedVersionStatus) return null;
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<div
|
||||
style={{
|
||||
...styles.chip,
|
||||
backgroundColor: getStatusColor(
|
||||
gdevelopTheme,
|
||||
openedVersionStatus.status
|
||||
),
|
||||
}}
|
||||
>
|
||||
<History />
|
||||
<Spacer />
|
||||
<Text noMargin color="inherit">
|
||||
{i18n.date(Date.parse(openedVersionStatus.version.createdAt), {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short',
|
||||
})}
|
||||
</Text>
|
||||
<Spacer />
|
||||
<ButtonBase classes={classes} onClick={onClickClose}>
|
||||
<Cross />
|
||||
</ButtonBase>
|
||||
</div>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
};
|
||||
|
||||
export default OpenedVersionStatusChip;
|
518
newIDE/app/src/VersionHistory/ProjectVersionRow.js
Normal file
518
newIDE/app/src/VersionHistory/ProjectVersionRow.js
Normal file
@@ -0,0 +1,518 @@
|
||||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import { I18n } from '@lingui/react';
|
||||
import { Trans, t } from '@lingui/macro';
|
||||
import Avatar from '@material-ui/core/Avatar';
|
||||
import Collapse from '@material-ui/core/Collapse';
|
||||
import ButtonBase from '@material-ui/core/ButtonBase';
|
||||
import { createStyles, makeStyles } from '@material-ui/core/styles';
|
||||
import {
|
||||
CLOUD_PROJECT_VERSION_LABEL_MAX_LENGTH,
|
||||
type FilledCloudProjectVersion,
|
||||
} from '../Utils/GDevelopServices/Project';
|
||||
import { type UserPublicProfileByIds } from '../Utils/GDevelopServices/User';
|
||||
import { Column, Line, Spacer } from '../UI/Grid';
|
||||
import Text from '../UI/Text';
|
||||
import { ColumnStackLayout, LineStackLayout } from '../UI/Layout';
|
||||
import IconButton from '../UI/IconButton';
|
||||
import ChevronArrowBottom from '../UI/CustomSvgIcons/ChevronArrowBottom';
|
||||
import ChevronArrowRight from '../UI/CustomSvgIcons/ChevronArrowRight';
|
||||
import ThreeDotsMenu from '../UI/CustomSvgIcons/ThreeDotsMenu';
|
||||
import {
|
||||
shouldCloseOrCancel,
|
||||
shouldValidate,
|
||||
} from '../UI/KeyboardShortcuts/InteractionKeys';
|
||||
import TextField, { type TextFieldInterface } from '../UI/TextField';
|
||||
import HistoryIcon from '../UI/CustomSvgIcons/History';
|
||||
import type { OpenedVersionStatus, VersionRestoringStatus } from '.';
|
||||
import GDevelopThemeContext from '../UI/Theme/GDevelopThemeContext';
|
||||
import Chip from '../UI/Chip';
|
||||
import CircularProgress from '../UI/CircularProgress';
|
||||
import { getStatusColor } from './Utils';
|
||||
|
||||
const thisYear = new Date().getFullYear();
|
||||
|
||||
const styles = {
|
||||
avatar: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
},
|
||||
greyed: { opacity: 0.7 },
|
||||
versionsContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
paddingLeft: 30, // Width of the collapse icon button.
|
||||
},
|
||||
dateContainer: {
|
||||
flexShrink: 0,
|
||||
},
|
||||
iconContainer: {
|
||||
fontSize: 20,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
restoredVersionContainer: {
|
||||
opacity: 0.7,
|
||||
},
|
||||
sharedRowStyle: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: 8,
|
||||
borderRadius: 4,
|
||||
},
|
||||
labelTextfield: { width: '100%' },
|
||||
datSubRow: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '2px 12px 2px 2px',
|
||||
borderRadius: 4,
|
||||
},
|
||||
versionSubRow: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: '2px 12px 2px 32px',
|
||||
borderRadius: 4,
|
||||
},
|
||||
statusIndicator: {
|
||||
height: 6,
|
||||
width: 6,
|
||||
borderRadius: 3,
|
||||
},
|
||||
};
|
||||
|
||||
const StatusIndicator = ({ status }: {| status: VersionRestoringStatus |}) => {
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
if (status === 'opened') return null;
|
||||
const backgroundColor = getStatusColor(gdevelopTheme, status);
|
||||
return <span style={{ ...styles.statusIndicator, backgroundColor }} />;
|
||||
};
|
||||
|
||||
const useOutline = (
|
||||
version: FilledCloudProjectVersion,
|
||||
openedVersionStatus: ?OpenedVersionStatus
|
||||
) => {
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
if (
|
||||
!openedVersionStatus ||
|
||||
openedVersionStatus.version.id !== version.id ||
|
||||
openedVersionStatus.status !== 'unsavedChanges'
|
||||
)
|
||||
return undefined;
|
||||
|
||||
return { outline: `1px solid ${gdevelopTheme.statusIndicator.error}` };
|
||||
};
|
||||
|
||||
const StatusChip = ({ status }: {| status: VersionRestoringStatus |}) => {
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
const label =
|
||||
status === 'unsavedChanges' ? (
|
||||
<Trans>Unsaved changes</Trans>
|
||||
) : status === 'saving' ? (
|
||||
<Trans>Saving...</Trans>
|
||||
) : status === 'latest' ? (
|
||||
<Trans>Latest save</Trans>
|
||||
) : (
|
||||
<Trans>Changes saved</Trans>
|
||||
);
|
||||
const backgroundColor = getStatusColor(gdevelopTheme, status);
|
||||
|
||||
return (
|
||||
<Chip
|
||||
style={{
|
||||
backgroundColor,
|
||||
color: '#111111',
|
||||
padding: '3px 0',
|
||||
height: 'auto',
|
||||
}}
|
||||
label={label}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const useClassesForRowContainer = makeStyles(theme =>
|
||||
createStyles({
|
||||
root: {
|
||||
...styles.sharedRowStyle,
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
},
|
||||
'&.selected': {
|
||||
backgroundColor: theme.palette.action.focus,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
type Props = {|
|
||||
version: FilledCloudProjectVersion,
|
||||
usersPublicProfileByIds: UserPublicProfileByIds,
|
||||
isEditing: boolean,
|
||||
isLatest: boolean,
|
||||
onRename: (FilledCloudProjectVersion, string) => Promise<void>,
|
||||
isLoading: boolean,
|
||||
onCancelRenaming: () => void,
|
||||
onContextMenu: (
|
||||
event: PointerEvent,
|
||||
version: FilledCloudProjectVersion
|
||||
) => void,
|
||||
displayFullDate?: boolean,
|
||||
openedVersionStatus: ?OpenedVersionStatus,
|
||||
getAnonymousAvatar: () => {| src: string, alt: string |},
|
||||
|};
|
||||
|
||||
const ProjectVersionRow = ({
|
||||
version,
|
||||
usersPublicProfileByIds,
|
||||
isEditing,
|
||||
isLatest,
|
||||
isLoading,
|
||||
onRename,
|
||||
onCancelRenaming,
|
||||
onContextMenu,
|
||||
displayFullDate,
|
||||
openedVersionStatus,
|
||||
getAnonymousAvatar,
|
||||
}: Props) => {
|
||||
const textFieldRef = React.useRef<?TextFieldInterface>(null);
|
||||
const [newLabel, setNewLabel] = React.useState<string>(version.label || '');
|
||||
const authorPublicProfile = version.userId
|
||||
? usersPublicProfileByIds[version.userId]
|
||||
: null;
|
||||
|
||||
const validateNewLabel = () => {
|
||||
onRename(version, newLabel);
|
||||
};
|
||||
|
||||
const classes = useClassesForRowContainer();
|
||||
const outlineStyle = useOutline(version, openedVersionStatus);
|
||||
const anonymousAvatar = getAnonymousAvatar();
|
||||
const versionStatus =
|
||||
openedVersionStatus &&
|
||||
openedVersionStatus.status !== 'opened' &&
|
||||
openedVersionStatus.version.id === version.id
|
||||
? openedVersionStatus.status
|
||||
: isLatest
|
||||
? 'latest'
|
||||
: null;
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<div
|
||||
className={`${classes.root}${
|
||||
openedVersionStatus && openedVersionStatus.version.id === version.id
|
||||
? ' selected'
|
||||
: ''
|
||||
}`}
|
||||
style={outlineStyle}
|
||||
>
|
||||
<Column noMargin expand>
|
||||
{versionStatus && (
|
||||
<>
|
||||
<Line noMargin>
|
||||
<StatusChip status={versionStatus} />
|
||||
</Line>
|
||||
<Spacer />
|
||||
</>
|
||||
)}
|
||||
{isEditing ? (
|
||||
<TextField
|
||||
ref={textFieldRef}
|
||||
margin="none"
|
||||
value={newLabel}
|
||||
translatableHintText={t`End of jam`}
|
||||
onChange={(event, text) =>
|
||||
setNewLabel(
|
||||
text.slice(0, CLOUD_PROJECT_VERSION_LABEL_MAX_LENGTH)
|
||||
)
|
||||
}
|
||||
autoFocus="desktopAndMobileDevices"
|
||||
onKeyPress={event => {
|
||||
if (shouldValidate(event)) {
|
||||
validateNewLabel();
|
||||
}
|
||||
}}
|
||||
onKeyDown={event => {
|
||||
// Prevent parent drawer to be closed when Esc is hit.
|
||||
event.stopPropagation();
|
||||
}}
|
||||
onKeyUp={event => {
|
||||
if (shouldCloseOrCancel(event)) {
|
||||
setNewLabel(version.label || '');
|
||||
onCancelRenaming();
|
||||
}
|
||||
}}
|
||||
style={styles.labelTextfield}
|
||||
/>
|
||||
) : version.label ? (
|
||||
<LineStackLayout noMargin>
|
||||
<Text noMargin>{version.label}</Text>
|
||||
{isLoading && (
|
||||
<Column noMargin>
|
||||
<CircularProgress size={20} />
|
||||
</Column>
|
||||
)}
|
||||
<div style={styles.dateContainer}>
|
||||
<Text noMargin style={styles.greyed}>
|
||||
{i18n.date(
|
||||
version.createdAt,
|
||||
displayFullDate
|
||||
? {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short',
|
||||
}
|
||||
: {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
}
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
</LineStackLayout>
|
||||
) : (
|
||||
<Text noMargin>
|
||||
{i18n.date(version.createdAt, {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
})}
|
||||
</Text>
|
||||
)}
|
||||
{version.restoredFromVersion && (
|
||||
<div style={styles.restoredVersionContainer}>
|
||||
<LineStackLayout noMargin alignItems="center">
|
||||
<div style={styles.iconContainer}>
|
||||
<HistoryIcon fontSize="inherit" />
|
||||
</div>
|
||||
<Text noMargin>
|
||||
{version.restoredFromVersion.label ||
|
||||
i18n.date(version.restoredFromVersion.createdAt, {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short',
|
||||
})}
|
||||
</Text>
|
||||
</LineStackLayout>
|
||||
</div>
|
||||
)}
|
||||
{authorPublicProfile ? (
|
||||
<LineStackLayout noMargin alignItems="center">
|
||||
<Avatar
|
||||
src={authorPublicProfile.iconUrl}
|
||||
style={styles.avatar}
|
||||
/>
|
||||
<Text noMargin style={styles.greyed}>
|
||||
{authorPublicProfile.username}
|
||||
</Text>
|
||||
</LineStackLayout>
|
||||
) : (
|
||||
<LineStackLayout noMargin alignItems="center">
|
||||
<img
|
||||
src={anonymousAvatar.src}
|
||||
alt={anonymousAvatar.alt}
|
||||
style={styles.avatar}
|
||||
/>
|
||||
<Text noMargin style={styles.greyed}>
|
||||
<Trans>Anonymous</Trans>
|
||||
</Text>
|
||||
</LineStackLayout>
|
||||
)}
|
||||
</Column>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={event => {
|
||||
onContextMenu(event, version);
|
||||
}}
|
||||
>
|
||||
<ThreeDotsMenu />
|
||||
</IconButton>
|
||||
</div>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
};
|
||||
|
||||
const useClassesForDayCollapse = makeStyles(theme =>
|
||||
createStyles({
|
||||
root: {
|
||||
...styles.sharedRowStyle,
|
||||
justifyContent: 'flex-start',
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
},
|
||||
'&:focus': {
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
},
|
||||
},
|
||||
datSubRow: {
|
||||
...styles.datSubRow,
|
||||
'&.selected': {
|
||||
backgroundColor: theme.palette.action.focus,
|
||||
},
|
||||
},
|
||||
versionSubRow: {
|
||||
...styles.versionSubRow,
|
||||
'&.selected': {
|
||||
backgroundColor: theme.palette.action.focus,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
type DayGroupRowProps = {|
|
||||
day: number,
|
||||
versions: FilledCloudProjectVersion[],
|
||||
isOpenedInitially: boolean,
|
||||
editedVersionId: ?string,
|
||||
latestVersion: ?FilledCloudProjectVersion,
|
||||
onRenameVersion: (FilledCloudProjectVersion, string) => Promise<void>,
|
||||
loadingVersionId: ?string,
|
||||
onCancelRenaming: () => void,
|
||||
onContextMenu: (
|
||||
event: PointerEvent,
|
||||
version: FilledCloudProjectVersion
|
||||
) => void,
|
||||
openedVersionStatus: ?OpenedVersionStatus,
|
||||
usersPublicProfileByIds: UserPublicProfileByIds,
|
||||
getAnonymousAvatar: () => {| src: string, alt: string |},
|
||||
|};
|
||||
|
||||
export const DayGroupRow = ({
|
||||
day,
|
||||
versions,
|
||||
isOpenedInitially,
|
||||
editedVersionId,
|
||||
latestVersion,
|
||||
loadingVersionId,
|
||||
onRenameVersion,
|
||||
onCancelRenaming,
|
||||
onContextMenu,
|
||||
openedVersionStatus,
|
||||
usersPublicProfileByIds,
|
||||
getAnonymousAvatar,
|
||||
}: DayGroupRowProps) => {
|
||||
const [isOpen, setIsOpen] = React.useState<boolean>(isOpenedInitially);
|
||||
const displayYear = new Date(day).getFullYear() !== thisYear;
|
||||
const namedVersions = React.useMemo(
|
||||
() => versions.filter(version => version.label),
|
||||
[versions]
|
||||
);
|
||||
const isLatestVersionInThisDayGroup = latestVersion
|
||||
? versions.find(version => version.id === latestVersion.id)
|
||||
: false;
|
||||
const isOpenedVersionInThisDayGroup = openedVersionStatus
|
||||
? versions.find(version => version.id === openedVersionStatus.version.id)
|
||||
: false;
|
||||
|
||||
const shouldHighlightDay =
|
||||
isOpenedVersionInThisDayGroup &&
|
||||
!!openedVersionStatus &&
|
||||
!openedVersionStatus.version.label &&
|
||||
!isOpen;
|
||||
const shouldDisplayLatestIndicatorOnDay =
|
||||
isLatestVersionInThisDayGroup &&
|
||||
latestVersion &&
|
||||
!latestVersion.label &&
|
||||
!isOpen;
|
||||
|
||||
const classes = useClassesForDayCollapse();
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<React.Fragment>
|
||||
<ButtonBase
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className={classes.root}
|
||||
>
|
||||
<Column noMargin expand>
|
||||
<div
|
||||
className={`${classes.datSubRow}${
|
||||
shouldHighlightDay ? ' selected' : ''
|
||||
}`}
|
||||
>
|
||||
{isOpen ? <ChevronArrowBottom /> : <ChevronArrowRight />}
|
||||
<Line
|
||||
noMargin
|
||||
justifyContent="space-between"
|
||||
expand
|
||||
alignItems="center"
|
||||
>
|
||||
<Text noMargin>
|
||||
{i18n.date(day, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: displayYear ? 'numeric' : undefined,
|
||||
})}
|
||||
</Text>
|
||||
{shouldHighlightDay && openedVersionStatus ? (
|
||||
<StatusIndicator status={openedVersionStatus.status} />
|
||||
) : shouldDisplayLatestIndicatorOnDay ? (
|
||||
<StatusIndicator status="latest" />
|
||||
) : null}
|
||||
</Line>
|
||||
</div>
|
||||
{namedVersions && (
|
||||
<Collapse in={!isOpen}>
|
||||
<ColumnStackLayout noMargin>
|
||||
{namedVersions.map(version => {
|
||||
const shouldHighlightVersion =
|
||||
openedVersionStatus &&
|
||||
openedVersionStatus.version.id === version.id;
|
||||
const isLatestVersion =
|
||||
latestVersion && latestVersion.id === version.id;
|
||||
return (
|
||||
<div
|
||||
key={version.id}
|
||||
className={`${classes.versionSubRow}${
|
||||
shouldHighlightVersion ? ' selected' : ''
|
||||
}`}
|
||||
>
|
||||
<div style={styles.greyed}>
|
||||
<Text size="body-small" noMargin>
|
||||
{version.label}
|
||||
</Text>
|
||||
</div>
|
||||
{shouldHighlightVersion && openedVersionStatus ? (
|
||||
<StatusIndicator
|
||||
status={openedVersionStatus.status}
|
||||
/>
|
||||
) : isLatestVersion ? (
|
||||
<StatusIndicator status="latest" />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</ColumnStackLayout>
|
||||
</Collapse>
|
||||
)}
|
||||
</Column>
|
||||
</ButtonBase>
|
||||
<Collapse in={isOpen}>
|
||||
<div style={styles.versionsContainer}>
|
||||
{versions.map(version => (
|
||||
<ProjectVersionRow
|
||||
key={version.id}
|
||||
isLatest={
|
||||
latestVersion ? latestVersion.id === version.id : false
|
||||
}
|
||||
version={version}
|
||||
onRename={onRenameVersion}
|
||||
isLoading={loadingVersionId === version.id}
|
||||
onCancelRenaming={onCancelRenaming}
|
||||
usersPublicProfileByIds={usersPublicProfileByIds}
|
||||
isEditing={version.id === editedVersionId}
|
||||
onContextMenu={onContextMenu}
|
||||
getAnonymousAvatar={getAnonymousAvatar}
|
||||
openedVersionStatus={openedVersionStatus}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Collapse>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
};
|
16
newIDE/app/src/VersionHistory/Utils.js
Normal file
16
newIDE/app/src/VersionHistory/Utils.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { VersionRestoringStatus } from '.';
|
||||
|
||||
export const getStatusColor = (
|
||||
gdevelopTheme: GDevelopTheme,
|
||||
status: VersionRestoringStatus
|
||||
) => {
|
||||
return status === 'unsavedChanges'
|
||||
? gdevelopTheme.statusIndicator.error
|
||||
: status === 'saving'
|
||||
? gdevelopTheme.statusIndicator.warning
|
||||
: status === 'latest'
|
||||
? gdevelopTheme.palette.secondary
|
||||
: status === 'opened'
|
||||
? gdevelopTheme.statusIndicator.warning
|
||||
: gdevelopTheme.statusIndicator.success;
|
||||
};
|
279
newIDE/app/src/VersionHistory/index.js
Normal file
279
newIDE/app/src/VersionHistory/index.js
Normal file
@@ -0,0 +1,279 @@
|
||||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import { I18n } from '@lingui/react';
|
||||
import { Trans, t } from '@lingui/macro';
|
||||
import { type I18n as I18nType } from '@lingui/core';
|
||||
import { type FilledCloudProjectVersion } from '../Utils/GDevelopServices/Project';
|
||||
import {
|
||||
getUserPublicProfilesByIds,
|
||||
type UserPublicProfileByIds,
|
||||
} from '../Utils/GDevelopServices/User';
|
||||
import { Column } from '../UI/Grid';
|
||||
import ContextMenu, { type ContextMenuInterface } from '../UI/Menu/ContextMenu';
|
||||
import FlatButton from '../UI/FlatButton';
|
||||
import { DayGroupRow } from './ProjectVersionRow';
|
||||
import EmptyMessage from '../UI/EmptyMessage';
|
||||
|
||||
const anonymousAvatars = [
|
||||
{ src: 'res/avatar/green-hero.svg', alt: 'Green hero avatar' },
|
||||
{ src: 'res/avatar/red-hero.svg', alt: 'Red hero avatar' },
|
||||
{ src: 'res/avatar/ghost.svg', alt: 'Ghost avatar' },
|
||||
{ src: 'res/avatar/pink-cloud.svg', alt: 'Pink cloud avatar' },
|
||||
];
|
||||
|
||||
type VersionsGroupedByDay = {|
|
||||
[day: number]: Array<FilledCloudProjectVersion>,
|
||||
|};
|
||||
|
||||
const groupVersionsByDay = (
|
||||
versions: Array<FilledCloudProjectVersion>
|
||||
): VersionsGroupedByDay => {
|
||||
if (versions.length === 0) return {};
|
||||
|
||||
const versionsGroupedByDay = {};
|
||||
versions.forEach(version => {
|
||||
const dayDate = new Date(version.createdAt.slice(0, 10)).getTime();
|
||||
if (!versionsGroupedByDay[dayDate]) {
|
||||
versionsGroupedByDay[dayDate] = [version];
|
||||
} else {
|
||||
versionsGroupedByDay[dayDate].push(version);
|
||||
}
|
||||
});
|
||||
return versionsGroupedByDay;
|
||||
};
|
||||
|
||||
export type VersionRestoringStatus =
|
||||
| 'opened'
|
||||
| 'unsavedChanges'
|
||||
| 'saving'
|
||||
| 'latest';
|
||||
export type OpenedVersionStatus = {|
|
||||
version: FilledCloudProjectVersion,
|
||||
status: VersionRestoringStatus,
|
||||
|};
|
||||
|
||||
type Props = {|
|
||||
projectId: string,
|
||||
versions: Array<FilledCloudProjectVersion>,
|
||||
onRenameVersion: (
|
||||
FilledCloudProjectVersion,
|
||||
{| label: string |}
|
||||
) => Promise<void>,
|
||||
openedVersionStatus: ?OpenedVersionStatus,
|
||||
onLoadMore: () => Promise<void>,
|
||||
canLoadMore: boolean,
|
||||
onCheckoutVersion: FilledCloudProjectVersion => Promise<void>,
|
||||
isVisible: boolean,
|
||||
|};
|
||||
|
||||
const VersionHistory = React.memo<Props>(
|
||||
({
|
||||
projectId,
|
||||
versions,
|
||||
onRenameVersion,
|
||||
openedVersionStatus,
|
||||
onLoadMore,
|
||||
canLoadMore,
|
||||
onCheckoutVersion,
|
||||
}) => {
|
||||
const [
|
||||
usersPublicProfileByIds,
|
||||
setUsersPublicProfileByIds,
|
||||
] = React.useState<?UserPublicProfileByIds>();
|
||||
const [editedVersionId, setEditedVersionId] = React.useState<?string>(null);
|
||||
const [
|
||||
versionIdBeingRenamed,
|
||||
setVersionIdBeingRenamed,
|
||||
] = React.useState<?string>(null);
|
||||
const [
|
||||
isLoadingMoreVersions,
|
||||
setIsLoadingMoreVersions,
|
||||
] = React.useState<boolean>(false);
|
||||
const contextMenuRef = React.useRef<?ContextMenuInterface>(null);
|
||||
|
||||
const userIdsToFetch = React.useMemo(
|
||||
() => new Set(versions.map(version => version.userId).filter(Boolean)),
|
||||
[versions]
|
||||
);
|
||||
const latestVersion = versions[0] || null;
|
||||
|
||||
const versionsGroupedByDay = React.useMemo(
|
||||
() => groupVersionsByDay(versions),
|
||||
[versions]
|
||||
);
|
||||
const days = Object.keys(versionsGroupedByDay)
|
||||
.map(dayStr => Number(dayStr))
|
||||
.sort()
|
||||
.reverse();
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
(async () => {
|
||||
if (!userIdsToFetch) return;
|
||||
if (userIdsToFetch.size === 0) {
|
||||
setUsersPublicProfileByIds({});
|
||||
return;
|
||||
}
|
||||
const _usersPublicProfileByIds = await getUserPublicProfilesByIds(
|
||||
Array.from(userIdsToFetch)
|
||||
);
|
||||
setUsersPublicProfileByIds(_usersPublicProfileByIds);
|
||||
})();
|
||||
},
|
||||
[userIdsToFetch]
|
||||
);
|
||||
|
||||
const buildVersionMenuTemplate = React.useCallback(
|
||||
(i18n: I18nType, options: { version: FilledCloudProjectVersion }) => {
|
||||
const isNotLatestVersionAndUserIsNotNavigatingHistory =
|
||||
!openedVersionStatus &&
|
||||
latestVersion &&
|
||||
latestVersion.id !== options.version.id;
|
||||
const isNotTheCurrentlyOpenedVersion =
|
||||
!!openedVersionStatus &&
|
||||
openedVersionStatus.version.id !== options.version.id;
|
||||
const isComingBackToLatestVersionAfterNavigating =
|
||||
!!openedVersionStatus &&
|
||||
latestVersion &&
|
||||
latestVersion.id === options.version.id;
|
||||
|
||||
return [
|
||||
{
|
||||
label: i18n._(
|
||||
options.version.label ? t`Edit name` : t`Name version`
|
||||
),
|
||||
click: () => {
|
||||
setEditedVersionId(options.version.id);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: isComingBackToLatestVersionAfterNavigating
|
||||
? i18n._(t`Come back to latest version`)
|
||||
: i18n._(t`Open version`),
|
||||
click: () => {
|
||||
onCheckoutVersion(options.version);
|
||||
},
|
||||
enabled:
|
||||
isNotLatestVersionAndUserIsNotNavigatingHistory ||
|
||||
isNotTheCurrentlyOpenedVersion,
|
||||
},
|
||||
];
|
||||
},
|
||||
[onCheckoutVersion, latestVersion, openedVersionStatus]
|
||||
);
|
||||
|
||||
const renameVersion = React.useCallback(
|
||||
async (version: FilledCloudProjectVersion, newName: string) => {
|
||||
setEditedVersionId(null);
|
||||
setVersionIdBeingRenamed(version.id);
|
||||
try {
|
||||
await onRenameVersion(version, { label: newName });
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'An error occurred while rename project version:',
|
||||
error
|
||||
);
|
||||
} finally {
|
||||
setVersionIdBeingRenamed(null);
|
||||
}
|
||||
},
|
||||
[onRenameVersion]
|
||||
);
|
||||
|
||||
const onCancelRenaming = React.useCallback(() => {
|
||||
setEditedVersionId(null);
|
||||
}, []);
|
||||
|
||||
const openContextMenu = React.useCallback(
|
||||
(event: PointerEvent, version: FilledCloudProjectVersion) => {
|
||||
const { current: contextMenu } = contextMenuRef;
|
||||
if (!contextMenu) return;
|
||||
contextMenu.open(event.clientX, event.clientY, { version });
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const loadMore = React.useCallback(
|
||||
async () => {
|
||||
setIsLoadingMoreVersions(true);
|
||||
try {
|
||||
await onLoadMore();
|
||||
} finally {
|
||||
setIsLoadingMoreVersions(false);
|
||||
}
|
||||
},
|
||||
[onLoadMore]
|
||||
);
|
||||
|
||||
const getAnonymousAvatar = React.useCallback(
|
||||
() => {
|
||||
let projectIdAsNumber = 0;
|
||||
projectId.split('').forEach(character => {
|
||||
projectIdAsNumber += projectId.charCodeAt(0);
|
||||
});
|
||||
return anonymousAvatars[projectIdAsNumber % anonymousAvatars.length];
|
||||
},
|
||||
[projectId]
|
||||
);
|
||||
|
||||
if (!usersPublicProfileByIds) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<Column noMargin>
|
||||
{days.map((day, index) => {
|
||||
const dayVersions = versionsGroupedByDay[day];
|
||||
if (!dayVersions || dayVersions.length === 0) return null;
|
||||
return (
|
||||
<DayGroupRow
|
||||
key={day}
|
||||
versions={dayVersions}
|
||||
latestVersion={latestVersion}
|
||||
day={day}
|
||||
isOpenedInitially={index === 0}
|
||||
usersPublicProfileByIds={usersPublicProfileByIds}
|
||||
onRenameVersion={renameVersion}
|
||||
onCancelRenaming={onCancelRenaming}
|
||||
onContextMenu={openContextMenu}
|
||||
editedVersionId={editedVersionId}
|
||||
loadingVersionId={versionIdBeingRenamed}
|
||||
getAnonymousAvatar={getAnonymousAvatar}
|
||||
openedVersionStatus={openedVersionStatus}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{canLoadMore ? (
|
||||
<FlatButton
|
||||
primary
|
||||
disabled={isLoadingMoreVersions || !canLoadMore}
|
||||
label={
|
||||
isLoadingMoreVersions ? (
|
||||
<Trans>Loading...</Trans>
|
||||
) : (
|
||||
<Trans>Show older</Trans>
|
||||
)
|
||||
}
|
||||
onClick={loadMore}
|
||||
/>
|
||||
) : (
|
||||
<EmptyMessage>
|
||||
<Trans>This is the end of the version history.</Trans>
|
||||
</EmptyMessage>
|
||||
)}
|
||||
</Column>
|
||||
)}
|
||||
</I18n>
|
||||
<ContextMenu
|
||||
ref={contextMenuRef}
|
||||
buildMenuTemplate={buildVersionMenuTemplate}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
(prevProps, nextProps) => !prevProps.isVisible && !nextProps.isVisible
|
||||
);
|
||||
|
||||
export default VersionHistory;
|
470
newIDE/app/src/VersionHistory/useVersionHistory.js
Normal file
470
newIDE/app/src/VersionHistory/useVersionHistory.js
Normal file
@@ -0,0 +1,470 @@
|
||||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import { t, Trans } from '@lingui/macro';
|
||||
import { I18n } from '@lingui/react';
|
||||
import Drawer from '@material-ui/core/Drawer';
|
||||
import DrawerTopBar from '../UI/DrawerTopBar';
|
||||
import {
|
||||
listVersionsOfProject,
|
||||
type FilledCloudProjectVersion,
|
||||
updateCloudProjectVersion,
|
||||
} from '../Utils/GDevelopServices/Project';
|
||||
import type { FileMetadata, StorageProvider } from '../ProjectsStorage';
|
||||
import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
|
||||
import { canSeeCloudProjectHistory } from '../Utils/GDevelopServices/Usage';
|
||||
import { Column, Line } from '../UI/Grid';
|
||||
import VersionHistory, { type OpenedVersionStatus } from '.';
|
||||
import UnsavedChangesContext from '../MainFrame/UnsavedChangesContext';
|
||||
import AlertMessage from '../UI/AlertMessage';
|
||||
import { ColumnStackLayout } from '../UI/Layout';
|
||||
import RaisedButton from '../UI/RaisedButton';
|
||||
import { SubscriptionSuggestionContext } from '../Profile/Subscription/SubscriptionSuggestionContext';
|
||||
import useAlertDialog from '../UI/Alert/useAlertDialog';
|
||||
import PlaceholderLoader from '../UI/PlaceholderLoader';
|
||||
|
||||
// TODO: If top bar is kept, persist these imports
|
||||
import History from '../UI/CustomSvgIcons/History';
|
||||
import Text from '../UI/Text';
|
||||
import { ButtonBase } from '@material-ui/core';
|
||||
import Cross from '../UI/CustomSvgIcons/Cross';
|
||||
import { useResponsiveWindowWidth } from '../UI/Reponsive/ResponsiveWindowMeasurer';
|
||||
import GDevelopThemeContext from '../UI/Theme/GDevelopThemeContext';
|
||||
import { getStatusColor } from './Utils';
|
||||
|
||||
const styles = {
|
||||
drawerContent: {
|
||||
width: 320,
|
||||
overflowX: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
};
|
||||
|
||||
const mergeVersionsLists = (
|
||||
list1: FilledCloudProjectVersion[],
|
||||
list2: FilledCloudProjectVersion[]
|
||||
) => {
|
||||
const mostRecentVersionDateInList2 = Date.parse(list2[0].createdAt);
|
||||
const moreRecentVersionsInList1 = list1.filter(
|
||||
version => Date.parse(version.createdAt) > mostRecentVersionDateInList2
|
||||
);
|
||||
return [...moreRecentVersionsInList1, ...list2];
|
||||
};
|
||||
|
||||
type PaginationState = {|
|
||||
versions: ?(FilledCloudProjectVersion[]),
|
||||
nextPageUri: ?Object,
|
||||
|};
|
||||
|
||||
const emptyPaginationState: PaginationState = {
|
||||
versions: null,
|
||||
nextPageUri: null,
|
||||
};
|
||||
|
||||
type Props = {|
|
||||
getStorageProvider: () => StorageProvider,
|
||||
fileMetadata: ?FileMetadata,
|
||||
isSavingProject: boolean,
|
||||
onOpenCloudProjectOnSpecificVersion: ({|
|
||||
fileMetadata: FileMetadata,
|
||||
versionId: string,
|
||||
ignoreUnsavedChanges: boolean,
|
||||
|}) => Promise<void>,
|
||||
|};
|
||||
|
||||
const useVersionHistory = ({
|
||||
fileMetadata,
|
||||
isSavingProject,
|
||||
getStorageProvider,
|
||||
onOpenCloudProjectOnSpecificVersion,
|
||||
}: Props) => {
|
||||
const windowWidth = useResponsiveWindowWidth();
|
||||
const isMobile = windowWidth === 'small';
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
const { hasUnsavedChanges } = React.useContext(UnsavedChangesContext);
|
||||
const { showAlert } = useAlertDialog();
|
||||
const { openSubscriptionDialog } = React.useContext(
|
||||
SubscriptionSuggestionContext
|
||||
);
|
||||
const authenticatedUser = React.useContext(AuthenticatedUserContext);
|
||||
const ignoreFileMetadataChangesRef = React.useRef<boolean>(false);
|
||||
const preventEffectsRunningRef = React.useRef<boolean>(false);
|
||||
const {
|
||||
subscription,
|
||||
getAuthorizationHeader,
|
||||
firebaseUser,
|
||||
} = authenticatedUser;
|
||||
const storageProvider = getStorageProvider();
|
||||
const [state, setState] = React.useState<PaginationState>(
|
||||
emptyPaginationState
|
||||
);
|
||||
const [
|
||||
checkedOutVersionStatus,
|
||||
setCheckedOutVersionStatus,
|
||||
] = React.useState<?OpenedVersionStatus>(null);
|
||||
const [
|
||||
versionHistoryPanelOpen,
|
||||
setVersionHistoryPanelOpen,
|
||||
] = React.useState<boolean>(false);
|
||||
const isCloudProject = storageProvider.internalName === 'Cloud';
|
||||
const isUserAllowedToSeeVersionHistory = canSeeCloudProjectHistory(
|
||||
subscription
|
||||
);
|
||||
const [cloudProjectId, setCloudProjectId] = React.useState<?string>(
|
||||
isCloudProject && fileMetadata ? fileMetadata.fileIdentifier : null
|
||||
);
|
||||
const [
|
||||
cloudProjectLastModifiedDate,
|
||||
setCloudProjectLastModifiedDate,
|
||||
] = React.useState<?number>(
|
||||
isCloudProject && fileMetadata ? fileMetadata.lastModifiedDate : null
|
||||
);
|
||||
const showVersionHistoryButton =
|
||||
isCloudProject && isUserAllowedToSeeVersionHistory;
|
||||
const latestVersionId =
|
||||
state.versions && state.versions[0] ? state.versions[0].id : null;
|
||||
|
||||
// This effect is used to avoid having cloudProjectId and cloudProjectLastModifiedDate
|
||||
// set to null when checking out a version, unmounting the VersionHistory component,
|
||||
// making it lose its state (fetched versions and collapse states).
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (ignoreFileMetadataChangesRef.current) return;
|
||||
setCloudProjectId(
|
||||
isCloudProject && fileMetadata ? fileMetadata.fileIdentifier : null
|
||||
);
|
||||
setCloudProjectLastModifiedDate(
|
||||
isCloudProject && fileMetadata ? fileMetadata.lastModifiedDate : null
|
||||
);
|
||||
},
|
||||
[isCloudProject, fileMetadata]
|
||||
);
|
||||
|
||||
// This effect is run in 2 cases:
|
||||
// - at start up to list the versions (when both cloudProjectId and
|
||||
// cloudProjectLastModifiedDate are set at the same time)
|
||||
// - when a new save is done (cloudProjectLastModifiedDate is updated)
|
||||
React.useEffect(
|
||||
() => {
|
||||
(async () => {
|
||||
if (preventEffectsRunningRef.current) return;
|
||||
if (!cloudProjectId || !showVersionHistoryButton) {
|
||||
setState(emptyPaginationState);
|
||||
return;
|
||||
}
|
||||
const listing = await listVersionsOfProject(
|
||||
getAuthorizationHeader,
|
||||
firebaseUser,
|
||||
cloudProjectId,
|
||||
// This effect should only run when the project changes, or the user subscription.
|
||||
// So we fetch the first page of versions.
|
||||
{ forceUri: null }
|
||||
);
|
||||
if (!listing) return;
|
||||
|
||||
setState(currentState => {
|
||||
if (!currentState.versions) {
|
||||
// Initial loading of versions.
|
||||
return {
|
||||
versions: listing.versions,
|
||||
nextPageUri: listing.nextPageUri,
|
||||
};
|
||||
}
|
||||
// From here, we're in the case where some versions where already loaded
|
||||
// so the effect is triggered by a modification of cloudProjectLastModifiedDate.
|
||||
// To the versions that are fetched should not replace the whole history that
|
||||
// the user maybe spent time to load.
|
||||
return {
|
||||
versions: mergeVersionsLists(
|
||||
listing.versions,
|
||||
currentState.versions
|
||||
),
|
||||
// Do not change next page URI.
|
||||
nextPageUri: currentState.nextPageUri,
|
||||
};
|
||||
});
|
||||
})();
|
||||
},
|
||||
[
|
||||
storageProvider,
|
||||
getAuthorizationHeader,
|
||||
firebaseUser,
|
||||
cloudProjectId,
|
||||
showVersionHistoryButton,
|
||||
cloudProjectLastModifiedDate,
|
||||
]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (preventEffectsRunningRef.current) return;
|
||||
setCheckedOutVersionStatus(currentCheckedOutVersionStatus => {
|
||||
if (
|
||||
!currentCheckedOutVersionStatus ||
|
||||
(hasUnsavedChanges &&
|
||||
currentCheckedOutVersionStatus.status === 'unsavedChanges')
|
||||
) {
|
||||
return currentCheckedOutVersionStatus;
|
||||
}
|
||||
|
||||
return {
|
||||
version: currentCheckedOutVersionStatus.version,
|
||||
status: 'unsavedChanges',
|
||||
};
|
||||
});
|
||||
},
|
||||
[hasUnsavedChanges]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (preventEffectsRunningRef.current) return;
|
||||
setCheckedOutVersionStatus(currentCheckedOutVersionStatus => {
|
||||
if (
|
||||
!currentCheckedOutVersionStatus ||
|
||||
(isSavingProject &&
|
||||
currentCheckedOutVersionStatus.status === 'saving')
|
||||
) {
|
||||
return currentCheckedOutVersionStatus;
|
||||
}
|
||||
|
||||
return isSavingProject
|
||||
? {
|
||||
version: currentCheckedOutVersionStatus.version,
|
||||
status: 'saving',
|
||||
}
|
||||
: null;
|
||||
});
|
||||
},
|
||||
[isSavingProject]
|
||||
);
|
||||
|
||||
const onLoadMoreVersions = React.useCallback(
|
||||
async () => {
|
||||
if (!cloudProjectId) return;
|
||||
const listing = await listVersionsOfProject(
|
||||
getAuthorizationHeader,
|
||||
firebaseUser,
|
||||
cloudProjectId,
|
||||
{ forceUri: state.nextPageUri }
|
||||
);
|
||||
if (!listing) return;
|
||||
setState({
|
||||
versions: [...(state.versions || []), ...listing.versions],
|
||||
nextPageUri: listing.nextPageUri,
|
||||
});
|
||||
},
|
||||
[getAuthorizationHeader, firebaseUser, cloudProjectId, state]
|
||||
);
|
||||
|
||||
const openVersionHistoryPanel = React.useCallback(() => {
|
||||
setVersionHistoryPanelOpen(true);
|
||||
}, []);
|
||||
|
||||
const onQuitVersionHistory = React.useCallback(
|
||||
async () => {
|
||||
if (!fileMetadata || !checkedOutVersionStatus || !latestVersionId) return;
|
||||
preventEffectsRunningRef.current = true;
|
||||
ignoreFileMetadataChangesRef.current = true;
|
||||
try {
|
||||
await onOpenCloudProjectOnSpecificVersion({
|
||||
fileMetadata,
|
||||
versionId: latestVersionId,
|
||||
ignoreUnsavedChanges: true,
|
||||
});
|
||||
setCheckedOutVersionStatus(null);
|
||||
} finally {
|
||||
preventEffectsRunningRef.current = false;
|
||||
ignoreFileMetadataChangesRef.current = false;
|
||||
}
|
||||
},
|
||||
[
|
||||
fileMetadata,
|
||||
onOpenCloudProjectOnSpecificVersion,
|
||||
checkedOutVersionStatus,
|
||||
latestVersionId,
|
||||
]
|
||||
);
|
||||
|
||||
const onCheckoutVersion = React.useCallback(
|
||||
async (version: FilledCloudProjectVersion) => {
|
||||
if (!fileMetadata) return;
|
||||
if (!checkedOutVersionStatus && hasUnsavedChanges) {
|
||||
await showAlert({
|
||||
title: t`There are unsaved changes`,
|
||||
message: t`Save your project before using the version history.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (checkedOutVersionStatus && version.id === latestVersionId) {
|
||||
await onQuitVersionHistory();
|
||||
setVersionHistoryPanelOpen(false);
|
||||
return;
|
||||
}
|
||||
preventEffectsRunningRef.current = true;
|
||||
ignoreFileMetadataChangesRef.current = true;
|
||||
try {
|
||||
await onOpenCloudProjectOnSpecificVersion({
|
||||
fileMetadata,
|
||||
versionId: version.id,
|
||||
ignoreUnsavedChanges: true,
|
||||
});
|
||||
setCheckedOutVersionStatus({ version, status: 'opened' });
|
||||
} finally {
|
||||
preventEffectsRunningRef.current = false;
|
||||
ignoreFileMetadataChangesRef.current = false;
|
||||
}
|
||||
},
|
||||
[
|
||||
fileMetadata,
|
||||
onOpenCloudProjectOnSpecificVersion,
|
||||
checkedOutVersionStatus,
|
||||
showAlert,
|
||||
hasUnsavedChanges,
|
||||
latestVersionId,
|
||||
onQuitVersionHistory,
|
||||
]
|
||||
);
|
||||
|
||||
const onRenameVersion = React.useCallback(
|
||||
async (
|
||||
version: FilledCloudProjectVersion,
|
||||
attributes: {| label: string |}
|
||||
) => {
|
||||
if (!cloudProjectId) return;
|
||||
const updatedVersion = await updateCloudProjectVersion(
|
||||
authenticatedUser,
|
||||
cloudProjectId,
|
||||
version.id,
|
||||
attributes
|
||||
);
|
||||
if (!updatedVersion) return;
|
||||
setState(currentState => {
|
||||
if (!currentState.versions) return currentState;
|
||||
return {
|
||||
versions: currentState.versions.map(version =>
|
||||
version.id === updatedVersion.id
|
||||
? { ...version, label: updatedVersion.label }
|
||||
: version
|
||||
),
|
||||
nextPageUri: currentState.nextPageUri,
|
||||
};
|
||||
});
|
||||
},
|
||||
[authenticatedUser, cloudProjectId]
|
||||
);
|
||||
|
||||
const renderVersionHistoryPanel = () => {
|
||||
return (
|
||||
<Drawer
|
||||
open={versionHistoryPanelOpen}
|
||||
PaperProps={{
|
||||
style: styles.drawerContent,
|
||||
className: 'safe-area-aware-left-container',
|
||||
}}
|
||||
ModalProps={{
|
||||
keepMounted: true,
|
||||
}}
|
||||
onClose={() => setVersionHistoryPanelOpen(false)}
|
||||
>
|
||||
<DrawerTopBar
|
||||
title={<Trans>File history</Trans>}
|
||||
onClose={() => setVersionHistoryPanelOpen(false)}
|
||||
id="version-history-drawer"
|
||||
/>
|
||||
<Line useFullHeight expand>
|
||||
<Column expand>
|
||||
{!cloudProjectId ? (
|
||||
<AlertMessage kind="info">
|
||||
<Trans>Open a cloud project to see the version history.</Trans>
|
||||
</AlertMessage>
|
||||
) : !isCloudProject ? (
|
||||
<AlertMessage kind="info">
|
||||
<Trans>
|
||||
The version history is available for cloud projects only.
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
) : !isUserAllowedToSeeVersionHistory ? (
|
||||
<ColumnStackLayout>
|
||||
<AlertMessage kind="info">
|
||||
<Trans>
|
||||
The version history is not included in your subscription.
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
<RaisedButton
|
||||
primary
|
||||
label={<Trans>Upgrade my subscription</Trans>}
|
||||
onClick={() =>
|
||||
openSubscriptionDialog({ reason: 'Version history' })
|
||||
}
|
||||
/>
|
||||
</ColumnStackLayout>
|
||||
) : state.versions ? (
|
||||
<VersionHistory
|
||||
isVisible={versionHistoryPanelOpen}
|
||||
projectId={fileMetadata ? fileMetadata.fileIdentifier : ''}
|
||||
canLoadMore={!!state.nextPageUri}
|
||||
onCheckoutVersion={onCheckoutVersion}
|
||||
onLoadMore={onLoadMoreVersions}
|
||||
onRenameVersion={onRenameVersion}
|
||||
openedVersionStatus={checkedOutVersionStatus}
|
||||
versions={state.versions}
|
||||
/>
|
||||
) : (
|
||||
<PlaceholderLoader />
|
||||
)}
|
||||
</Column>
|
||||
</Line>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
const renderMobileTopBarStatus = () => {
|
||||
if (!checkedOutVersionStatus || !isMobile) return null;
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: '2px 10px 2px 10px',
|
||||
color: '#111111',
|
||||
backgroundColor: getStatusColor(
|
||||
gdevelopTheme,
|
||||
checkedOutVersionStatus.status
|
||||
),
|
||||
}}
|
||||
>
|
||||
<History fontSize="small" />
|
||||
<Text noMargin color="inherit" size="body-small">
|
||||
{i18n.date(
|
||||
Date.parse(checkedOutVersionStatus.version.createdAt),
|
||||
{
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short',
|
||||
}
|
||||
)}
|
||||
</Text>
|
||||
<ButtonBase onClick={onQuitVersionHistory}>
|
||||
<Cross fontSize="small" />
|
||||
</ButtonBase>
|
||||
</div>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
checkedOutVersionStatus,
|
||||
showVersionHistoryButton,
|
||||
openVersionHistoryPanel,
|
||||
renderVersionHistoryPanel,
|
||||
renderMobileTopBarStatus,
|
||||
onQuitVersionHistory,
|
||||
};
|
||||
};
|
||||
|
||||
export default useVersionHistory;
|
@@ -47,6 +47,7 @@ const defaultProps: MainFrameToolbarProps = {
|
||||
showProjectButtons: true,
|
||||
toggleProjectManager: () => {},
|
||||
openShareDialog: () => {},
|
||||
isSharingEnabled: true,
|
||||
|
||||
onPreviewWithoutHotReload: () => {},
|
||||
onOpenDebugger: () => {},
|
||||
@@ -58,6 +59,8 @@ const defaultProps: MainFrameToolbarProps = {
|
||||
hasPreviewsRunning: false,
|
||||
canSave: true,
|
||||
onSave: async () => {},
|
||||
showVersionHistoryButton: true,
|
||||
onOpenVersionHistory: () => {},
|
||||
previewState: {
|
||||
isPreviewOverriden: false,
|
||||
previewLayoutName: null,
|
||||
|
@@ -8,7 +8,7 @@ import emptyGameContent from './fixtures/emptyGame.json';
|
||||
|
||||
import {
|
||||
apiClient as projectApiAxiosClient,
|
||||
type CloudProjectVersion,
|
||||
type FilledCloudProjectVersion,
|
||||
projectResourcesClient as resourcesAxiosClient,
|
||||
} from '../../../Utils/GDevelopServices/Project';
|
||||
import CloudProjectRecoveryDialog from '../../../ProjectsStorage/CloudStorageProvider/CloudProjectRecoveryDialog';
|
||||
@@ -29,7 +29,7 @@ export default {
|
||||
|
||||
export const Default = () => {
|
||||
const projectId = 'fb4d878a-1935-4916-b681-f9235475d354';
|
||||
const versions: Array<CloudProjectVersion> = [
|
||||
const versions: Array<FilledCloudProjectVersion> = [
|
||||
{
|
||||
id: '8e067d2d-6f08-4f93-ad2d-f3ad5ca3c69c',
|
||||
projectId,
|
||||
@@ -99,7 +99,7 @@ export const Default = () => {
|
||||
|
||||
export const NoFallbackVersion = () => {
|
||||
const projectId = 'fb4d878a-1935-4916-b681-f9235475d354';
|
||||
const versions: Array<CloudProjectVersion> = [
|
||||
const versions: Array<FilledCloudProjectVersion> = [
|
||||
{
|
||||
id: '8e067d2d-6f08-4f93-ad2d-f3ad5ca3c69c',
|
||||
projectId,
|
||||
|
@@ -0,0 +1,271 @@
|
||||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import { type FilledCloudProjectVersion } from '../../../Utils/GDevelopServices/Project';
|
||||
|
||||
import muiDecorator from '../../ThemeDecorator';
|
||||
import paperDecorator from '../../PaperDecorator';
|
||||
import VersionHistory from '../../../VersionHistory';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import type { OpenedVersionStatus } from '../../../VersionHistory';
|
||||
import { apiClient as projectApiAxiosClient } from '../../../Utils/GDevelopServices/User';
|
||||
import { GDevelopUserApi } from '../../../Utils/GDevelopServices/ApiConfigs';
|
||||
import { delay } from '../../../Utils/Delay';
|
||||
import {
|
||||
ColumnStackLayout,
|
||||
ResponsiveLineStackLayout,
|
||||
} from '../../../UI/Layout';
|
||||
import FlatButton from '../../../UI/FlatButton';
|
||||
import { Column } from '../../../UI/Grid';
|
||||
import OpenedVersionStatusChip from '../../../VersionHistory/OpenedVersionStatusChip';
|
||||
|
||||
export default {
|
||||
title: 'VersionHistory',
|
||||
component: VersionHistory,
|
||||
decorators: [paperDecorator, muiDecorator],
|
||||
};
|
||||
|
||||
const projectId = 'fb4d878a-1935-4916-b681-f9235475d354';
|
||||
const initialVersions: Array<FilledCloudProjectVersion> = [
|
||||
{
|
||||
id: 'dddbe02b-be5e-4008-aff4-90ab32e3315a',
|
||||
projectId,
|
||||
createdAt: '2023-12-04T17:21:26.729Z',
|
||||
previousVersion: '0b43b267-ae5a-4822-a926-af14bf52f06d',
|
||||
userId: 'a9bc54be-07e1-4f29-9739-5fbec2b04da7',
|
||||
},
|
||||
{
|
||||
id: '0b43b267-ae5a-4822-a926-af14bf52f06d',
|
||||
projectId,
|
||||
createdAt: '2023-12-04T16:02:26.729Z',
|
||||
previousVersion: '24ab6329-9fed-41a3-989f-f88b90647658',
|
||||
userId: 'a9bc54be-07e1-4f29-9739-5fbec2b04da7',
|
||||
},
|
||||
{
|
||||
id: '24ab6329-9fed-41a3-989f-f88b90647658',
|
||||
projectId,
|
||||
label: 'Client presentation for the newcomers of this year',
|
||||
createdAt: '2023-08-01T07:21:26.729Z',
|
||||
previousVersion: '30194561-9651-445b-8cec-702310ca2ec8',
|
||||
userId: 'c73c4d69-86a2-441b-a8b7-afe6b8fce810',
|
||||
},
|
||||
{
|
||||
id: '30194561-9651-445b-8cec-702310ca2ec8',
|
||||
projectId,
|
||||
createdAt: '2023-07-28T19:52:26.729Z',
|
||||
previousVersion: '8e067d2d-6f08-4f93-ad2d-f3ad5ca3c69c',
|
||||
userId: 'c73c4d69-86a2-441b-a8b7-afe6b8fce810',
|
||||
restoredFromVersion: {
|
||||
id: 'fd174383-30dd-4c69-945b-5d348b273828',
|
||||
projectId,
|
||||
createdAt: '2022-10-12T03:58:49.305Z',
|
||||
previousVersion: '4b0e3a7a-3127-4f52-b7d8-ce646728b96b',
|
||||
userId: 'a9bc54be-07e1-4f29-9739-5fbec2b04da7',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '8e067d2d-6f08-4f93-ad2d-f3ad5ca3c69c',
|
||||
projectId,
|
||||
createdAt: '2022-11-14T10:11:49.305Z',
|
||||
previousVersion: '9f9f50a3-1bb2-41c3-9ddb-feaf9be45648',
|
||||
userId: 'a9bc54be-07e1-4f29-9739-5fbec2b04da7',
|
||||
},
|
||||
{
|
||||
id: '9f9f50a3-1bb2-41c3-9ddb-feaf9be45648',
|
||||
projectId,
|
||||
createdAt: '2022-11-13T10:11:49.305Z',
|
||||
previousVersion: '5280e344-bd36-4662-9948-cb0d18928d03',
|
||||
userId: 'c73c4d69-86a2-441b-a8b7-afe6b8fce810',
|
||||
},
|
||||
{
|
||||
id: '5280e344-bd36-4662-9948-cb0d18928d03',
|
||||
projectId,
|
||||
createdAt: '2022-10-12T10:11:49.305Z',
|
||||
previousVersion: 'e51c123a-2abb-47fc-aff6-50b0572c5dc2',
|
||||
},
|
||||
];
|
||||
|
||||
const nextVersions: Array<FilledCloudProjectVersion> = [
|
||||
{
|
||||
id: 'e51c123a-2abb-47fc-aff6-50b0572c5dc2',
|
||||
projectId,
|
||||
createdAt: '2022-10-12T09:02:49.305Z',
|
||||
previousVersion: 'fd174383-30dd-4c69-945b-5d348b273828',
|
||||
userId: 'a9bc54be-07e1-4f29-9739-5fbec2b04da7',
|
||||
},
|
||||
{
|
||||
id: 'fd174383-30dd-4c69-945b-5d348b273828',
|
||||
projectId,
|
||||
createdAt: '2022-10-12T03:58:49.305Z',
|
||||
previousVersion: '4b0e3a7a-3127-4f52-b7d8-ce646728b96b',
|
||||
userId: 'a9bc54be-07e1-4f29-9739-5fbec2b04da7',
|
||||
},
|
||||
{
|
||||
id: '4b0e3a7a-3127-4f52-b7d8-ce646728b96b',
|
||||
projectId,
|
||||
createdAt: '2022-10-10T18:52:49.305Z',
|
||||
previousVersion: '76d74273-2a87-4b65-af80-ce2ce75d796c',
|
||||
userId: 'c73c4d69-86a2-441b-a8b7-afe6b8fce810',
|
||||
},
|
||||
{
|
||||
id: '76d74273-2a87-4b65-af80-ce2ce75d796c',
|
||||
projectId,
|
||||
createdAt: '2022-10-09T17:51:49.305Z',
|
||||
previousVersion: '5757f84a-c5e3-407f-9504-2dc9601382ac',
|
||||
userId: 'c73c4d69-86a2-441b-a8b7-afe6b8fce810',
|
||||
},
|
||||
{
|
||||
id: '5757f84a-c5e3-407f-9504-2dc9601382ac',
|
||||
projectId,
|
||||
createdAt: '2022-10-02T12:47:49.305Z',
|
||||
userId: 'a9bc54be-07e1-4f29-9739-5fbec2b04da7',
|
||||
previousVersion: null,
|
||||
},
|
||||
];
|
||||
|
||||
const userPublicProfilesByIds = {
|
||||
'c73c4d69-86a2-441b-a8b7-afe6b8fce810': {
|
||||
id: 'c73c4d69-86a2-441b-a8b7-afe6b8fce810',
|
||||
username: 'alex_',
|
||||
description: null,
|
||||
donateLink: null,
|
||||
discordUsername: null,
|
||||
communityLinks: {},
|
||||
iconUrl:
|
||||
'https://www.gravatar.com/avatar/6079a3eba0dc05f12034c55bbce6aaa3?s=40&d=retro',
|
||||
},
|
||||
'a9bc54be-07e1-4f29-9739-5fbec2b04da7': {
|
||||
id: '9bc54be-07e1-4f29-9739-5fbec2b04da7',
|
||||
username: 'LuniMoon',
|
||||
description: null,
|
||||
donateLink: null,
|
||||
discordUsername: null,
|
||||
communityLinks: {},
|
||||
iconUrl:
|
||||
'https://www.gravatar.com/avatar/6079a3eba0dc05f12034c55bbce65aa3?s=40&d=retro',
|
||||
},
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
const [versions, setVersions] = React.useState<FilledCloudProjectVersion[]>(
|
||||
initialVersions
|
||||
);
|
||||
const [
|
||||
openedVersionStatus,
|
||||
setOpenedVersionStatus,
|
||||
] = React.useState<?OpenedVersionStatus>(null);
|
||||
const projectServiceMock = new MockAdapter(projectApiAxiosClient, {
|
||||
delayResponse: 1000,
|
||||
});
|
||||
const latestVersion = versions[0];
|
||||
|
||||
const onCheckoutVersion = React.useCallback(
|
||||
async (version: FilledCloudProjectVersion) => {
|
||||
if (version.id === latestVersion.id) {
|
||||
setOpenedVersionStatus(null);
|
||||
return;
|
||||
}
|
||||
setOpenedVersionStatus({
|
||||
version,
|
||||
status: 'opened',
|
||||
});
|
||||
},
|
||||
[latestVersion.id]
|
||||
);
|
||||
|
||||
const onSaveCurrentlyOpenedVersion = React.useCallback(
|
||||
async () => {
|
||||
if (!openedVersionStatus) return;
|
||||
setOpenedVersionStatus({ ...openedVersionStatus, status: 'saving' });
|
||||
await delay(2000);
|
||||
|
||||
setOpenedVersionStatus(null);
|
||||
},
|
||||
[openedVersionStatus]
|
||||
);
|
||||
|
||||
const onAddChanges = React.useCallback(
|
||||
() => {
|
||||
if (!openedVersionStatus) return;
|
||||
if (!openedVersionStatus) return;
|
||||
setOpenedVersionStatus({
|
||||
...openedVersionStatus,
|
||||
status: 'unsavedChanges',
|
||||
});
|
||||
},
|
||||
[openedVersionStatus]
|
||||
);
|
||||
|
||||
const onRenameVersion = async (
|
||||
version: FilledCloudProjectVersion,
|
||||
{ label }: {| label: string |}
|
||||
) => {
|
||||
await delay(1500);
|
||||
const newVersions = [...versions];
|
||||
const index = newVersions.findIndex(version_ => version_.id === version.id);
|
||||
newVersions.splice(index, 1, { ...version, label: label || undefined });
|
||||
newVersions.forEach(version_ => {
|
||||
if (
|
||||
version_.restoredFromVersion &&
|
||||
version_.restoredFromVersion.id === version.id
|
||||
) {
|
||||
version_.restoredFromVersion = {
|
||||
id: version.id,
|
||||
createdAt: version.createdAt,
|
||||
previousVersion: version.previousVersion,
|
||||
projectId: version.projectId,
|
||||
label: label || undefined,
|
||||
};
|
||||
}
|
||||
});
|
||||
setVersions(newVersions);
|
||||
};
|
||||
|
||||
const onLoadMore = async () => {
|
||||
await delay(1000);
|
||||
setVersions([...versions, ...nextVersions]);
|
||||
};
|
||||
|
||||
const onQuitVersionExploration = async () => {
|
||||
setOpenedVersionStatus(null);
|
||||
};
|
||||
|
||||
const canLoadMore = versions.every(version => version.previousVersion);
|
||||
|
||||
projectServiceMock
|
||||
.onGet(`${GDevelopUserApi.baseUrl}/user-public-profile`)
|
||||
.reply(200, userPublicProfilesByIds)
|
||||
.onAny()
|
||||
.reply(config => {
|
||||
console.error(`Unexpected call to ${config.url} (${config.method})`);
|
||||
return [504, null];
|
||||
});
|
||||
|
||||
return (
|
||||
<ResponsiveLineStackLayout>
|
||||
<Column expand>
|
||||
<VersionHistory
|
||||
isVisible
|
||||
projectId={projectId}
|
||||
versions={versions}
|
||||
onRenameVersion={onRenameVersion}
|
||||
onLoadMore={onLoadMore}
|
||||
canLoadMore={canLoadMore}
|
||||
onCheckoutVersion={onCheckoutVersion}
|
||||
openedVersionStatus={openedVersionStatus}
|
||||
/>
|
||||
</Column>
|
||||
{openedVersionStatus && (
|
||||
<ColumnStackLayout>
|
||||
<OpenedVersionStatusChip
|
||||
openedVersionStatus={openedVersionStatus}
|
||||
onClickClose={onQuitVersionExploration}
|
||||
/>
|
||||
<FlatButton label="Save" onClick={onSaveCurrentlyOpenedVersion} />
|
||||
<FlatButton label="Add changes to version" onClick={onAddChanges} />
|
||||
</ColumnStackLayout>
|
||||
)}
|
||||
</ResponsiveLineStackLayout>
|
||||
);
|
||||
};
|
Reference in New Issue
Block a user