Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c377e70c9 | ||
|
|
67fdd9bb51 | ||
|
|
c68ec4ad16 | ||
|
|
91e87f7d16 | ||
|
|
762ffe53f0 | ||
|
|
d63c8b1dbb | ||
|
|
5e4a2b788c | ||
|
|
8caff78e08 | ||
|
|
e1c3bb6d96 | ||
|
|
aebdcc855c | ||
|
|
86fd6ec8bf | ||
|
|
b351cc599d | ||
|
|
0230546a20 | ||
|
|
984b87ead5 | ||
|
|
59c4f36b90 | ||
|
|
21a6fe3a27 | ||
|
|
02f806fc9d | ||
|
|
d9003c9d56 | ||
|
|
c944295140 | ||
|
|
2c64a62f23 | ||
|
|
8639f1838c | ||
|
|
4e80510b98 | ||
|
|
9f58bd9ee3 | ||
|
|
236cc6f872 | ||
|
|
b3d336a08f | ||
|
|
97fcbf85fc | ||
|
|
e3b3bcc963 | ||
|
|
775b805e56 | ||
|
|
a1124807ca | ||
|
|
b1674c1b0c | ||
|
|
7148733acb | ||
|
|
e98862ef66 | ||
|
|
53a805026f | ||
|
|
84b168f757 | ||
|
|
0a86a769e7 | ||
|
|
816fb10e16 | ||
|
|
eece6a5f8d | ||
|
|
27766042fa | ||
|
|
639330b9ce | ||
|
|
6f6a34ec50 | ||
|
|
2560c0736e | ||
|
|
7a2d7d035e | ||
|
|
62ee7e069f | ||
|
|
3b82d52a84 | ||
|
|
435f335cff | ||
|
|
6822d3b7dd | ||
|
|
40e9c07b00 | ||
|
|
c4bf772dd1 | ||
|
|
886dbca01c | ||
|
|
3ee5b44e7e | ||
|
|
0eb7d3017b | ||
|
|
139c9f8043 | ||
|
|
3faedc4301 | ||
|
|
faa81e77c7 | ||
|
|
58ff2606b3 | ||
|
|
dabdaf00d2 | ||
|
|
3eb56b4e9f | ||
|
|
61102e9de8 | ||
|
|
ce164d81d7 | ||
|
|
e428083da8 | ||
|
|
5afd19b1ce | ||
|
|
17ba917399 | ||
|
|
73b53988fc | ||
|
|
39e1bac823 | ||
|
|
c5f17db7d1 | ||
|
|
ca06418287 | ||
|
|
a98562e03c | ||
|
|
036c627130 | ||
|
|
dd8fb2024d | ||
|
|
c4b43dd321 | ||
|
|
f4061aedaa | ||
|
|
c3d7c086f8 | ||
|
|
5ecaa2fb44 | ||
|
|
3a35693ce3 | ||
|
|
cd23935bc3 | ||
|
|
055853d746 | ||
|
|
bae72c0413 | ||
|
|
ca927f111b | ||
|
|
13a0e9098f | ||
|
|
3d3eb3d059 | ||
|
|
f788870dc2 | ||
|
|
6eaf6312b9 | ||
|
|
d3474992b7 | ||
|
|
bdfe83e110 | ||
|
|
19c9db0eb1 | ||
|
|
2b642e9ef0 | ||
|
|
a55f9ae9ed | ||
|
|
054d3b1a8f | ||
|
|
a27b27fa71 | ||
|
|
3f51a86d9f | ||
|
|
3fe0fa9b44 | ||
|
|
269ee1dd8e | ||
|
|
46cecc0c3a | ||
|
|
b0945f80cf | ||
|
|
89c88fec4c | ||
|
|
9a43442cf5 | ||
|
|
2bf8db0dfa | ||
|
|
78c38af7a6 | ||
|
|
19e392d061 | ||
|
|
f19ac7459a | ||
|
|
3fe729c025 | ||
|
|
428b5da5b6 | ||
|
|
78d831d6af | ||
|
|
8f7a9d96f1 | ||
|
|
c23678ef3d | ||
|
|
6f2a0b057a | ||
|
|
e73c5ce91c | ||
|
|
be8873a6e0 | ||
|
|
43f5f0aebc | ||
|
|
84b6c33813 | ||
|
|
deff6dd385 | ||
|
|
ca0c47f742 | ||
|
|
5253554fff | ||
|
|
81c22f369c | ||
|
|
fde5b56e65 | ||
|
|
c7b4e1b832 | ||
|
|
a37868f921 | ||
|
|
395b55868b | ||
|
|
e3fa196ade | ||
|
|
f4a9bfac9f | ||
|
|
b6ff1f40b5 | ||
|
|
98c1846c12 | ||
|
|
03f3f12fcc | ||
|
|
758b0ef1ff | ||
|
|
68780ae6cf | ||
|
|
22e2c17ff4 | ||
|
|
9c33bbb2ee | ||
|
|
b63ac51fb1 | ||
|
|
91e2fc92f3 | ||
|
|
a3f0652e7d | ||
|
|
7193c50bbe | ||
|
|
be0259f543 | ||
|
|
5fed732e7e | ||
|
|
33d7181432 | ||
|
|
f6fa9e3e25 | ||
|
|
dee9323f5c | ||
|
|
3513a680a6 | ||
|
|
b8833199e1 | ||
|
|
38aa76fae8 | ||
|
|
18c7b58a16 | ||
|
|
d7d38109a8 | ||
|
|
2317758df8 | ||
|
|
25c8253138 | ||
|
|
75f9da9f22 | ||
|
|
8c3ab1df78 | ||
|
|
e04c290587 | ||
|
|
527f8dd3e7 | ||
|
|
251db0108d | ||
|
|
5100a1bd8a | ||
|
|
ee5e2324a8 | ||
|
|
624bf7c965 | ||
|
|
3089503ab1 | ||
|
|
8cac1b1bb7 | ||
|
|
67fe5185fb | ||
|
|
9122b26d1a | ||
|
|
efcd2c9ebc | ||
|
|
181c7ea93f | ||
|
|
a8a0bc437a | ||
|
|
c6bc1c3450 | ||
|
|
1ad0f43d23 | ||
|
|
a3400b3b0f | ||
|
|
523667dfaf | ||
|
|
5599626405 | ||
|
|
d31d42cf3c | ||
|
|
11646d4877 | ||
|
|
e53c8ddd85 | ||
|
|
45cb96f2d4 | ||
|
|
ab0d58ed6c | ||
|
|
bdcddf5655 | ||
|
|
0f0219a4b1 | ||
|
|
aa2890a2ca | ||
|
|
43bddbba14 | ||
|
|
e4af27cd40 | ||
|
|
d390e67542 | ||
|
|
a8b7be6197 | ||
|
|
35da6ace36 | ||
|
|
8590ceb4ce | ||
|
|
96e637cfc0 | ||
|
|
f1470feb0a | ||
|
|
7070b19562 | ||
|
|
931a17462a | ||
|
|
859e636d1a | ||
|
|
7ccf9f5bef | ||
|
|
0f589004a1 | ||
|
|
2df4df0791 | ||
|
|
41296a9624 | ||
|
|
3dbfebe923 | ||
|
|
47271e7af5 | ||
|
|
5e0fc58f27 | ||
|
|
5eb8438782 | ||
|
|
4f0bf3c5aa | ||
|
|
b02e5140b9 | ||
|
|
abe79b30ff | ||
|
|
5d1b1fb4c0 | ||
|
|
9265ee6323 | ||
|
|
519256c50c | ||
|
|
17324e5340 | ||
|
|
7d01844371 | ||
|
|
745fa360c3 | ||
|
|
51fbae655e | ||
|
|
c3119aae2c | ||
|
|
bc1b787974 | ||
|
|
fb4b492f1c | ||
|
|
e3cc730cd7 | ||
|
|
46743c9bf8 | ||
|
|
d573e34501 | ||
|
|
fac69fbcf4 | ||
|
|
99f2ac586d | ||
|
|
33dc1f7095 | ||
|
|
0707527800 | ||
|
|
f71e117e3a | ||
|
|
d011aca301 | ||
|
|
2c3f3349b5 | ||
|
|
10eef7234f | ||
|
|
17154cefec | ||
|
|
e3d4d6e9d7 | ||
|
|
89918a304f | ||
|
|
74b795ada8 | ||
|
|
40a3b857da | ||
|
|
615927f3f2 | ||
|
|
3d1ad740dd | ||
|
|
4c76f654b7 | ||
|
|
bd835adcd4 | ||
|
|
78e4cfce68 | ||
|
|
deb5889a2c | ||
|
|
2030ad54bc | ||
|
|
a95604008d | ||
|
|
bb21288cff | ||
|
|
f31977489f | ||
|
|
f179587c2e | ||
|
|
fa3d9c9e1e | ||
|
|
459eb2b767 | ||
|
|
4e1c6afe02 | ||
|
|
dc69ccea64 | ||
|
|
03d9f8f2c6 | ||
|
|
5d006cf677 | ||
|
|
6c9657e453 | ||
|
|
e82ec50e7b | ||
|
|
09ccbb4c01 | ||
|
|
e3b2ab22a6 | ||
|
|
a2bc04e5d4 | ||
|
|
a4c81e55c0 | ||
|
|
2b571d9d83 | ||
|
|
6caaaf6f36 | ||
|
|
2b9550f3d9 | ||
|
|
94bb677fd8 | ||
|
|
5b6f33e1a4 | ||
|
|
32723a8876 | ||
|
|
a1fc068dd1 | ||
|
|
225fc772f6 | ||
|
|
2a167ba775 | ||
|
|
193717a1a9 | ||
|
|
4fc140ec2c | ||
|
|
2ffac96579 | ||
|
|
bc57adc12c | ||
|
|
dc64aa92fd | ||
|
|
8b5ff14ac9 | ||
|
|
6641c4696b | ||
|
|
a680d30930 | ||
|
|
1dce71e787 | ||
|
|
1451416e6b | ||
|
|
f4f20203f4 | ||
|
|
2df0c80b30 | ||
|
|
7aa036a18c | ||
|
|
2f9a2ec305 | ||
|
|
75ccdbad51 | ||
|
|
6414985461 | ||
|
|
230debb911 | ||
|
|
b42291d10b | ||
|
|
79616ba067 | ||
|
|
35c1a3e5e2 | ||
|
|
ac0e81dc00 | ||
|
|
f9ea1088e8 | ||
|
|
bb837f57e2 | ||
|
|
059d3f709b | ||
|
|
26ca5da725 | ||
|
|
ce8b0ff593 | ||
|
|
b74f19e6e6 | ||
|
|
3566251676 | ||
|
|
4f02dbd807 | ||
|
|
eed0aab4e5 | ||
|
|
498ab2f547 | ||
|
|
9c1c6c6e4c | ||
|
|
bd29738d52 | ||
|
|
2f08acae45 | ||
|
|
ff78f0bcb3 | ||
|
|
8ba5ecbec0 | ||
|
|
a8c3eb5831 | ||
|
|
d3ecb7b725 | ||
|
|
c2321f8fe9 | ||
|
|
018d451431 | ||
|
|
986d8360e2 | ||
|
|
087b73540c | ||
|
|
02a3221a09 | ||
|
|
16960b5941 | ||
|
|
11ffda10e6 | ||
|
|
a07b234745 | ||
|
|
276bcb0b28 | ||
|
|
4e1842134a | ||
|
|
7b9aed73f3 | ||
|
|
280f139ba2 | ||
|
|
def5cdc450 | ||
|
|
79fe377f19 | ||
|
|
b2dd3ed421 | ||
|
|
a76dcfa1f9 | ||
|
|
f2e4bc518e | ||
|
|
770b9db129 | ||
|
|
13c502d76d | ||
|
|
c2785f6429 | ||
|
|
0e8b6d7290 | ||
|
|
5c1b806f2d | ||
|
|
146c5d19ad | ||
|
|
25611283c4 | ||
|
|
ebc91adfb5 | ||
|
|
4a19260911 | ||
|
|
e9880c4d17 | ||
|
|
b2bf8605ae | ||
|
|
3f5e5fbc0e | ||
|
|
ad1d7e2751 | ||
|
|
98eee04712 | ||
|
|
593c3448eb | ||
|
|
4e8e0d09b0 | ||
|
|
88210fb5cc | ||
|
|
0913c1134d | ||
|
|
0d4808748d | ||
|
|
907059be00 | ||
|
|
7ffe6d650f | ||
|
|
179b850837 | ||
|
|
72617e8e6d | ||
|
|
149e50595b | ||
|
|
c8c33755de | ||
|
|
e9e07052f8 | ||
|
|
3252a33a87 | ||
|
|
00f0c009c7 | ||
|
|
0648950207 | ||
|
|
ab0ee830d8 | ||
|
|
8c94fef24c | ||
|
|
ec386be590 | ||
|
|
0adf321455 | ||
|
|
d4d6dede94 | ||
|
|
95abe8207d | ||
|
|
445874cb9c | ||
|
|
829435d5e6 | ||
|
|
6658158972 | ||
|
|
127aa38f58 | ||
|
|
770f381f6c | ||
|
|
90b71ce5e5 | ||
|
|
6db27c7fd9 | ||
|
|
dfdb491c7b | ||
|
|
212a0a5a59 | ||
|
|
442a949e97 | ||
|
|
bdcb54f8b1 | ||
|
|
eb989d7b2e | ||
|
|
f30eb26b6d | ||
|
|
468da6fcbf | ||
|
|
ae5068a411 | ||
|
|
e9b4b61c5f | ||
|
|
043cbec994 | ||
|
|
4e34d5ab55 | ||
|
|
baf7427453 | ||
|
|
b07c645124 | ||
|
|
69b7eef768 | ||
|
|
3feff9fea8 | ||
|
|
e23ac95c2c | ||
|
|
3c6bfe0607 | ||
|
|
3701a4fb44 | ||
|
|
e2c2bd3927 | ||
|
|
d7e338607a | ||
|
|
285c06df9c | ||
|
|
928fd2afab | ||
|
|
ab1537dab4 | ||
|
|
043e31a472 | ||
|
|
91f5c4910f | ||
|
|
41cc11c32e | ||
|
|
6216e7dfa7 | ||
|
|
8ff41a3986 | ||
|
|
f3d1a91533 | ||
|
|
a72a420bd3 | ||
|
|
6df151d244 | ||
|
|
14fa71d7f7 | ||
|
|
d73f912944 | ||
|
|
593d919d48 | ||
|
|
d452607c7a | ||
|
|
5987562bcb | ||
|
|
3fae69e035 | ||
|
|
6222c79bee | ||
|
|
e4ebd53e69 | ||
|
|
c855152e86 | ||
|
|
fb22e42b59 | ||
|
|
dce65e33a5 | ||
|
|
1259457ee4 | ||
|
|
4757bb73e4 | ||
|
|
6453345577 | ||
|
|
a931f0e7be | ||
|
|
5ec2c42e55 | ||
|
|
f664b37b4b | ||
|
|
7d1dfb67b7 | ||
|
|
6bbff14aa4 | ||
|
|
58499cec73 | ||
|
|
2890609053 | ||
|
|
416284afad | ||
|
|
73480840db | ||
|
|
227d747c72 | ||
|
|
6100e0f199 | ||
|
|
5b1670f520 | ||
|
|
f62f170226 | ||
|
|
7e38d9dd0c | ||
|
|
5cd13c3039 | ||
|
|
1b4b50b09a | ||
|
|
cd7b5d93ee | ||
|
|
1563f7892d | ||
|
|
5472f0a3e9 | ||
|
|
032cd5aa02 | ||
|
|
b0dbe9e323 | ||
|
|
8f23093617 | ||
|
|
afccf65157 | ||
|
|
c215104522 | ||
|
|
68389c8e6f | ||
|
|
d4201075d0 | ||
|
|
1f95ff0fb1 | ||
|
|
dd0c3d7468 | ||
|
|
0b9969c412 | ||
|
|
d8df13a12c | ||
|
|
f28b6d8e00 | ||
|
|
c23e4c4611 | ||
|
|
bc3e69d047 | ||
|
|
aaa9c355e9 | ||
|
|
c1dcbb0997 | ||
|
|
bc501cede2 | ||
|
|
c243fa9c23 | ||
|
|
765a5d3b95 | ||
|
|
351c0ba9b1 | ||
|
|
ac30e17222 | ||
|
|
bcdb2d5f3c | ||
|
|
6e5f378633 | ||
|
|
e70a99e52d | ||
|
|
c6230ffc66 | ||
|
|
edaf9badcf | ||
|
|
24d2e344e5 | ||
|
|
79a819989f | ||
|
|
d882d180a5 | ||
|
|
6be4e5e122 | ||
|
|
75dbed61fa | ||
|
|
c4122ec03f | ||
|
|
0f17157a3d | ||
|
|
5d2287b0b5 | ||
|
|
c9ec59dc8a | ||
|
|
3ab1b8254b | ||
|
|
bf5b05e63f | ||
|
|
0eacc4b718 | ||
|
|
e1100f975c | ||
|
|
da4b492e08 | ||
|
|
288aa721b6 | ||
|
|
324f3a6687 | ||
|
|
d9409f0f0c | ||
|
|
40dc8487f4 | ||
|
|
bbe72e3485 | ||
|
|
d6f2b0a48a | ||
|
|
e74255cf10 | ||
|
|
dd2841f349 | ||
|
|
3823dd0629 | ||
|
|
056da9ab94 | ||
|
|
c9808e0eee | ||
|
|
6bf9fac393 | ||
|
|
6d482fdd5e | ||
|
|
48c50692fc | ||
|
|
4e47cc875a | ||
|
|
415ecee8ba | ||
|
|
d1e6960cd4 | ||
|
|
f2ac39fe0e | ||
|
|
8e67cd413a | ||
|
|
4418231892 | ||
|
|
8eee001c12 | ||
|
|
a290fb0227 | ||
|
|
42ddb2387a | ||
|
|
1e51907e74 | ||
|
|
568a75480f | ||
|
|
460a0f1969 | ||
|
|
8d4f86dc3b | ||
|
|
56f9f02798 | ||
|
|
abf1b731bd | ||
|
|
5062518d16 | ||
|
|
df7e77c865 | ||
|
|
f0a18d3fd3 | ||
|
|
8dade6d387 | ||
|
|
73eb44c0f6 | ||
|
|
b190d29d46 | ||
|
|
f858590358 | ||
|
|
0795c511de | ||
|
|
de678d8663 | ||
|
|
5c60052775 | ||
|
|
a0edb49c0a | ||
|
|
1282f659f9 | ||
|
|
f7b91a79e6 | ||
|
|
426546c36e | ||
|
|
404d9ed65f | ||
|
|
753aebdba7 | ||
|
|
2659490a70 | ||
|
|
cff0b17881 | ||
|
|
9d4218531b | ||
|
|
b56020be3e | ||
|
|
b5c5e28b59 | ||
|
|
8a2bcb3f53 | ||
|
|
e5ae5b7035 | ||
|
|
413df6ed65 | ||
|
|
0ec46056f0 | ||
|
|
9dcb1f8126 | ||
|
|
4e9c8aea01 | ||
|
|
7d748ce59d | ||
|
|
198cf00498 | ||
|
|
145592b2e1 | ||
|
|
6fe8f7b2ec | ||
|
|
3cbfa7cfc3 | ||
|
|
30a35d7651 | ||
|
|
c46069759a | ||
|
|
68a4284e8c | ||
|
|
c11624ab06 | ||
|
|
91c81131d8 | ||
|
|
ee44e225f5 | ||
|
|
0a489ea2ea | ||
|
|
1e147e4397 | ||
|
|
c11b4a34ef | ||
|
|
e5a517a325 | ||
|
|
7bbe5f0589 | ||
|
|
8f949f532f | ||
|
|
f71a481d0e | ||
|
|
fdd4b61d5d | ||
|
|
b34e1c69f0 | ||
|
|
64562a8a05 | ||
|
|
0d88c7755b | ||
|
|
7704bafb00 | ||
|
|
ab31d36251 | ||
|
|
7362bd999c | ||
|
|
b4a0879584 | ||
|
|
8274a8c6c7 | ||
|
|
cdffd567b6 | ||
|
|
c7003b4765 | ||
|
|
4e3f2160ae | ||
|
|
095fea939d | ||
|
|
26e4bc2ad2 | ||
|
|
18241a5cbd | ||
|
|
d8cd3e38c7 | ||
|
|
0bbb11012d | ||
|
|
212c998980 | ||
|
|
e359efd97e | ||
|
|
547640bfcc | ||
|
|
d7a2264c0d | ||
|
|
0bb91add74 | ||
|
|
cea2dd9ab9 | ||
|
|
c424f25da1 | ||
|
|
7c1931749c | ||
|
|
4708ccc7dc | ||
|
|
9e0a239ec8 | ||
|
|
8a2c001531 | ||
|
|
044b958e08 | ||
|
|
a2bbed65d5 | ||
|
|
cf62045e26 | ||
|
|
164ddbe4f4 | ||
|
|
6b5eb032ad | ||
|
|
8d3a85860e | ||
|
|
cbb265e9f0 | ||
|
|
948fb2f747 | ||
|
|
b0ca6dfcaa | ||
|
|
61eb31ea66 | ||
|
|
eed4c36b7b | ||
|
|
b5a3a53780 | ||
|
|
a6302ecc14 | ||
|
|
2a9d74adb4 | ||
|
|
e5772a5902 | ||
|
|
abdeb8dbdb | ||
|
|
8611c9ff1a | ||
|
|
f7244e6c89 | ||
|
|
4b7f72c00c | ||
|
|
1476c88a54 | ||
|
|
c061f2e2f0 | ||
|
|
dc49c0ebe4 | ||
|
|
11d4273982 | ||
|
|
7755f76215 | ||
|
|
6e44bfea95 | ||
|
|
fa8ceacbcf | ||
|
|
d19c468336 | ||
|
|
86edd829c0 | ||
|
|
bdc9c1e7de | ||
|
|
6e007b1e4a | ||
|
|
356c1d9811 | ||
|
|
723a2bf117 | ||
|
|
7b7e5e6e72 | ||
|
|
3fcbf74c71 | ||
|
|
8f4a6ce255 | ||
|
|
9e0853aa9a | ||
|
|
c4480b9ade | ||
|
|
1a588880bf | ||
|
|
e2c2392a70 | ||
|
|
5160b5ae22 | ||
|
|
8be847b68a | ||
|
|
e7ed24f1ef | ||
|
|
13e324b52d | ||
|
|
5c7b221799 | ||
|
|
7398a30223 | ||
|
|
637b9fd225 | ||
|
|
88442708c2 | ||
|
|
d7759317c4 | ||
|
|
fd3045eb9d | ||
|
|
09ae22f329 | ||
|
|
365e3b3bce | ||
|
|
a6fe8585fb | ||
|
|
930b2ec05c | ||
|
|
c9d1535967 | ||
|
|
550849c93f | ||
|
|
8128902cd6 | ||
|
|
b350049ca7 | ||
|
|
e0e07929e8 | ||
|
|
128b196220 | ||
|
|
5b806dff27 | ||
|
|
9c5d358c3e | ||
|
|
ab89ee1a4f | ||
|
|
dc82e5f653 | ||
|
|
7fb3a81a66 | ||
|
|
d958b14b8d | ||
|
|
0a6c75eb93 | ||
|
|
7c5a4fc682 | ||
|
|
ca8caa8504 | ||
|
|
339dfe0b08 | ||
|
|
b9cefd325b | ||
|
|
fa974c68bd | ||
|
|
3c47e0e040 | ||
|
|
34a9a97201 | ||
|
|
f6847e3aea | ||
|
|
e27021cbf4 | ||
|
|
3dc6413ea4 | ||
|
|
53c04a1ad9 | ||
|
|
b2486380fb | ||
|
|
9f41ddaaa2 | ||
|
|
ef4d8c6e8d | ||
|
|
a6245b87f6 | ||
|
|
fa4541e85c | ||
|
|
376f5626a4 | ||
|
|
e17dc90983 | ||
|
|
a63385343a | ||
|
|
3c3206d8e2 | ||
|
|
f6c8feae6e | ||
|
|
98a007a17e | ||
|
|
3e0d5b9ede | ||
|
|
555082bb1d | ||
|
|
5c95c352c3 | ||
|
|
3b4758ca5c | ||
|
|
38455a7108 | ||
|
|
ebe310bc65 | ||
|
|
5bc3198263 | ||
|
|
5d6669db6d | ||
|
|
519ffe3b91 | ||
|
|
717c773800 | ||
|
|
935a4520da | ||
|
|
7685dad40a | ||
|
|
699671bde5 | ||
|
|
639fc6a994 | ||
|
|
7facbf35b2 | ||
|
|
b54fe54b4a | ||
|
|
796339dcfe | ||
|
|
1721a322c2 | ||
|
|
3125097b83 | ||
|
|
aa0c40fca1 | ||
|
|
2d61027bbe | ||
|
|
2dac3cae81 | ||
|
|
43d3df2ffd | ||
|
|
44ffaeee2c | ||
|
|
0bf4e2c781 | ||
|
|
d342b82219 | ||
|
|
227581ebcf | ||
|
|
eb74d6c033 | ||
|
|
a2035a68e4 | ||
|
|
1b055af545 | ||
|
|
5b79947178 | ||
|
|
9e329b999e | ||
|
|
e5e0c3dd28 | ||
|
|
e810422429 | ||
|
|
8b2c590309 | ||
|
|
d92ef0c807 | ||
|
|
faa14b7add | ||
|
|
abbc8ddad0 | ||
|
|
551e03b256 | ||
|
|
f65f80160f | ||
|
|
5438c14bdb | ||
|
|
8f3d84ec60 | ||
|
|
c3df930bea | ||
|
|
4513a45306 | ||
|
|
a94a83dc4f | ||
|
|
e1e5c8352c | ||
|
|
98ceb00bb4 | ||
|
|
77974839b0 | ||
|
|
706341011a | ||
|
|
4a26810956 | ||
|
|
92fd318aa8 | ||
|
|
bc2555ddc4 | ||
|
|
8ee9794304 | ||
|
|
30388ffa4f | ||
|
|
fdea0a1620 | ||
|
|
41e2909782 | ||
|
|
4048ed8c84 | ||
|
|
7a21b17d0c | ||
|
|
72bc6f3005 | ||
|
|
a8a7e0c92f | ||
|
|
a11fcfe373 | ||
|
|
52a3600819 | ||
|
|
bec6223e6e | ||
|
|
333b91ccfd | ||
|
|
a3266fe641 | ||
|
|
10449a21b5 | ||
|
|
29f5d2a872 | ||
|
|
f8b3e4961c | ||
|
|
cca701e954 | ||
|
|
89c0807cba | ||
|
|
544a4c745d | ||
|
|
f6e9a7dd23 | ||
|
|
69132b6f43 | ||
|
|
aa88e2c6a6 | ||
|
|
b89734b0e9 | ||
|
|
d5eb058d00 | ||
|
|
fe308b6b58 | ||
|
|
ab14942c80 | ||
|
|
908cb2e5c6 | ||
|
|
6935aa0820 | ||
|
|
4ec8ac16a8 | ||
|
|
bfe2877d0b | ||
|
|
9d50e9a165 | ||
|
|
eb496cfe75 | ||
|
|
dfd01da913 | ||
|
|
c8bdb6ce09 | ||
|
|
3169c03e66 | ||
|
|
26323cccf2 | ||
|
|
2bb9a61539 | ||
|
|
4efd3fcbad | ||
|
|
170705440a | ||
|
|
a52fdaa032 | ||
|
|
6d972da227 | ||
|
|
96bd677ef9 | ||
|
|
b77bc7a8b4 | ||
|
|
7f06f55f83 | ||
|
|
04381439c2 | ||
|
|
7b658c60c7 | ||
|
|
0c68103eb6 | ||
|
|
65c787d0da | ||
|
|
0b40daf936 | ||
|
|
75607ebbe3 | ||
|
|
58b178f679 | ||
|
|
ffeebb03bd | ||
|
|
7c1f0d2739 | ||
|
|
ac0c6a4305 | ||
|
|
ff776dc90c | ||
|
|
516baedad9 | ||
|
|
d83ec5a0b5 | ||
|
|
1c8ba1d1eb | ||
|
|
16024769c8 | ||
|
|
ed9316266e | ||
|
|
103f55bd6e | ||
|
|
009d9db74f | ||
|
|
416bc85455 | ||
|
|
88a5ab8052 | ||
|
|
31454f7ca0 | ||
|
|
bfd0712c83 | ||
|
|
3c9d482a02 | ||
|
|
5c3694ec19 | ||
|
|
997abfeb27 | ||
|
|
df77245f0d | ||
|
|
5e2ea2ee64 | ||
|
|
655af4ed31 | ||
|
|
9d385faa84 | ||
|
|
455808fe26 | ||
|
|
c2dbdc6279 | ||
|
|
8faf8af5da | ||
|
|
cfaf1ee049 | ||
|
|
29d9bb0d41 | ||
|
|
9d185bd6c8 | ||
|
|
7c26245c53 | ||
|
|
4dc3f73e74 | ||
|
|
1a66060aa2 | ||
|
|
cf9e165ccf | ||
|
|
e877486daa | ||
|
|
1eb095e235 | ||
|
|
905e938c57 | ||
|
|
fe8b914f8d | ||
|
|
a48cc065c0 | ||
|
|
6e50909e0f | ||
|
|
6480b262de | ||
|
|
a0745e4f40 | ||
|
|
8eb9e37d67 | ||
|
|
69506d9c1d | ||
|
|
d37f1fcae1 | ||
|
|
dfb7b55145 | ||
|
|
6e335085d1 | ||
|
|
cde9660b12 | ||
|
|
abe782cdba | ||
|
|
96f3377733 | ||
|
|
e43011233f | ||
|
|
7c4d92ac08 | ||
|
|
3033717d67 | ||
|
|
5a2bec60d9 | ||
|
|
7e2f9b8361 | ||
|
|
5c1d3e38bd | ||
|
|
f8243eae9e | ||
|
|
14ead16a8a | ||
|
|
c064eaed4e | ||
|
|
5758626777 | ||
|
|
934396d87a | ||
|
|
130c5de4b0 | ||
|
|
09bcfba800 | ||
|
|
cb928b8490 | ||
|
|
4aba90c8fb | ||
|
|
308568a332 | ||
|
|
c87560aabc | ||
|
|
13f72153a3 | ||
|
|
b435ac3101 | ||
|
|
0d7a05d37a | ||
|
|
0fbbf55b51 | ||
|
|
a9c624b746 | ||
|
|
0caee112e0 | ||
|
|
17e28b2a24 | ||
|
|
7fcbae60bc | ||
|
|
9aabea4065 | ||
|
|
8a1b104f28 | ||
|
|
8f35eacf9f | ||
|
|
327983fc8c | ||
|
|
57ede34c29 | ||
|
|
d76eacd7e5 | ||
|
|
32f9ae614c | ||
|
|
490c56e46e | ||
|
|
c5290a2640 | ||
|
|
00f07c3801 | ||
|
|
a441cbbe60 | ||
|
|
8656148a91 | ||
|
|
b32e0ca6d7 | ||
|
|
5b4daec4ab | ||
|
|
aed66d49cf | ||
|
|
5d5331b0ad | ||
|
|
83b48a683a | ||
|
|
93477a19d6 | ||
|
|
a87b08d791 | ||
|
|
494269bcf5 | ||
|
|
c684774f06 | ||
|
|
9296438911 | ||
|
|
e9a9767f67 | ||
|
|
174bd295e1 | ||
|
|
57a9e0b8ca | ||
|
|
e89a8eab11 | ||
|
|
98274c8b8e | ||
|
|
c8617289bc | ||
|
|
e82e370e83 | ||
|
|
e2adf9bf14 | ||
|
|
881e17dc67 | ||
|
|
021063470d | ||
|
|
2b5196106a | ||
|
|
628bc2cd26 | ||
|
|
6d5a6d407b | ||
|
|
ce6df3633a | ||
|
|
fecc6f4f6f | ||
|
|
839deda612 | ||
|
|
dac1ece8fd | ||
|
|
fc8c242173 | ||
|
|
866af2f766 | ||
|
|
4fab686aea | ||
|
|
bfae437886 | ||
|
|
c9a989bfab | ||
|
|
9bd2ca246b | ||
|
|
f2b9732748 | ||
|
|
fda637edba | ||
|
|
a8ea46a357 | ||
|
|
37826cb901 | ||
|
|
6fec13c941 | ||
|
|
656e8c2c77 | ||
|
|
3f73c6df7b | ||
|
|
f5d1dd7e03 | ||
|
|
28cd140d9f | ||
|
|
5f6b89ec2a | ||
|
|
1fd9a1f689 | ||
|
|
f773ee2dab | ||
|
|
a7487fb26d | ||
|
|
ce5aa9fb7a | ||
|
|
790fc13157 | ||
|
|
face0d20d4 | ||
|
|
5c16407818 | ||
|
|
d8201fd4ad | ||
|
|
2b1cd4951d | ||
|
|
849fe8fc0c | ||
|
|
f600e6095e | ||
|
|
d952f774dd | ||
|
|
38ee27d896 | ||
|
|
73e1bc352b | ||
|
|
46ce75bf2a | ||
|
|
2c285fee8a | ||
|
|
d1c3cf8bb0 | ||
|
|
a09c7ddb53 | ||
|
|
fec81c93a3 | ||
|
|
2b3c70dfc3 | ||
|
|
db9236bef2 | ||
|
|
65642edc15 | ||
|
|
86b6e12453 |
No files matched your search
@@ -0,0 +1,3 @@
|
||||
last 3 versions
|
||||
Safari >= 8
|
||||
iOS >= 8
|
||||
@@ -0,0 +1,207 @@
|
||||
version: 2.1
|
||||
aliases:
|
||||
- &save_git_cache
|
||||
save_cache:
|
||||
paths:
|
||||
- .git
|
||||
key: v1-git-{{ .Revision }}
|
||||
- &restore_git_cache
|
||||
restore_cache:
|
||||
keys:
|
||||
- v1-git-{{ .Revision }}
|
||||
- v1-git-
|
||||
- &save_build_cache
|
||||
save_cache:
|
||||
paths:
|
||||
- build
|
||||
key: v1-build-{{ .Revision }}
|
||||
- &restore_build_cache
|
||||
restore_cache:
|
||||
keys:
|
||||
- v1-build-{{ .Revision }}
|
||||
- &save_dist_cache
|
||||
save_cache:
|
||||
paths:
|
||||
- dist
|
||||
key: v1-dist-{{ .Revision }}
|
||||
- &restore_dist_cache
|
||||
restore_cache:
|
||||
keys:
|
||||
- v1-dist-{{ .Revision }}
|
||||
- &save_npm_cache
|
||||
save_cache:
|
||||
paths:
|
||||
- node_modules
|
||||
key: v1-npm-{{ checksum "package-lock.json" }}
|
||||
- &restore_npm_cache
|
||||
restore_cache:
|
||||
keys:
|
||||
- v1-npm-{{ checksum "package-lock.json" }}
|
||||
- v1-npm-
|
||||
- &defaults
|
||||
docker:
|
||||
- image: circleci/node:8.16-browsers
|
||||
working_directory: ~/repo
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- *restore_git_cache
|
||||
- checkout
|
||||
- *restore_npm_cache
|
||||
- run: npm install
|
||||
- *save_git_cache
|
||||
- *save_npm_cache
|
||||
lint:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- *restore_git_cache
|
||||
- checkout
|
||||
- *restore_npm_cache
|
||||
- run:
|
||||
name: Lint
|
||||
command: npm run test:lint -- --quiet --output-file test-results/eslint/results.xml --format junit
|
||||
- store_test_results:
|
||||
path: test-results
|
||||
unit:
|
||||
<<: *defaults
|
||||
environment:
|
||||
JEST_JUNIT_OUTPUT_NAME: results.xml
|
||||
steps:
|
||||
- *restore_git_cache
|
||||
- checkout
|
||||
- *restore_npm_cache
|
||||
- run:
|
||||
name: Unit
|
||||
environment:
|
||||
JEST_JUNIT_OUTPUT_DIR: test-results/unit
|
||||
command: npm run test:unit -- --reporters="default" --reporters="jest-junit" --coverage --coverageReporters=text --coverageReporters=lcov
|
||||
- store_artifacts:
|
||||
path: coverage
|
||||
- store_test_results:
|
||||
path: test-results
|
||||
build:
|
||||
<<: *defaults
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
NODE_OPTIONS: --max-old-space-size=4000
|
||||
steps:
|
||||
- *restore_git_cache
|
||||
- checkout
|
||||
- *restore_npm_cache
|
||||
- run:
|
||||
name: Build
|
||||
command: npm run build
|
||||
- *save_build_cache
|
||||
- *save_dist_cache
|
||||
store_build:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- *restore_build_cache
|
||||
- store_artifacts:
|
||||
path: build
|
||||
store_dist:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- *restore_dist_cache
|
||||
- store_artifacts:
|
||||
path: dist
|
||||
integration:
|
||||
<<: *defaults
|
||||
parallelism: 2
|
||||
environment:
|
||||
JEST_JUNIT_OUTPUT_NAME: results.txt
|
||||
steps:
|
||||
- *restore_git_cache
|
||||
- checkout
|
||||
- *restore_npm_cache
|
||||
- *restore_build_cache
|
||||
- run:
|
||||
name: Integration
|
||||
environment:
|
||||
JEST_JUNIT_OUTPUT_DIR: test-results/integration
|
||||
command: |
|
||||
export TESTFILES=$(circleci tests glob "test/integration/*.test.js" | circleci tests split --split-by=timings)
|
||||
$(npm bin)/jest ${TESTFILES} --reporters="default" --reporters="jest-junit"
|
||||
- store_test_results:
|
||||
path: test-results
|
||||
|
||||
deploy-npm:
|
||||
<<: *defaults
|
||||
environment:
|
||||
NODE_OPTIONS: --max-old-space-size=4000
|
||||
steps:
|
||||
- *restore_git_cache
|
||||
- *restore_dist_cache
|
||||
- checkout
|
||||
- run: |
|
||||
echo export RELEASE_VERSION="0.1.0-prerelease.$(date +'%Y%m%d%H%M%S')" >> $BASH_ENV
|
||||
echo export NPM_TAG=circlelatest >> $BASH_ENV
|
||||
if [ "$CIRCLE_BRANCH" == "master" ]
|
||||
then echo export NPM_TAG=circlestable >> $BASH_ENV
|
||||
fi
|
||||
if [[ "$CIRCLE_BRANCH" == hotfix/* ]] # double brackets are important for matching the wildcard
|
||||
then echo export NPM_TAG=circlehotfix >> $BASH_ENV
|
||||
fi
|
||||
- run: npm version --no-git-tag-version $RELEASE_VERSION
|
||||
- run: |
|
||||
npm set //registry.npmjs.org/:_authToken=$NPM_TOKEN
|
||||
npm publish --tag $NPM_TAG
|
||||
- run: git tag $RELEASE_VERSION
|
||||
- run: git push $CIRCLE_REPOSITORY_URL $RELEASE_VERSION
|
||||
|
||||
deploy-gh-pages:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- *restore_git_cache
|
||||
- checkout
|
||||
- *restore_npm_cache
|
||||
- *restore_build_cache
|
||||
- run: |
|
||||
git config --global user.email $(git log --pretty=format:"%ae" -n1)
|
||||
git config --global user.name $(git log --pretty=format:"%an" -n1)
|
||||
- run: npm run deploy -- -e $CIRCLE_BRANCH
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build-test-deploy:
|
||||
jobs:
|
||||
- setup
|
||||
- lint:
|
||||
requires:
|
||||
- setup
|
||||
- unit:
|
||||
requires:
|
||||
- setup
|
||||
- build:
|
||||
requires:
|
||||
- setup
|
||||
- integration:
|
||||
requires:
|
||||
- build
|
||||
- store_build:
|
||||
requires:
|
||||
- build
|
||||
- store_dist:
|
||||
requires:
|
||||
- build
|
||||
# Disable deployment while running in parallel with Travis
|
||||
# - deploy-npm:
|
||||
# requires:
|
||||
# - lint
|
||||
# - unit
|
||||
# - integration
|
||||
# - build
|
||||
# filters:
|
||||
# branches:
|
||||
# only:
|
||||
# - master
|
||||
# - develop
|
||||
# - /^hotfix\/.*/
|
||||
# - deploy-gh-pages:
|
||||
# requires:
|
||||
# - lint
|
||||
# - unit
|
||||
# - integration
|
||||
# - build
|
||||
@@ -4,7 +4,6 @@
|
||||
# NPM
|
||||
/node_modules
|
||||
npm-*
|
||||
/package-lock.json
|
||||
|
||||
# Testing
|
||||
/.nyc_output
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
/node_modules
|
||||
npm-*
|
||||
|
||||
# Double copies of all the static assets and tutorial gifs
|
||||
/src
|
||||
|
||||
# Testing
|
||||
/.nyc_output
|
||||
/coverage
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
language: node_js
|
||||
sudo: required
|
||||
dist: trusty
|
||||
dist: xenial
|
||||
addons:
|
||||
chrome: stable
|
||||
node_js:
|
||||
- 8
|
||||
env:
|
||||
global:
|
||||
- CHROMEDRIVER_VERSION=LATEST
|
||||
- NODE_ENV=production
|
||||
- SMOKE_URL=https://llk.github.io/scratch-gui/$TRAVIS_PULL_REQUEST_BRANCH
|
||||
- NODE_OPTIONS=--max-old-space-size=7250
|
||||
- NPM_TAG=latest
|
||||
- RELEASE_VERSION="0.1.0-prerelease.$(date +'%Y%m%d%H%M%S')"
|
||||
cache:
|
||||
@@ -16,15 +17,14 @@ cache:
|
||||
- node_modules
|
||||
install:
|
||||
- npm --production=false install
|
||||
- npm --production=false update
|
||||
script:
|
||||
- npm test
|
||||
- if [ "$TRAVIS_EVENT_TYPE" == "pull_request" ] && [ "$TRAVIS_BRANCH" == "master" ]; then npm run test:smoke; fi
|
||||
before_deploy:
|
||||
- >
|
||||
if [ -z "$BEFORE_DEPLOY_RAN" ]; then
|
||||
npm --no-git-tag-version version $RELEASE_VERSION
|
||||
if [ "$TRAVIS_BRANCH" == "master" ]; then export NPM_TAG=stable; fi
|
||||
if [[ "$TRAVIS_BRANCH" == hotfix/* ]]; then export NPM_TAG=hotfix; fi # double brackets are important for matching the wildcard
|
||||
git config --global user.email $(git log --pretty=format:"%ae" -n1)
|
||||
git config --global user.name $(git log --pretty=format:"%an" -n1)
|
||||
export BEFORE_DEPLOY_RAN=true
|
||||
@@ -35,7 +35,7 @@ deploy:
|
||||
branch:
|
||||
- master
|
||||
- develop
|
||||
- smoke
|
||||
- hotfix/*
|
||||
condition: $TRAVIS_EVENT_TYPE != cron
|
||||
skip_cleanup: true
|
||||
email: $NPM_EMAIL
|
||||
@@ -47,25 +47,15 @@ deploy:
|
||||
- master
|
||||
- develop
|
||||
- smoke
|
||||
- hotfix/*
|
||||
condition: $TRAVIS_EVENT_TYPE != cron
|
||||
skip_cleanup: true
|
||||
script: if npm info scratch-gui | grep -q $RELEASE_VERSION; then git tag $RELEASE_VERSION && git push https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git $RELEASE_VERSION; fi
|
||||
- provider: s3
|
||||
on:
|
||||
branch:
|
||||
- master
|
||||
- $PREVIEW_BRANCH
|
||||
condition: $TRAVIS_EVENT_TYPE != cron
|
||||
access_key_id: $AWS_ACCESS_KEY_ID
|
||||
secret_access_key: $AWS_SECRET_ACCESS_KEY
|
||||
bucket: $AWS_BUCKET_NAME
|
||||
acl: public_read
|
||||
skip_cleanup: true
|
||||
local_dir: build
|
||||
- provider: script
|
||||
on:
|
||||
all_branches: true
|
||||
condition: $TRAVIS_EVENT_TYPE != cron
|
||||
condition: $TRAVIS_EVENT_TYPE != cron && ! $TRAVIS_BRANCH =~ ^greenkeeper/
|
||||
tags: false # Don't push tags to gh-pages
|
||||
skip_cleanup: true
|
||||
script: npm run deploy -- -x -e $TRAVIS_BRANCH -r https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git
|
||||
- provider: script
|
||||
@@ -74,5 +64,3 @@ deploy:
|
||||
condition: $TRAVIS_EVENT_TYPE == cron
|
||||
skip_cleanup: true
|
||||
script: npm run i18n:src && npm run i18n:push
|
||||
after_deploy:
|
||||
- 'curl -X POST -H "Fastly-Key: $FASTLY_TOKEN" -H "Accept: application/json" https://api.fastly.com/service/$FASTLY_SERVICE_ID/purge_all'
|
||||
@@ -30,29 +30,38 @@ Then go to [http://localhost:8601/](http://localhost:8601/) - the playground out
|
||||
|
||||
## Developing alongside other Scratch repositories
|
||||
|
||||
### Linking this code to another project's `node_modules/scratch-gui`
|
||||
### Getting another repo to point to this code
|
||||
|
||||
|
||||
If you wish to develop `scratch-gui` alongside other scratch repositories that depend on it, you may wish
|
||||
to have the other repositories use your local `scratch-gui` build instead of fetching the current production
|
||||
version of the scratch-gui that is found by default using `npm install`.
|
||||
|
||||
Here's how to link your local `scratch-gui` code to another project's `node_modules/scratch-gui`.
|
||||
|
||||
#### Configuration
|
||||
|
||||
If you wish to develop scratch-gui alongside other scratch repositories that depend on it, you may wish
|
||||
to have the other repositories use your local scratch-gui build instead of fetching the current production
|
||||
version of the scratch-gui that is found by default using `npm install`.
|
||||
1. In your local `scratch-gui` repository's top level:
|
||||
1. Make sure you have run `npm install`
|
||||
2. Build the `dist` directory by running `BUILD_MODE=dist npm run build`
|
||||
3. Establish a link to this repository by running `npm link`
|
||||
|
||||
To do this:
|
||||
1. Make sure you have run `npm install` from this (scratch-gui) repository's top level
|
||||
2. Make sure you have run `npm install` from the top level of each repository (such as scratch-www) that depends on scratch-gui
|
||||
3. From this (scratch-gui) repository's top level, build the `dist` directory by running `BUILD_MODE=dist npm run build`
|
||||
4. From this (scratch-gui) repository's top level, establish a link to this repository by running `npm link`
|
||||
5. From the top level of each repository that depends on scratch-gui, run `npm link scratch-gui`
|
||||
6. Build or run the repositories that depend on scratch-gui
|
||||
2. From the top level of each repository (such as `scratch-www`) that depends on `scratch-gui`:
|
||||
1. Make sure you have run `npm install`
|
||||
2. Run `npm link scratch-gui`
|
||||
3. Build or run the repositoriy
|
||||
|
||||
Instead of `BUILD_MODE=dist npm run build` you can also use `BUILD_MODE=dist npm run watch`, however this may be unreliable.
|
||||
#### Using `npm run watch`
|
||||
|
||||
Instead of `BUILD_MODE=dist npm run build`, you can use `BUILD_MODE=dist npm run watch` instead. This will watch for changes to your `scratch-gui` code, and automatically rebuild when there are changes. Sometimes this has been unreliable; if you are having problems, try going back to `BUILD_MODE=dist npm run build` until you resolve them.
|
||||
|
||||
#### Oh no! It didn't work!
|
||||
* Follow the recipe above step by step and don't change the order. It is especially important to run npm first because installing after the linking will reset the linking.
|
||||
* Make sure the repositories are siblings on your machine's file tree.
|
||||
* If you have multiple Terminal tabs or windows open for the different Scratch repositories, make sure to use the same node version in all of them.
|
||||
* In the worst case unlink the repositories by running `npm unlink` in both, and start over.
|
||||
|
||||
If you can't get linking to work right, try:
|
||||
* Follow the recipe above step by step and don't change the order. It is especially important to run `npm install` _before_ `npm link`, because installing after the linking will reset the linking.
|
||||
* Make sure the repositories are siblings on your machine's file tree, like `.../.../MY_SCRATCH_DEV_DIRECTORY/scratch-gui/` and `.../.../MY_SCRATCH_DEV_DIRECTORY/scratch-www/`.
|
||||
* Consistent node.js version: If you have multiple Terminal tabs or windows open for the different Scratch repositories, make sure to use the same node version in all of them.
|
||||
* If nothing else works, unlink the repositories by running `npm unlink` in both, and start over.
|
||||
|
||||
## Testing
|
||||
### Documentation
|
||||
@@ -121,6 +130,66 @@ If you want to watch the browser as it runs the test, rather than running headle
|
||||
USE_HEADLESS=no $(npm bin)/jest --runInBand test/integration/backpack.test.js
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Ignoring optional dependencies
|
||||
|
||||
When running `npm install`, you can get warnings about optionsl dependencies:
|
||||
|
||||
```
|
||||
npm WARN optional Skipping failed optional dependency /chokidar/fsevents:
|
||||
npm WARN notsup Not compatible with your operating system or architecture: fsevents@1.2.7
|
||||
```
|
||||
|
||||
You can suppress them by adding the `no-optional` switch:
|
||||
|
||||
```
|
||||
npm install --no-optional
|
||||
```
|
||||
|
||||
Further reading: [Stack Overflow](https://stackoverflow.com/questions/36725181/not-compatible-with-your-operating-system-or-architecture-fsevents1-0-11)
|
||||
|
||||
### Resolving dependencies
|
||||
|
||||
When installing for the first time, you can get warnings which need to be resolved:
|
||||
|
||||
```
|
||||
npm WARN eslint-config-scratch@5.0.0 requires a peer of babel-eslint@^8.0.1 but none was installed.
|
||||
npm WARN eslint-config-scratch@5.0.0 requires a peer of eslint@^4.0 but none was installed.
|
||||
npm WARN scratch-paint@0.2.0-prerelease.20190318170811 requires a peer of react-intl-redux@^0.7 but none was installed.
|
||||
npm WARN scratch-paint@0.2.0-prerelease.20190318170811 requires a peer of react-responsive@^4 but none was installed.
|
||||
```
|
||||
|
||||
You can check which versions are available:
|
||||
|
||||
```
|
||||
npm view react-intl-redux@0.* version
|
||||
```
|
||||
|
||||
You will neet do install the required version:
|
||||
|
||||
```
|
||||
npm install --no-optional --save-dev react-intl-redux@^0.7
|
||||
```
|
||||
|
||||
The dependency itself might have more missing dependencies, which will show up like this:
|
||||
|
||||
```
|
||||
user@machine:~/sources/scratch/scratch-gui (491-translatable-library-objects)$ npm install --no-optional --save-dev react-intl-redux@^0.7
|
||||
scratch-gui@0.1.0 /media/cuideigin/Linux/sources/scratch/scratch-gui
|
||||
├── react-intl-redux@0.7.0
|
||||
└── UNMET PEER DEPENDENCY react-responsive@5.0.0
|
||||
```
|
||||
|
||||
You will need to install those as well:
|
||||
|
||||
```
|
||||
npm install --no-optional --save-dev react-responsive@^5.0.0
|
||||
```
|
||||
|
||||
Further reading: [Stack Overflow](https://stackoverflow.com/questions/46602286/npm-requires-a-peer-of-but-all-peers-are-in-package-json-and-node-modules)
|
||||
|
||||
|
||||
## Publishing to GitHub Pages
|
||||
You can publish the GUI to github.io so that others on the Internet can view it.
|
||||
[Read the wiki for a step-by-step guide.](https://github.com/LLK/scratch-gui/wiki/Publishing-to-GitHub-Pages)
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
"babel-loader": "^8.0.4",
|
||||
"base64-loader": "1.0.0",
|
||||
"bowser": "1.9.4",
|
||||
"chromedriver": "2.44.1",
|
||||
"chromedriver": "78.0.1",
|
||||
"classnames": "2.2.6",
|
||||
"computed-style-to-inline-style": "3.0.0",
|
||||
"copy-webpack-plugin": "^4.5.1",
|
||||
@@ -55,8 +55,10 @@
|
||||
"es6-object-assign": "1.1.0",
|
||||
"eslint": "^5.0.1",
|
||||
"eslint-config-scratch": "^5.0.0",
|
||||
"eslint-plugin-import": "^2.8.0",
|
||||
"eslint-plugin-react": "7.11.1",
|
||||
"eslint-import-resolver-webpack": "^0.11.1",
|
||||
"eslint-plugin-import": "^2.18.2",
|
||||
"eslint-plugin-jest": "^22.14.1",
|
||||
"eslint-plugin-react": "^7.12.4",
|
||||
"file-loader": "2.0.0",
|
||||
"get-float-time-domain-data": "0.1.0",
|
||||
"get-user-media-promise": "1.1.4",
|
||||
@@ -65,6 +67,7 @@
|
||||
"immutable": "3.8.2",
|
||||
"intl": "1.2.5",
|
||||
"jest": "^21.0.0",
|
||||
"jest-junit": "^7.0.0",
|
||||
"js-base64": "2.4.9",
|
||||
"keymirror": "0.1.1",
|
||||
"lodash.bindall": "4.4.0",
|
||||
@@ -90,8 +93,8 @@
|
||||
"react-dom": "16.2.0",
|
||||
"react-draggable": "3.0.5",
|
||||
"react-ga": "2.5.3",
|
||||
"react-intl": "2.4.0",
|
||||
"react-modal": "3.6.1",
|
||||
"react-intl": "2.9.0",
|
||||
"react-modal": "3.9.1",
|
||||
"react-popover": "0.5.10",
|
||||
"react-redux": "5.0.7",
|
||||
"react-responsive": "5.0.0",
|
||||
@@ -104,14 +107,14 @@
|
||||
"redux-mock-store": "^1.2.3",
|
||||
"redux-throttle": "0.1.1",
|
||||
"rimraf": "^2.6.1",
|
||||
"scratch-audio": "0.1.0-prerelease.20190114210212",
|
||||
"scratch-blocks": "0.1.0-prerelease.1552662801",
|
||||
"scratch-l10n": "3.1.20190220143732",
|
||||
"scratch-paint": "0.2.0-prerelease.20190318170811",
|
||||
"scratch-render": "0.1.0-prerelease.20190312125229",
|
||||
"scratch-storage": "1.2.2",
|
||||
"scratch-svg-renderer": "0.2.0-prerelease.20190304180800",
|
||||
"scratch-vm": "0.2.0-prerelease.20190321151527",
|
||||
"scratch-audio": "0.1.0-prerelease.20190925183642",
|
||||
"scratch-l10n": "3.6.20191210224141",
|
||||
"scratch-blocks": "0.1.0-prerelease.1574180945",
|
||||
"scratch-paint": "0.2.0-prerelease.20191210104711",
|
||||
"scratch-render": "0.1.0-prerelease.20191217212645",
|
||||
"scratch-storage": "1.3.2",
|
||||
"scratch-svg-renderer": "0.2.0-prerelease.20191217211338",
|
||||
"scratch-vm": "0.2.0-prerelease.20191119203901",
|
||||
"selenium-webdriver": "3.6.0",
|
||||
"startaudiocontext": "1.2.1",
|
||||
"style-loader": "^0.23.0",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const path = require('path');
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['scratch', 'scratch/es6', 'scratch/react', 'plugin:import/errors'],
|
||||
@@ -20,6 +21,11 @@ module.exports = {
|
||||
settings: {
|
||||
react: {
|
||||
version: '16.2' // Prevent 16.3 lifecycle method errors
|
||||
},
|
||||
'import/resolver': {
|
||||
webpack: {
|
||||
config: path.resolve(__dirname, '../webpack.config.js')
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -24,7 +24,7 @@
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
overflow-y: auto;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
[dir="ltr"] .detail-area {
|
||||
|
||||
@@ -50,8 +50,16 @@ $fade-out-distance: 100px;
|
||||
overflow-y: scroll;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.list-area:after {
|
||||
/* Make sure there is room to scroll beyond the last tile */
|
||||
padding-bottom: 70px;
|
||||
content: '';
|
||||
display: block;
|
||||
height: 70px;
|
||||
width: 100%;
|
||||
flex-shrink: 0;
|
||||
order: 99999999;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import Box from '../box/box.jsx';
|
||||
import styles from './audio-trimmer.css';
|
||||
import SelectionHandle from './selection-handle.jsx';
|
||||
import Playhead from './playhead.jsx';
|
||||
|
||||
const AudioSelector = props => (
|
||||
<div
|
||||
className={classNames(styles.absolute, styles.selector)}
|
||||
ref={props.containerRef}
|
||||
onMouseDown={props.onNewSelectionMouseDown}
|
||||
onTouchStart={props.onNewSelectionMouseDown}
|
||||
>
|
||||
{props.trimStart === null ? null : (
|
||||
<Box
|
||||
className={classNames(styles.absolute)}
|
||||
style={{
|
||||
left: `${props.trimStart * 100}%`,
|
||||
width: `${100 * (props.trimEnd - props.trimStart)}%`
|
||||
}}
|
||||
>
|
||||
<Box className={classNames(styles.absolute, styles.selectionBackground)} />
|
||||
<SelectionHandle
|
||||
handleStyle={styles.leftHandle}
|
||||
onMouseDown={props.onTrimStartMouseDown}
|
||||
/>
|
||||
<SelectionHandle
|
||||
handleStyle={styles.rightHandle}
|
||||
onMouseDown={props.onTrimEndMouseDown}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{props.playhead ? (
|
||||
<Playhead
|
||||
playbackPosition={props.playhead}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
AudioSelector.propTypes = {
|
||||
containerRef: PropTypes.func,
|
||||
onNewSelectionMouseDown: PropTypes.func.isRequired,
|
||||
onTrimEndMouseDown: PropTypes.func.isRequired,
|
||||
onTrimStartMouseDown: PropTypes.func.isRequired,
|
||||
playhead: PropTypes.number,
|
||||
trimEnd: PropTypes.number,
|
||||
trimStart: PropTypes.number
|
||||
};
|
||||
|
||||
export default AudioSelector;
|
||||
@@ -1,10 +1,11 @@
|
||||
@import "../../css/colors.css";
|
||||
|
||||
$border-radius: 4px;
|
||||
$trim-handle-width: 12px;
|
||||
$trim-handle-height: 14px;
|
||||
$trim-handle-width: 30px;
|
||||
$trim-handle-height: 30px;
|
||||
$trim-handle-border: 3px;
|
||||
$stripe-size: 10px;
|
||||
$hover-scale: 2;
|
||||
$hover-scale: 1.25;
|
||||
|
||||
.absolute {
|
||||
position: absolute;
|
||||
@@ -17,6 +18,10 @@ $hover-scale: 2;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.selector {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.trim-background {
|
||||
cursor: pointer;
|
||||
touch-action: none;
|
||||
@@ -35,6 +40,11 @@ $hover-scale: 2;
|
||||
);
|
||||
}
|
||||
|
||||
.selection-background {
|
||||
background: $motion-primary;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.start-trim-background .trim-background-mask {
|
||||
border-top-left-radius: $border-radius;
|
||||
border-bottom-left-radius: $border-radius;
|
||||
@@ -53,6 +63,10 @@ $hover-scale: 2;
|
||||
border: 1px solid $red-tertiary;
|
||||
}
|
||||
|
||||
.selector .trim-line {
|
||||
border: 1px solid $motion-tertiary;
|
||||
}
|
||||
|
||||
.playhead-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -68,31 +82,50 @@ $hover-scale: 2;
|
||||
so that we can use transform: translateX() using percentages.
|
||||
*/
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-left: 1px solid $motion-primary;
|
||||
border-top: none;
|
||||
border-bottom: none;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.start-trim-line {
|
||||
right: 0;
|
||||
.right-handle {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.end-trim-line {
|
||||
left: 0;
|
||||
.selector .left-handle {
|
||||
left: -1px
|
||||
}
|
||||
|
||||
.selector .right-handle {
|
||||
right: -1px
|
||||
}
|
||||
|
||||
.trimmer .left-handle {
|
||||
right: -1px
|
||||
}
|
||||
|
||||
.trimmer .right-handle {
|
||||
left: -1px
|
||||
}
|
||||
|
||||
.trim-handle {
|
||||
position: absolute;
|
||||
left: calc(-$trim-handle-width / 2);
|
||||
width: $trim-handle-width;
|
||||
height: $trim-handle-height;
|
||||
right: 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.trimmer .trim-handle {
|
||||
filter: hue-rotate(150deg);
|
||||
}
|
||||
|
||||
.trim-handle img {
|
||||
position: absolute;
|
||||
width: $trim-handle-width;
|
||||
height: $trim-handle-height;
|
||||
left: calc($trim-handle-border * 1.5);
|
||||
|
||||
/* Make sure image dragging isn't triggered */
|
||||
user-select: none;
|
||||
@@ -103,22 +136,13 @@ $hover-scale: 2;
|
||||
}
|
||||
|
||||
.top-trim-handle {
|
||||
top: -$trim-handle-height;
|
||||
top: calc(-$trim-handle-height + $trim-handle-border);
|
||||
}
|
||||
|
||||
.bottom-trim-handle {
|
||||
bottom: -$trim-handle-height;
|
||||
bottom: calc(-$trim-handle-height + $trim-handle-border);
|
||||
}
|
||||
|
||||
.top-trim-handle img {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* Increase handle size when anywhere on draggable area is hovered */
|
||||
.trim-background:hover img {
|
||||
transform: scale($hover-scale);
|
||||
}
|
||||
|
||||
.trim-background:hover .top-trim-handle img {
|
||||
transform: rotate(180deg) scale($hover-scale);
|
||||
transform: scaleY(-1);
|
||||
}
|
||||
@@ -3,11 +3,12 @@ import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import Box from '../box/box.jsx';
|
||||
import styles from './audio-trimmer.css';
|
||||
import handleIcon from './icon--handle.svg';
|
||||
import SelectionHandle from './selection-handle.jsx';
|
||||
import Playhead from './playhead.jsx';
|
||||
|
||||
const AudioTrimmer = props => (
|
||||
<div
|
||||
className={styles.absolute}
|
||||
className={classNames(styles.absolute, styles.trimmer)}
|
||||
ref={props.containerRef}
|
||||
>
|
||||
{props.trimStart === null ? null : (
|
||||
@@ -20,28 +21,16 @@ const AudioTrimmer = props => (
|
||||
onTouchStart={props.onTrimStartMouseDown}
|
||||
>
|
||||
<Box className={classNames(styles.absolute, styles.trimBackgroundMask)} />
|
||||
<Box className={classNames(styles.trimLine, styles.startTrimLine)}>
|
||||
<Box className={classNames(styles.trimHandle, styles.topTrimHandle, styles.startTrimHandle)}>
|
||||
<img src={handleIcon} />
|
||||
</Box>
|
||||
<Box className={classNames(styles.trimHandle, styles.bottomTrimHandle, styles.startTrimHandle)}>
|
||||
<img src={handleIcon} />
|
||||
</Box>
|
||||
</Box>
|
||||
<SelectionHandle
|
||||
handleStyle={styles.leftHandle}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{props.playhead ? (
|
||||
<div className={styles.playheadContainer}>
|
||||
<div
|
||||
className={classNames(styles.trimLine, styles.playhead)}
|
||||
style={{
|
||||
transform: `translateX(${100 * props.playhead}%)`
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Playhead
|
||||
playbackPosition={props.playhead}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{props.trimEnd === null ? null : (
|
||||
<Box
|
||||
className={classNames(styles.absolute, styles.trimBackground, styles.endTrimBackground)}
|
||||
@@ -53,14 +42,9 @@ const AudioTrimmer = props => (
|
||||
onTouchStart={props.onTrimEndMouseDown}
|
||||
>
|
||||
<Box className={classNames(styles.absolute, styles.trimBackgroundMask)} />
|
||||
<Box className={classNames(styles.trimLine, styles.endTrimLine)}>
|
||||
<Box className={classNames(styles.trimHandle, styles.topTrimHandle, styles.endTrimHandle)}>
|
||||
<img src={handleIcon} />
|
||||
</Box>
|
||||
<Box className={classNames(styles.trimHandle, styles.bottomTrimHandle, styles.endTrimHandle)}>
|
||||
<img src={handleIcon} />
|
||||
</Box>
|
||||
</Box>
|
||||
<SelectionHandle
|
||||
handleStyle={styles.rightHandle}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="8px" height="10px" viewBox="0 0 8 10" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>handle</title>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="34px" height="34px" viewBox="1 1 33 33" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 55.2 (78181) - https://sketchapp.com -->
|
||||
<title>Bottom Left Handle</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
||||
<g id="Desktop---1280x720" transform="translate(-461.000000, -329.000000)" stroke="#E64D00" fill="#FF661A">
|
||||
<g id="Step-5---Confirming-Trim" transform="translate(0.000000, 42.000000)">
|
||||
<g id="Sound-Sketches" transform="translate(427.000000, 167.000000)">
|
||||
<g id="start-playhead" transform="translate(35.000000, 0.000000)">
|
||||
<path d="M0,122.213854 C0,121.658366 0.45097518,121.208054 0.990777969,121.208054 L5.00922203,121.208054 C5.55641359,121.208054 6,121.659838 6,122.213854 L6,126.298963 L3,129.208054 L0,126.298963 L0,122.213854 Z" id="handle" transform="translate(3.000000, 125.208054) scale(1, -1) translate(-3.000000, -125.208054) "></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<path d="M17,5 C23.627417,5 29,10.372583 29,17 L29,29 L17,29 C10.372583,29 5,23.627417 5,17 C5,10.372583 10.372583,5 17,5 Z" id="path-1"></path>
|
||||
</defs>
|
||||
<g id="Bottom-Left-Handle" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Bottom-Left" transform="translate(17.000000, 17.000000) scale(1, -1) translate(-17.000000, -17.000000) ">
|
||||
<use stroke="#4D97FF33" stroke-width="8" xlink:href="#path-1"></use>
|
||||
<use fill="black" fill-opacity="1" filter="url(#filter-2)" xlink:href="#path-1"></use>
|
||||
<use stroke="#3D73CC" stroke-width="1" fill="#4D97FF" fill-rule="evenodd" xlink:href="#path-1"></use>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,21 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styles from './audio-trimmer.css';
|
||||
|
||||
const Playhead = props => (
|
||||
<div className={styles.playheadContainer}>
|
||||
<div
|
||||
className={classNames(styles.playhead)}
|
||||
style={{
|
||||
transform: `translateX(${100 * props.playbackPosition}%)`
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
Playhead.propTypes = {
|
||||
playbackPosition: PropTypes.number
|
||||
};
|
||||
|
||||
export default Playhead;
|
||||
@@ -0,0 +1,28 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import Box from '../box/box.jsx';
|
||||
import styles from './audio-trimmer.css';
|
||||
import handleIcon from './icon--handle.svg';
|
||||
|
||||
const SelectionHandle = props => (
|
||||
<Box
|
||||
className={classNames(styles.trimLine, props.handleStyle)}
|
||||
onMouseDown={props.onMouseDown}
|
||||
onTouchStart={props.onMouseDown}
|
||||
>
|
||||
<Box className={classNames(styles.trimHandle, styles.topTrimHandle)}>
|
||||
<img src={handleIcon} />
|
||||
</Box>
|
||||
<Box className={classNames(styles.trimHandle, styles.bottomTrimHandle)}>
|
||||
<img src={handleIcon} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
SelectionHandle.propTypes = {
|
||||
handleStyle: PropTypes.string,
|
||||
onMouseDown: PropTypes.func
|
||||
};
|
||||
|
||||
export default SelectionHandle;
|
||||
@@ -2,10 +2,18 @@
|
||||
@import "../../css/colors.css";
|
||||
@import "../../css/z-index.css";
|
||||
|
||||
.card-container-overlay {
|
||||
position: fixed;
|
||||
pointer-events: none;
|
||||
z-index: $z-index-card;
|
||||
}
|
||||
|
||||
.card-container {
|
||||
position:absolute;
|
||||
pointer-events: auto;
|
||||
z-index: $z-index-card;
|
||||
margin: 0.5rem 2rem;
|
||||
min-width: 468px;
|
||||
}
|
||||
|
||||
.left-card, .right-card {
|
||||
@@ -104,11 +112,38 @@
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.header-buttons-hidden {
|
||||
border-bottom: 0px;
|
||||
}
|
||||
|
||||
.header-buttons-right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.header-buttons img {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.shrink-expand-button {
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.shrink-expand-button:hover, .all-button:hover {
|
||||
background-color: $ui-black-transparent;
|
||||
}
|
||||
|
||||
.remove-button, .all-button {
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
@@ -127,6 +162,7 @@
|
||||
}
|
||||
|
||||
.step-body {
|
||||
width: 100%;
|
||||
background: $ui-white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -134,14 +170,19 @@
|
||||
align-items: center;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
|
||||
/* Min height prevents layout changing when images change */
|
||||
min-height: 256px;
|
||||
}
|
||||
|
||||
.step-video {
|
||||
height: 337px;
|
||||
height: 256px;
|
||||
}
|
||||
|
||||
.step-image {
|
||||
max-width: 450px;
|
||||
max-height: 200px;
|
||||
object-fit: contain;
|
||||
background: #F9F9F9;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 0.5rem;
|
||||
@@ -188,7 +229,7 @@
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
height: 1rem;
|
||||
height: 1.25rem;
|
||||
margin: .125rem 0; /* To offset the .25rem difference in icon size */
|
||||
}
|
||||
|
||||
@@ -240,14 +281,6 @@
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.video-cover {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.steps-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -267,3 +300,7 @@
|
||||
background: $ui-white;
|
||||
box-shadow: 0px 0px 0px 2px $ui-black-transparent;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, {Fragment} from 'react';
|
||||
import classNames from 'classnames';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import Draggable from 'react-draggable';
|
||||
|
||||
import styles from './card.css';
|
||||
|
||||
import shrinkIcon from './icon--shrink.svg';
|
||||
import expandIcon from './icon--expand.svg';
|
||||
|
||||
import rightArrow from './icon--next.svg';
|
||||
import leftArrow from './icon--prev.svg';
|
||||
|
||||
@@ -14,8 +18,8 @@ import closeIcon from './icon--close.svg';
|
||||
import {translateVideo} from '../../lib/libraries/decks/translate-video.js';
|
||||
import {translateImage} from '../../lib/libraries/decks/translate-image.js';
|
||||
|
||||
const CardHeader = ({onCloseCards, onShowAll, totalSteps, step}) => (
|
||||
<div className={styles.headerButtons}>
|
||||
const CardHeader = ({onCloseCards, onShrinkExpandCards, onShowAll, totalSteps, step, expanded}) => (
|
||||
<div className={expanded ? styles.headerButtons : classNames(styles.headerButtons, styles.headerButtonsHidden)}>
|
||||
<div
|
||||
className={styles.allButton}
|
||||
onClick={onShowAll}
|
||||
@@ -41,49 +45,107 @@ const CardHeader = ({onCloseCards, onShowAll, totalSteps, step}) => (
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
className={styles.removeButton}
|
||||
onClick={onCloseCards}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Close"
|
||||
description="Title for button to close how-to card"
|
||||
id="gui.cards.close"
|
||||
/>
|
||||
<img
|
||||
className={styles.closeIcon}
|
||||
src={closeIcon}
|
||||
/>
|
||||
<div className={styles.headerButtonsRight}>
|
||||
<div
|
||||
className={styles.shrinkExpandButton}
|
||||
onClick={onShrinkExpandCards}
|
||||
>
|
||||
<img
|
||||
draggable={false}
|
||||
src={expanded ? shrinkIcon : expandIcon}
|
||||
/>
|
||||
{expanded ?
|
||||
<FormattedMessage
|
||||
defaultMessage="Shrink"
|
||||
description="Title for button to shrink how-to card"
|
||||
id="gui.cards.shrink"
|
||||
/> :
|
||||
<FormattedMessage
|
||||
defaultMessage="Expand"
|
||||
description="Title for button to expand how-to card"
|
||||
id="gui.cards.expand"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
<div
|
||||
className={styles.removeButton}
|
||||
onClick={onCloseCards}
|
||||
>
|
||||
<img
|
||||
className={styles.closeIcon}
|
||||
src={closeIcon}
|
||||
/>
|
||||
<FormattedMessage
|
||||
defaultMessage="Close"
|
||||
description="Title for button to close how-to card"
|
||||
id="gui.cards.close"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Video step needs to know if the card is being dragged to cover the video
|
||||
// so that the mouseup is not swallowed by the iframe.
|
||||
const VideoStep = ({video, dragging}) => (
|
||||
<div className={styles.stepVideo}>
|
||||
{dragging ? (
|
||||
<div className={styles.videoCover} />
|
||||
) : null}
|
||||
<iframe
|
||||
allowFullScreen
|
||||
allowTransparency="true"
|
||||
frameBorder="0"
|
||||
height="338"
|
||||
scrolling="no"
|
||||
src={`https://fast.wistia.net/embed/iframe/${video}?seo=false&videoFoam=true`}
|
||||
title="📹"
|
||||
width="600"
|
||||
/>
|
||||
<script
|
||||
async
|
||||
src="https://fast.wistia.net/assets/external/E-v1.js"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
class VideoStep extends React.Component {
|
||||
|
||||
componentDidMount () {
|
||||
const script = document.createElement('script');
|
||||
script.src = `https://fast.wistia.com/embed/medias/${this.props.video}.jsonp`;
|
||||
script.async = true;
|
||||
script.setAttribute('id', 'wistia-video-content');
|
||||
document.body.appendChild(script);
|
||||
|
||||
const script2 = document.createElement('script');
|
||||
script2.src = 'https://fast.wistia.com/assets/external/E-v1.js';
|
||||
script2.async = true;
|
||||
script2.setAttribute('id', 'wistia-video-api');
|
||||
document.body.appendChild(script2);
|
||||
}
|
||||
|
||||
// We use the Wistia API here to update or pause the video dynamically:
|
||||
// https://wistia.com/support/developers/player-api
|
||||
componentDidUpdate (prevProps) {
|
||||
// Ensure the wistia API is loaded and available
|
||||
if (!(window.Wistia && window.Wistia.api)) return;
|
||||
|
||||
// Get a handle on the currently loaded video
|
||||
const video = window.Wistia.api(prevProps.video);
|
||||
|
||||
// Reset the video source if a new video has been chosen from the library
|
||||
if (prevProps.video !== this.props.video) {
|
||||
video.replaceWith(this.props.video);
|
||||
}
|
||||
|
||||
// Pause the video if the modal is being shrunken
|
||||
if (!this.props.expanded) {
|
||||
video.pause();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
const script = document.getElementById('wistia-video-content');
|
||||
script.parentNode.removeChild(script);
|
||||
|
||||
const script2 = document.getElementById('wistia-video-api');
|
||||
script2.parentNode.removeChild(script2);
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className={styles.stepVideo}>
|
||||
<div
|
||||
className={`wistia_embed wistia_async_${this.props.video}`}
|
||||
id="video-div"
|
||||
style={{height: `257px`, width: `466px`}}
|
||||
>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
VideoStep.propTypes = {
|
||||
dragging: PropTypes.bool.isRequired,
|
||||
expanded: PropTypes.bool.isRequired,
|
||||
video: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
@@ -96,6 +158,7 @@ const ImageStep = ({title, image}) => (
|
||||
<img
|
||||
className={styles.stepImage}
|
||||
draggable={false}
|
||||
key={image} /* Use src as key to prevent hanging around on slow connections */
|
||||
src={image}
|
||||
/>
|
||||
</div>
|
||||
@@ -107,13 +170,13 @@ ImageStep.propTypes = {
|
||||
title: PropTypes.node.isRequired
|
||||
};
|
||||
|
||||
const NextPrevButtons = ({isRtl, onNextStep, onPrevStep}) => (
|
||||
const NextPrevButtons = ({isRtl, onNextStep, onPrevStep, expanded}) => (
|
||||
<Fragment>
|
||||
{onNextStep ? (
|
||||
<div>
|
||||
<div className={isRtl ? styles.leftCard : styles.rightCard} />
|
||||
<div className={expanded ? (isRtl ? styles.leftCard : styles.rightCard) : styles.hidden} />
|
||||
<div
|
||||
className={isRtl ? styles.leftButton : styles.rightButton}
|
||||
className={expanded ? (isRtl ? styles.leftButton : styles.rightButton) : styles.hidden}
|
||||
onClick={onNextStep}
|
||||
>
|
||||
<img
|
||||
@@ -125,9 +188,9 @@ const NextPrevButtons = ({isRtl, onNextStep, onPrevStep}) => (
|
||||
) : null}
|
||||
{onPrevStep ? (
|
||||
<div>
|
||||
<div className={isRtl ? styles.rightCard : styles.leftCard} />
|
||||
<div className={expanded ? (isRtl ? styles.rightCard : styles.leftCard) : styles.hidden} />
|
||||
<div
|
||||
className={isRtl ? styles.rightButton : styles.leftButton}
|
||||
className={expanded ? (isRtl ? styles.rightButton : styles.leftButton) : styles.hidden}
|
||||
onClick={onPrevStep}
|
||||
>
|
||||
<img
|
||||
@@ -141,13 +204,16 @@ const NextPrevButtons = ({isRtl, onNextStep, onPrevStep}) => (
|
||||
);
|
||||
|
||||
NextPrevButtons.propTypes = {
|
||||
expanded: PropTypes.bool.isRequired,
|
||||
isRtl: PropTypes.bool,
|
||||
onNextStep: PropTypes.func,
|
||||
onPrevStep: PropTypes.func
|
||||
};
|
||||
CardHeader.propTypes = {
|
||||
expanded: PropTypes.bool.isRequired,
|
||||
onCloseCards: PropTypes.func.isRequired,
|
||||
onShowAll: PropTypes.func.isRequired,
|
||||
onShrinkExpandCards: PropTypes.func.isRequired,
|
||||
step: PropTypes.number,
|
||||
totalSteps: PropTypes.number
|
||||
};
|
||||
@@ -219,78 +285,113 @@ const Cards = props => {
|
||||
locale,
|
||||
onActivateDeckFactory,
|
||||
onCloseCards,
|
||||
onShrinkExpandCards,
|
||||
onDrag,
|
||||
onStartDrag,
|
||||
onEndDrag,
|
||||
onShowAll,
|
||||
onNextStep,
|
||||
onPrevStep,
|
||||
showVideos,
|
||||
step,
|
||||
expanded,
|
||||
...posProps
|
||||
} = props;
|
||||
let {x, y} = posProps;
|
||||
|
||||
if (activeDeckId === null) return;
|
||||
|
||||
// Tutorial cards need to calculate their own dragging bounds
|
||||
// to allow for dragging the cards off the left, right and bottom
|
||||
// edges of the workspace.
|
||||
const cardHorizontalDragOffset = 400; // ~80% of card width
|
||||
const cardVerticalDragOffset = expanded ? 257 : 0; // ~80% of card height, if expanded
|
||||
const menuBarHeight = 48; // TODO: get pre-calculated from elsewhere?
|
||||
const wideCardWidth = 500;
|
||||
|
||||
if (x === 0 && y === 0) {
|
||||
// initialize positions
|
||||
x = isRtl ? -292 : 292;
|
||||
// The tallest cards are about 385px high, and the default position is pinned
|
||||
x = isRtl ? (-190 - wideCardWidth - cardHorizontalDragOffset) : 292;
|
||||
x += cardHorizontalDragOffset;
|
||||
// The tallest cards are about 320px high, and the default position is pinned
|
||||
// to near the bottom of the blocks palette to allow room to work above.
|
||||
const tallCardHeight = 385;
|
||||
const tallCardHeight = 320;
|
||||
const bottomMargin = 60; // To avoid overlapping the backpack region
|
||||
y = window.innerHeight - tallCardHeight - bottomMargin;
|
||||
y = window.innerHeight - tallCardHeight - bottomMargin - menuBarHeight;
|
||||
}
|
||||
|
||||
const steps = content[activeDeckId].steps;
|
||||
|
||||
return (
|
||||
<Draggable
|
||||
bounds="parent"
|
||||
position={{x: x, y: y}}
|
||||
onDrag={onDrag}
|
||||
onStart={onStartDrag}
|
||||
onStop={onEndDrag}
|
||||
// Custom overlay to act as the bounding parent for the draggable, using values from above
|
||||
<div
|
||||
className={styles.cardContainerOverlay}
|
||||
style={{
|
||||
width: `${window.innerWidth + (2 * cardHorizontalDragOffset)}px`,
|
||||
height: `${window.innerHeight - menuBarHeight + cardVerticalDragOffset}px`,
|
||||
top: `${menuBarHeight}px`,
|
||||
left: `${-cardHorizontalDragOffset}px`
|
||||
}}
|
||||
>
|
||||
<div className={styles.cardContainer}>
|
||||
<div className={styles.card}>
|
||||
<CardHeader
|
||||
step={step}
|
||||
totalSteps={steps.length}
|
||||
onCloseCards={onCloseCards}
|
||||
onShowAll={onShowAll}
|
||||
/>
|
||||
<div className={styles.stepBody}>
|
||||
{steps[step].deckIds ? (
|
||||
<PreviewsStep
|
||||
content={content}
|
||||
deckIds={steps[step].deckIds}
|
||||
onActivateDeckFactory={onActivateDeckFactory}
|
||||
onShowAll={onShowAll}
|
||||
/>
|
||||
) : (
|
||||
steps[step].video ? (
|
||||
<VideoStep
|
||||
dragging={dragging}
|
||||
video={translateVideo(steps[step].video, locale)}
|
||||
<Draggable
|
||||
bounds="parent"
|
||||
cancel="#video-div" // disable dragging on video div
|
||||
position={{x: x, y: y}}
|
||||
onDrag={onDrag}
|
||||
onStart={onStartDrag}
|
||||
onStop={onEndDrag}
|
||||
>
|
||||
<div className={styles.cardContainer}>
|
||||
<div className={styles.card}>
|
||||
<CardHeader
|
||||
expanded={expanded}
|
||||
step={step}
|
||||
totalSteps={steps.length}
|
||||
onCloseCards={onCloseCards}
|
||||
onShowAll={onShowAll}
|
||||
onShrinkExpandCards={onShrinkExpandCards}
|
||||
/>
|
||||
<div className={expanded ? styles.stepBody : styles.hidden}>
|
||||
{steps[step].deckIds ? (
|
||||
<PreviewsStep
|
||||
content={content}
|
||||
deckIds={steps[step].deckIds}
|
||||
onActivateDeckFactory={onActivateDeckFactory}
|
||||
onShowAll={onShowAll}
|
||||
/>
|
||||
) : (
|
||||
<ImageStep
|
||||
image={translateImage(steps[step].image, locale)}
|
||||
title={steps[step].title}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{steps[step].trackingPixel && steps[step].trackingPixel}
|
||||
steps[step].video ? (
|
||||
showVideos ? (
|
||||
<VideoStep
|
||||
dragging={dragging}
|
||||
expanded={expanded}
|
||||
video={translateVideo(steps[step].video, locale)}
|
||||
/>
|
||||
) : ( // Else show the deck image and title
|
||||
<ImageStep
|
||||
image={content[activeDeckId].img}
|
||||
title={content[activeDeckId].name}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<ImageStep
|
||||
image={translateImage(steps[step].image, locale)}
|
||||
title={steps[step].title}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{steps[step].trackingPixel && steps[step].trackingPixel}
|
||||
</div>
|
||||
<NextPrevButtons
|
||||
expanded={expanded}
|
||||
isRtl={isRtl}
|
||||
onNextStep={step < steps.length - 1 ? onNextStep : null}
|
||||
onPrevStep={step > 0 ? onPrevStep : null}
|
||||
/>
|
||||
</div>
|
||||
<NextPrevButtons
|
||||
isRtl={isRtl}
|
||||
onNextStep={step < steps.length - 1 ? onNextStep : null}
|
||||
onPrevStep={step > 0 ? onPrevStep : null}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Draggable>
|
||||
</Draggable>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -309,6 +410,7 @@ Cards.propTypes = {
|
||||
})
|
||||
}),
|
||||
dragging: PropTypes.bool.isRequired,
|
||||
expanded: PropTypes.bool.isRequired,
|
||||
isRtl: PropTypes.bool.isRequired,
|
||||
locale: PropTypes.string.isRequired,
|
||||
onActivateDeckFactory: PropTypes.func.isRequired,
|
||||
@@ -318,10 +420,21 @@ Cards.propTypes = {
|
||||
onNextStep: PropTypes.func.isRequired,
|
||||
onPrevStep: PropTypes.func.isRequired,
|
||||
onShowAll: PropTypes.func,
|
||||
onShrinkExpandCards: PropTypes.func.isRequired,
|
||||
onStartDrag: PropTypes.func,
|
||||
showVideos: PropTypes.bool,
|
||||
step: PropTypes.number.isRequired,
|
||||
x: PropTypes.number,
|
||||
y: PropTypes.number
|
||||
};
|
||||
|
||||
export default Cards;
|
||||
Cards.defaultProps = {
|
||||
showVideos: true
|
||||
};
|
||||
|
||||
export {
|
||||
Cards as default,
|
||||
// Others exported for testability
|
||||
ImageStep,
|
||||
VideoStep
|
||||
};
|
||||
@@ -1,17 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 52.2 (67145) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>General/Check Copy</title>
|
||||
<!-- Generator: Sketch 53.2 (72643) - https://sketchapp.com -->
|
||||
<title>Extensions/Connection/Close</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
<path d="M10,7.73577763 L12.2675846,5.46819305 C12.8909754,4.84480221 13.8988379,4.84194841 14.5284447,5.47155526 C15.1536925,6.096803 15.1476685,7.11655392 14.531807,7.73241542 L12.2642224,10 L14.531807,12.2675846 C15.1551978,12.8909754 15.1580516,13.8988379 14.5284447,14.5284447 C13.903197,15.1536925 12.8834461,15.1476685 12.2675846,14.531807 L10,12.2642224 L7.73241542,14.531807 C7.10902458,15.1551978 6.10116212,15.1580516 5.47155526,14.5284447 C4.84630752,13.903197 4.85233155,12.8834461 5.46819305,12.2675846 L7.73577763,10 L5.46819305,7.73241542 C4.84480221,7.10902458 4.84194841,6.10116212 5.47155526,5.47155526 C6.096803,4.84630752 7.11655392,4.85233155 7.73241542,5.46819305 L10,7.73577763 Z" id="path-1"></path>
|
||||
<path d="M15.464935,15.467297 C14.7644059,16.1777705 13.6185877,16.1777705 12.9081142,15.467297 L9.99881899,12.5580018 L7.08841885,15.467297 C6.38236506,16.1733508 5.23765187,16.1733508 4.53159807,15.467297 C4.17912364,15.1148226 4.00012409,14.6485398 4.00012409,14.1888866 C4.00012409,13.7281285 4.17912364,13.2629506 4.53159807,12.9104762 L7.44089328,10.001181 L4.52717833,7.08636112 C4.17359897,6.73278176 3.99459941,6.26760391 4.00012409,5.80242606 C4.00012409,5.34166795 4.17359897,4.88201477 4.52717833,4.52954034 C5.23212719,3.82348655 6.37684038,3.82348655 7.08399911,4.52954034 L9.99881899,7.44325529 L12.9125339,4.52954034 C13.6185877,3.82348655 14.7644059,3.82348655 15.4704597,4.52954034 C16.1765134,5.2344892 16.1765134,6.38030733 15.4704597,7.08636112 L12.5545348,10.001181 L15.4704597,12.914896 C16.1765134,13.6209497 16.1765134,14.7557185 15.464935,15.467297" id="path-1"></path>
|
||||
</defs>
|
||||
<g id="General/Check-Copy" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Extensions/Connection/Close" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<mask id="mask-2" fill="white">
|
||||
<use xlink:href="#path-1"></use>
|
||||
</mask>
|
||||
<use id="close-icon" fill="#FFFFFF" xlink:href="#path-1"></use>
|
||||
<g id="Color/White" mask="url(#mask-2)" fill="#FFFFFF">
|
||||
<use id="close" fill="#FFFFFF" xlink:href="#path-1"></use>
|
||||
<g id="White" mask="url(#mask-2)" fill="#FFFFFF">
|
||||
<rect id="Color" x="0" y="0" width="20" height="20"></rect>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 53.2 (72643) - https://sketchapp.com -->
|
||||
<title>Tutorials/Expand</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
<path d="M4.5,2 L15.5,2 C16.8807119,2 18,3.11928813 18,4.5 L18,5.5 C18,5.77614237 17.7761424,6 17.5,6 L2.5,6 C2.22385763,6 2,5.77614237 2,5.5 L2,4.5 C2,3.11928813 3.11928813,2 4.5,2 Z M2.5,7 L17.5,7 C17.7761424,7 18,7.22385763 18,7.5 L18,15.5 C18,16.8807119 16.8807119,18 15.5,18 L4.5,18 C3.11928813,18 2,16.8807119 2,15.5 L2,7.5 C2,7.22385763 2.22385763,7 2.5,7 Z M9.65509466,15.1636146 C9.745416,15.2533569 9.86700241,15.3031495 9.99727357,15.3031495 C10.1275447,15.3031495 10.2491311,15.2533569 10.3394525,15.1636146 L12.1574588,13.3456082 C12.3004676,13.2025994 12.3415754,12.9993764 12.2657286,12.8158388 C12.1898819,12.6334592 12.0109762,12.5153467 11.8100691,12.5153467 L11.1506075,12.5153467 L10.7899012,9.98808625 C10.7464775,9.64474938 10.4703026,9.36567961 10.1124912,9.30951827 C10.0783312,9.30546539 10.0435922,9.30314946 10.0094322,9.30314946 C9.61282891,9.30314946 9.27586085,9.59785176 9.22548933,9.98519134 L8.8740467,12.5153467 L8.18447804,12.5153467 C7.9858869,12.5153467 7.81450796,12.6265114 7.73692425,12.8065751 C7.65876155,12.9866388 7.69639544,13.1962306 7.83708829,13.3456082 L9.65509466,15.1636146 Z" id="path-1"></path>
|
||||
</defs>
|
||||
<g id="Tutorials/Expand" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<mask id="mask-2" fill="white">
|
||||
<use xlink:href="#path-1"></use>
|
||||
</mask>
|
||||
<g id="Combined-Shape" fill-rule="nonzero"></g>
|
||||
<g id="White" mask="url(#mask-2)" fill="#FFFFFF">
|
||||
<rect id="Color" x="0" y="0" width="20" height="20"></rect>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 53.2 (72643) - https://sketchapp.com -->
|
||||
<title>Tutorials/Shink</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
<path d="M2.5,8 C2.22385763,8 2,7.77614237 2,7.5 C2,7.22385763 2.22385763,7 2.5,7 L3.5,7 C3.77614237,7 4,7.22385763 4,7.5 C4,7.77614237 3.77614237,8 3.5,8 L2.5,8 Z M5.5,8 C5.22385763,8 5,7.77614237 5,7.5 C5,7.22385763 5.22385763,7 5.5,7 L6.5,7 C6.77614237,7 7,7.22385763 7,7.5 C7,7.77614237 6.77614237,8 6.5,8 L5.5,8 Z M8.5,8 C8.22385763,8 8,7.77614237 8,7.5 C8,7.22385763 8.22385763,7 8.5,7 L9.5,7 C9.77614237,7 10,7.22385763 10,7.5 C10,7.77614237 9.77614237,8 9.5,8 L8.5,8 Z M11.5,8 C11.2238576,8 11,7.77614237 11,7.5 C11,7.22385763 11.2238576,7 11.5,7 L12.5,7 C12.7761424,7 13,7.22385763 13,7.5 C13,7.77614237 12.7761424,8 12.5,8 L11.5,8 Z M14.5,8 C14.2238576,8 14,7.77614237 14,7.5 C14,7.22385763 14.2238576,7 14.5,7 L15.5,7 C15.7761424,7 16,7.22385763 16,7.5 C16,7.77614237 15.7761424,8 15.5,8 L14.5,8 Z M17,7.5 C17,7.22385763 17.2238576,7 17.5,7 C17.7761424,7 18,7.22385763 18,7.5 L18,8.5 C18,8.77614237 17.7761424,9 17.5,9 C17.2238576,9 17,8.77614237 17,8.5 L17,7.5 Z M17,10.5 C17,10.2238576 17.2238576,10 17.5,10 C17.7761424,10 18,10.2238576 18,10.5 L18,11.5 C18,11.7761424 17.7761424,12 17.5,12 C17.2238576,12 17,11.7761424 17,11.5 L17,10.5 Z M17,13.5 C17,13.2238576 17.2238576,13 17.5,13 C17.7761424,13 18,13.2238576 18,13.5 L18,14.5 C18,14.7761424 17.7761424,15 17.5,15 C17.2238576,15 17,14.7761424 17,14.5 L17,13.5 Z M16.8130884,16.2258789 C16.9469778,15.9843665 17.2513011,15.8971208 17.4928135,16.0310102 C17.7343259,16.1648996 17.8215716,16.4692229 17.6876822,16.7107353 C17.4827416,17.0804113 17.1867239,17.3929429 16.8289716,17.6178178 C16.59518,17.7647739 16.286523,17.6943798 16.1395669,17.4605882 C15.9926108,17.2267966 16.0630049,16.9181396 16.2967965,16.7711835 C16.5118233,16.6360225 16.6900031,16.4479027 16.8130884,16.2258789 Z M14.5614675,17 C14.8376098,17 15.0614675,17.2238576 15.0614675,17.5 C15.0614675,17.7761424 14.8376098,18 14.5614675,18 L13.5614675,18 C13.2853251,18 13.0614675,17.7761424 13.0614675,17.5 C13.0614675,17.2238576 13.2853251,17 13.5614675,17 L14.5614675,17 Z M11.5614675,17 C11.8376098,17 12.0614675,17.2238576 12.0614675,17.5 C12.0614675,17.7761424 11.8376098,18 11.5614675,18 L10.5614675,18 C10.2853251,18 10.0614675,17.7761424 10.0614675,17.5 C10.0614675,17.2238576 10.2853251,17 10.5614675,17 L11.5614675,17 Z M8.56146746,17 C8.83760983,17 9.06146746,17.2238576 9.06146746,17.5 C9.06146746,17.7761424 8.83760983,18 8.56146746,18 L7.56146746,18 C7.28532508,18 7.06146746,17.7761424 7.06146746,17.5 C7.06146746,17.2238576 7.28532508,17 7.56146746,17 L8.56146746,17 Z M5.56146746,17 C5.83760983,17 6.06146746,17.2238576 6.06146746,17.5 C6.06146746,17.7761424 5.83760983,18 5.56146746,18 L4.56146746,18 C4.28532508,18 4.06146746,17.7761424 4.06146746,17.5 C4.06146746,17.2238576 4.28532508,17 4.56146746,17 L5.56146746,17 Z M3.22779886,16.2951753 C3.37445671,16.5291541 3.30366909,16.8377211 3.06969027,16.9843789 C2.83571145,17.1310368 2.52714447,17.0602491 2.38048662,16.8262703 C2.15682122,16.4694332 2.02609629,16.0609535 2.00352115,15.6335963 C1.98895424,15.3578384 2.20069137,15.1224836 2.47644927,15.1079167 C2.75220716,15.0933498 2.98756192,15.3050869 3.00212882,15.5808448 C3.01567642,15.8373067 3.0937811,16.0813625 3.22779886,16.2951753 Z M3,13.6229349 C3,13.8990773 2.77614237,14.1229349 2.5,14.1229349 C2.22385763,14.1229349 2,13.8990773 2,13.6229349 L2,12.6229349 C2,12.3467925 2.22385763,12.1229349 2.5,12.1229349 C2.77614237,12.1229349 3,12.3467925 3,12.6229349 L3,13.6229349 Z M3,10.6229349 C3,10.8990773 2.77614237,11.1229349 2.5,11.1229349 C2.22385763,11.1229349 2,10.8990773 2,10.6229349 L2,9.62293492 C2,9.34679254 2.22385763,9.12293492 2.5,9.12293492 C2.77614237,9.12293492 3,9.34679254 3,9.62293492 L3,10.6229349 Z M3,7.62293492 C3,7.89907729 2.77614237,8.12293492 2.5,8.12293492 C2.22385763,8.12293492 2,7.89907729 2,7.62293492 L2,7.5 C2,7.22385763 2.22385763,7 2.5,7 C2.77614237,7 3,7.22385763 3,7.5 L3,7.62293492 Z M4.5,2 L15.5,2 C16.8807119,2 18,3.11928813 18,4.5 L18,5.5 C18,5.77614237 17.7761424,6 17.5,6 L2.5,6 C2.22385763,6 2,5.77614237 2,5.5 L2,4.5 C2,3.11928813 3.11928813,2 4.5,2 Z M10.3449053,9.44268434 L12.1629117,11.2606907 C12.3036046,11.4100683 12.3412384,11.6196601 12.2630758,11.7997238 C12.185492,11.9797875 12.0141131,12.0909522 11.815522,12.0909522 L11.1259533,12.0909522 L10.7745107,14.6211076 C10.7241392,15.0084472 10.3871711,15.3031495 9.99056779,15.3031495 C9.9564078,15.3031495 9.92166882,15.3008335 9.88750883,15.2967806 C9.52969739,15.2406193 9.25352253,14.9615495 9.21009882,14.6182127 L8.84939246,12.0909522 L8.18993091,12.0909522 C7.98902384,12.0909522 7.81011812,11.9728397 7.73427135,11.7904601 C7.65842459,11.6069225 7.69953238,11.4036995 7.84254116,11.2606907 L9.66054753,9.44268434 C9.75086886,9.35294199 9.87245528,9.30314946 10.0027264,9.30314946 C10.1329976,9.30314946 10.254584,9.35294199 10.3449053,9.44268434 Z" id="path-1"></path>
|
||||
</defs>
|
||||
<g id="Tutorials/Shink" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<mask id="mask-2" fill="white">
|
||||
<use xlink:href="#path-1"></use>
|
||||
</mask>
|
||||
<g id="Rectangle" fill-rule="nonzero"></g>
|
||||
<g id="White" mask="url(#mask-2)" fill="#FFFFFF">
|
||||
<rect id="Color" x="0" y="0" width="20" height="20"></rect>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.5 KiB |
@@ -44,7 +44,7 @@ const ConnectionModalComponent = props => (
|
||||
);
|
||||
|
||||
ConnectionModalComponent.propTypes = {
|
||||
connectingMessage: PropTypes.node,
|
||||
connectingMessage: PropTypes.node.isRequired,
|
||||
connectionSmallIconURL: PropTypes.string,
|
||||
connectionTipIconURL: PropTypes.string,
|
||||
name: PropTypes.node,
|
||||
@@ -55,6 +55,10 @@ ConnectionModalComponent.propTypes = {
|
||||
useAutoScan: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
ConnectionModalComponent.defaultProps = {
|
||||
connectingMessage: 'Connecting'
|
||||
};
|
||||
|
||||
export {
|
||||
ConnectionModalComponent as default,
|
||||
PHASES
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
@import "../../css/colors.css";
|
||||
@import "../../css/units.css";
|
||||
|
||||
/* wrapper to allow for touch slop if we decide to add it */
|
||||
.delete-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease-out;
|
||||
}
|
||||
|
||||
.delete-button-visible {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden; /* Mask the icon animation */
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
box-shadow: 0px 0px 0px 2px $motion-transparent;
|
||||
background-color: $motion-primary;
|
||||
color: $ui-white;
|
||||
border-radius: 50%;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease-out;
|
||||
}
|
||||
|
||||
.delete-icon {
|
||||
position: relative;
|
||||
margin: 0.25rem;
|
||||
user-select: none;
|
||||
transform-origin: 50%;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import styles from './delete-button.css';
|
||||
import deleteIcon from './icon--delete.svg';
|
||||
|
||||
const DeleteButton = props => (
|
||||
<div
|
||||
aria-label="Delete"
|
||||
className={classNames(
|
||||
styles.deleteButton,
|
||||
props.className
|
||||
)}
|
||||
role="button"
|
||||
tabIndex={props.tabIndex}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
<div className={styles.deleteButtonVisible}>
|
||||
<img
|
||||
className={styles.deleteIcon}
|
||||
src={deleteIcon}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
);
|
||||
|
||||
DeleteButton.propTypes = {
|
||||
className: PropTypes.string,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
tabIndex: PropTypes.number
|
||||
};
|
||||
|
||||
DeleteButton.defaultProps = {
|
||||
tabIndex: 0
|
||||
};
|
||||
|
||||
export default DeleteButton;
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 55.1 (78136) - https://sketchapp.com -->
|
||||
<title>Sound/General/Delete</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
<path d="M4.54751641,6.99994966 L15.4523042,6.99994966 C15.7284466,6.99994966 15.9523042,7.22380729 15.9523042,7.49994966 C15.9523042,7.51506367 15.9516189,7.5301699 15.9502504,7.54522183 L15.1651793,16.1801783 C15.0715275,17.2102489 14.207924,17.9989808 13.1736049,17.9990897 L6.82662224,17.9997575 C5.79213514,17.9998663 4.92828345,17.2110677 4.83462539,16.180829 L4.04956981,7.54521753 C4.02456905,7.27020922 4.22724022,7.02700381 4.50224854,7.00200306 C4.51729904,7.00063483 4.53240384,6.99994966 4.54751641,6.99994966 Z M7.33333333,4 L7.88603796,2.34188612 C7.95409498,2.13771505 8.14516441,2 8.36037961,2 L11.6396204,2 C11.8548356,2 12.045905,2.13771505 12.113962,2.34188612 L12.6666667,4 L16.5,4 C16.7761424,4 17,4.22385763 17,4.5 L17,5.5 C17,5.77614237 16.7761424,6 16.5,6 L3.5,6 C3.22385763,6 3,5.77614237 3,5.5 L3,4.5 C3,4.22385763 3.22385763,4 3.5,4 L7.33333333,4 Z M8.38742589,4 L11.6125741,4 L11.2792408,3 L8.72075922,3 L8.38742589,4 Z M10,11.7204812 L11.5952436,10.1252376 C11.7905057,9.92997548 12.1070882,9.92997548 12.3023504,10.1252376 L12.3747624,10.1976496 C12.5700245,10.3929118 12.5700245,10.7094943 12.3747624,10.9047564 L10.7795188,12.5 L12.3747624,14.0952436 C12.5700245,14.2905057 12.5700245,14.6070882 12.3747624,14.8023504 L12.3023504,14.8747624 C12.1070882,15.0700245 11.7905057,15.0700245 11.5952436,14.8747624 L10,13.2795188 L8.40475641,14.8747624 C8.20949427,15.0700245 7.89291178,15.0700245 7.69764963,14.8747624 L7.62523762,14.8023504 C7.42997548,14.6070882 7.42997548,14.2905057 7.62523762,14.0952436 L9.22048121,12.5 L7.62523762,10.9047564 C7.42997548,10.7094943 7.42997548,10.3929118 7.62523762,10.1976496 L7.69764963,10.1252376 C7.89291178,9.92997548 8.20949427,9.92997548 8.40475641,10.1252376 L10,11.7204812 Z" id="path-1"></path>
|
||||
</defs>
|
||||
<g id="Sound/General/Delete" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<mask id="mask-2" fill="white">
|
||||
<use xlink:href="#path-1"></use>
|
||||
</mask>
|
||||
<g id="Trash-Can"></g>
|
||||
<g id="White" mask="url(#mask-2)" fill="#FFFFFF">
|
||||
<rect id="Color" x="0" y="0" width="20" height="20"></rect>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -66,8 +66,10 @@ const GUIComponent = props => {
|
||||
backpackVisible,
|
||||
blocksTabVisible,
|
||||
cardsVisible,
|
||||
canChangeLanguage,
|
||||
canCreateNew,
|
||||
canEditTitle,
|
||||
canManageFiles,
|
||||
canRemix,
|
||||
canSave,
|
||||
canCreateCopy,
|
||||
@@ -85,13 +87,13 @@ const GUIComponent = props => {
|
||||
isRtl,
|
||||
isShared,
|
||||
loading,
|
||||
logo,
|
||||
renderLogin,
|
||||
onClickAccountNav,
|
||||
onCloseAccountNav,
|
||||
onLogOut,
|
||||
onOpenRegistration,
|
||||
onToggleLoginOpen,
|
||||
onUpdateProjectTitle,
|
||||
onActivateCostumesTab,
|
||||
onActivateSoundsTab,
|
||||
onActivateTab,
|
||||
@@ -202,15 +204,18 @@ const GUIComponent = props => {
|
||||
authorId={authorId}
|
||||
authorThumbnailUrl={authorThumbnailUrl}
|
||||
authorUsername={authorUsername}
|
||||
canChangeLanguage={canChangeLanguage}
|
||||
canCreateCopy={canCreateCopy}
|
||||
canCreateNew={canCreateNew}
|
||||
canEditTitle={canEditTitle}
|
||||
canManageFiles={canManageFiles}
|
||||
canRemix={canRemix}
|
||||
canSave={canSave}
|
||||
canShare={canShare}
|
||||
className={styles.menuBarPosition}
|
||||
enableCommunity={enableCommunity}
|
||||
isShared={isShared}
|
||||
logo={logo}
|
||||
renderLogin={renderLogin}
|
||||
showComingSoon={showComingSoon}
|
||||
onClickAccountNav={onClickAccountNav}
|
||||
@@ -222,7 +227,6 @@ const GUIComponent = props => {
|
||||
onSeeCommunity={onSeeCommunity}
|
||||
onShare={onShare}
|
||||
onToggleLoginOpen={onToggleLoginOpen}
|
||||
onUpdateProjectTitle={onUpdateProjectTitle}
|
||||
/>
|
||||
<Box className={styles.bodyWrapper}>
|
||||
<Box className={styles.flexWrapper}>
|
||||
@@ -359,9 +363,11 @@ GUIComponent.propTypes = {
|
||||
backpackVisible: PropTypes.bool,
|
||||
basePath: PropTypes.string,
|
||||
blocksTabVisible: PropTypes.bool,
|
||||
canChangeLanguage: PropTypes.bool,
|
||||
canCreateCopy: PropTypes.bool,
|
||||
canCreateNew: PropTypes.bool,
|
||||
canEditTitle: PropTypes.bool,
|
||||
canManageFiles: PropTypes.bool,
|
||||
canRemix: PropTypes.bool,
|
||||
canSave: PropTypes.bool,
|
||||
canShare: PropTypes.bool,
|
||||
@@ -378,6 +384,7 @@ GUIComponent.propTypes = {
|
||||
isRtl: PropTypes.bool,
|
||||
isShared: PropTypes.bool,
|
||||
loading: PropTypes.bool,
|
||||
logo: PropTypes.string,
|
||||
onActivateCostumesTab: PropTypes.func,
|
||||
onActivateSoundsTab: PropTypes.func,
|
||||
onActivateTab: PropTypes.func,
|
||||
@@ -397,7 +404,6 @@ GUIComponent.propTypes = {
|
||||
onTelemetryModalOptIn: PropTypes.func,
|
||||
onTelemetryModalOptOut: PropTypes.func,
|
||||
onToggleLoginOpen: PropTypes.func,
|
||||
onUpdateProjectTitle: PropTypes.func,
|
||||
renderLogin: PropTypes.func,
|
||||
showComingSoon: PropTypes.bool,
|
||||
soundsTabVisible: PropTypes.bool,
|
||||
@@ -411,8 +417,10 @@ GUIComponent.defaultProps = {
|
||||
backpackHost: null,
|
||||
backpackVisible: false,
|
||||
basePath: './',
|
||||
canChangeLanguage: true,
|
||||
canCreateNew: false,
|
||||
canEditTitle: false,
|
||||
canManageFiles: true,
|
||||
canRemix: false,
|
||||
canSave: false,
|
||||
canCreateCopy: false,
|
||||
@@ -422,7 +430,6 @@ GUIComponent.defaultProps = {
|
||||
isCreating: false,
|
||||
isShared: false,
|
||||
loading: false,
|
||||
onUpdateProjectTitle: () => {},
|
||||
showComingSoon: false,
|
||||
stageSizeMode: STAGE_SIZE_MODES.large
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
font-size: 0.75rem;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
color: $motion-primary;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.container + .container {
|
||||
@@ -16,4 +17,14 @@
|
||||
|
||||
.title {
|
||||
margin-top: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.container:active {
|
||||
background-color: $motion-light-transparent;
|
||||
}
|
||||
@@ -5,14 +5,19 @@ import styles from './icon-button.css';
|
||||
|
||||
const IconButton = ({
|
||||
img,
|
||||
disabled,
|
||||
className,
|
||||
title,
|
||||
onClick
|
||||
}) => (
|
||||
<div
|
||||
className={classNames(styles.container, className)}
|
||||
className={classNames(
|
||||
styles.container,
|
||||
className,
|
||||
disabled ? styles.disabled : null
|
||||
)}
|
||||
role="button"
|
||||
onClick={onClick}
|
||||
onClick={disabled ? null : onClick}
|
||||
>
|
||||
<img
|
||||
className={styles.icon}
|
||||
@@ -27,6 +32,7 @@ const IconButton = ({
|
||||
|
||||
IconButton.propTypes = {
|
||||
className: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
img: PropTypes.string,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
title: PropTypes.node.isRequired
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="100px" height="100px" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 56.2 (81672) - https://sketch.com -->
|
||||
<title>Sound/General/Library Tile Icon/RTL</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="Sound/General/Library-Tile-Icon/RTL" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M40.7941213,60.3333334 C39.5757531,60.3333334 38.3858512,59.7627412 37.7254274,58.7262523 C34.4973213,53.700852 34.4973213,47.4609802 37.7254274,42.43558 C38.7160634,40.8860816 40.8909074,40.373072 42.5989001,41.2839258 C44.2898131,42.2052491 44.8534507,44.2101744 43.8514282,45.7701424 C41.9441696,48.7382694 41.9441696,52.423563 43.8514282,55.3969245 C44.8534507,56.951658 44.2898131,58.956583 42.5989001,59.8779064 C42.0295692,60.1867593 41.4033053,60.3333334 40.7941213,60.3333334 Z M30.014623,64.6666666 C28.9761967,64.6666666 27.9668853,64.1309203 27.3991478,63.1577291 C22.8669507,55.401691 22.8669507,45.7582521 27.3991478,38.002214 C28.2531806,36.5473425 30.1068197,36.0705772 31.5528528,36.9208906 C32.9940332,37.7859493 33.4744267,39.6733502 32.6203939,41.1331368 C29.2188198,46.957538 29.2188198,54.202405 32.6203939,60.0317213 C33.4744267,61.491508 32.9940332,63.3739938 31.5528528,64.2390524 C31.0724594,64.5290438 30.5338363,64.6666666 30.014623,64.6666666 Z M48.8695653,34.1786293 C48.8695653,30.5538239 53.3068341,28.6536838 56.0736627,31.1003021 L62.1037975,36.4397458 C64.2189991,38.3098044 66.9754844,39.3476118 69.8405742,39.3476118 L70.8283578,39.3476118 C73.6831043,39.3476118 76,41.5936876 76,44.3611739 L76,54.689112 C76,57.4616116 73.6831043,59.7026738 70.8283578,59.7026738 L69.7836861,59.7026738 C66.9185963,59.7026738 64.1621111,60.7404813 62.0469094,62.6105398 L56.0736627,67.899848 C53.3068341,70.3464662 48.8695653,68.4463264 48.8695653,64.8165072 L48.8695653,34.1786293 Z" id="Shape" fill="#575E75" fill-rule="nonzero"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="100px" height="100px" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 56.2 (81672) - https://sketch.com -->
|
||||
<title>Sound/General/Library Tile Icon/LTR</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="Sound/General/Library-Tile-Icon/LTR" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M59.2058787,60.3333334 C58.5966947,60.3333334 57.9704308,60.1867593 57.4010999,59.8779064 C55.7101869,58.956583 55.1465493,56.951658 56.1485718,55.3969245 C58.0558304,52.423563 58.0558304,48.7382694 56.1485718,45.7701424 C55.1465493,44.2101744 55.7101869,42.2052491 57.4010999,41.2839258 C59.1090926,40.373072 61.2839366,40.8860816 62.2745726,42.43558 C65.5026787,47.4609802 65.5026787,53.700852 62.2745726,58.7262523 C61.6141488,59.7627412 60.4242469,60.3333334 59.2058787,60.3333334 Z M69.985377,64.6666666 C69.4661637,64.6666666 68.9275406,64.5290438 68.4471472,64.2390524 C67.0059668,63.3739938 66.5255733,61.491508 67.3796061,60.0317213 C70.7811802,54.202405 70.7811802,46.957538 67.3796061,41.1331368 C66.5255733,39.6733502 67.0059668,37.7859493 68.4471472,36.9208906 C69.8931803,36.0705772 71.7468194,36.5473425 72.6008522,38.002214 C77.1330493,45.7582521 77.1330493,55.401691 72.6008522,63.1577291 C72.0331147,64.1309203 71.0238033,64.6666666 69.985377,64.6666666 Z M51.1304347,34.1786293 L51.1304347,64.8165072 C51.1304347,68.4463264 46.6931659,70.3464662 43.9263373,67.899848 L37.9530906,62.6105398 C35.8378889,60.7404813 33.0814037,59.7026738 30.2163139,59.7026738 L29.1716422,59.7026738 C26.3168957,59.7026738 24,57.4616116 24,54.689112 L24,44.3611739 C24,41.5936876 26.3168957,39.3476118 29.1716422,39.3476118 L30.1594258,39.3476118 C33.0245156,39.3476118 35.7810009,38.3098044 37.8962025,36.4397458 L43.9263373,31.1003021 C46.6931659,28.6536838 51.1304347,30.5538239 51.1304347,34.1786293 Z" id="Combined-Shape" fill="#575E75" fill-rule="nonzero"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -7,6 +7,7 @@
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
flex-basis: 160px;
|
||||
position: relative;
|
||||
height: 160px;
|
||||
max-width: 160px;
|
||||
margin: $space;
|
||||
@@ -138,7 +139,9 @@
|
||||
.featured-description {
|
||||
display: block;
|
||||
font-weight: normal;
|
||||
line-height: 2;
|
||||
line-height: 1.375rem;
|
||||
padding-top: .3125rem;
|
||||
padding-bottom: .25rem;
|
||||
}
|
||||
|
||||
.featured-extension-metadata {
|
||||
|
||||
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
import Box from '../box/box.jsx';
|
||||
import PlayButton from '../../containers/play-button.jsx';
|
||||
import styles from './library-item.css';
|
||||
import classNames from 'classnames';
|
||||
|
||||
@@ -106,8 +107,9 @@ class LibraryItemComponent extends React.PureComponent {
|
||||
) : (
|
||||
<Box
|
||||
className={classNames(
|
||||
styles.libraryItem,
|
||||
this.props.hidden ? styles.hidden : null
|
||||
styles.libraryItem, {
|
||||
[styles.hidden]: this.props.hidden
|
||||
}
|
||||
)}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
@@ -115,12 +117,16 @@ class LibraryItemComponent extends React.PureComponent {
|
||||
onClick={this.props.onClick}
|
||||
onFocus={this.props.onFocus}
|
||||
onKeyPress={this.props.onKeyPress}
|
||||
onMouseEnter={this.props.onMouseEnter}
|
||||
onMouseLeave={this.props.onMouseLeave}
|
||||
onMouseEnter={this.props.showPlayButton ? null : this.props.onMouseEnter}
|
||||
onMouseLeave={this.props.showPlayButton ? null : this.props.onMouseLeave}
|
||||
>
|
||||
{/* Layers of wrapping is to prevent layout thrashing on animation */}
|
||||
<Box className={styles.libraryItemImageContainerWrapper}>
|
||||
<Box className={styles.libraryItemImageContainer}>
|
||||
<Box
|
||||
className={styles.libraryItemImageContainer}
|
||||
onMouseEnter={this.props.showPlayButton ? this.props.onMouseEnter : null}
|
||||
onMouseLeave={this.props.showPlayButton ? this.props.onMouseLeave : null}
|
||||
>
|
||||
<img
|
||||
className={styles.libraryItemImage}
|
||||
src={this.props.iconURL}
|
||||
@@ -128,6 +134,13 @@ class LibraryItemComponent extends React.PureComponent {
|
||||
</Box>
|
||||
</Box>
|
||||
<span className={styles.libraryItemName}>{this.props.name}</span>
|
||||
{this.props.showPlayButton ? (
|
||||
<PlayButton
|
||||
isPlaying={this.props.isPlaying}
|
||||
onPlay={this.props.onPlay}
|
||||
onStop={this.props.onStop}
|
||||
/>
|
||||
) : null}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -149,6 +162,7 @@ LibraryItemComponent.propTypes = {
|
||||
iconURL: PropTypes.string,
|
||||
insetIconURL: PropTypes.string,
|
||||
internetConnectionRequired: PropTypes.bool,
|
||||
isPlaying: PropTypes.bool,
|
||||
name: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.node
|
||||
@@ -158,11 +172,15 @@ LibraryItemComponent.propTypes = {
|
||||
onFocus: PropTypes.func.isRequired,
|
||||
onKeyPress: PropTypes.func.isRequired,
|
||||
onMouseEnter: PropTypes.func.isRequired,
|
||||
onMouseLeave: PropTypes.func.isRequired
|
||||
onMouseLeave: PropTypes.func.isRequired,
|
||||
onPlay: PropTypes.func.isRequired,
|
||||
onStop: PropTypes.func.isRequired,
|
||||
showPlayButton: PropTypes.bool
|
||||
};
|
||||
|
||||
LibraryItemComponent.defaultProps = {
|
||||
disabled: false
|
||||
disabled: false,
|
||||
showPlayButton: false
|
||||
};
|
||||
|
||||
export default LibraryItemComponent;
|
||||
@@ -12,6 +12,7 @@
|
||||
overflow-y: auto;
|
||||
height: auto;
|
||||
padding: 0.5rem;
|
||||
height: calc(100% - $library-header-height);
|
||||
}
|
||||
|
||||
.library-scroll-grid.withFilterBar {
|
||||
@@ -59,3 +60,11 @@
|
||||
height: $library-filter-bar-height;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.spinner-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import Modal from '../../containers/modal.jsx';
|
||||
import Divider from '../divider/divider.jsx';
|
||||
import Filter from '../filter/filter.jsx';
|
||||
import TagButton from '../../containers/tag-button.jsx';
|
||||
import Spinner from '../spinner/spinner.jsx';
|
||||
|
||||
import styles from './library.css';
|
||||
|
||||
@@ -37,16 +38,25 @@ class LibraryComponent extends React.Component {
|
||||
'handleFilterClear',
|
||||
'handleMouseEnter',
|
||||
'handleMouseLeave',
|
||||
'handlePlayingEnd',
|
||||
'handleSelect',
|
||||
'handleTagClick',
|
||||
'setFilteredDataRef'
|
||||
]);
|
||||
this.state = {
|
||||
selectedItem: null,
|
||||
playingItem: null,
|
||||
filterQuery: '',
|
||||
selectedTag: ALL_TAG.tag
|
||||
selectedTag: ALL_TAG.tag,
|
||||
loaded: false
|
||||
};
|
||||
}
|
||||
componentDidMount () {
|
||||
// Allow the spinner to display before loading the content
|
||||
setTimeout(() => {
|
||||
this.setState({loaded: true});
|
||||
});
|
||||
if (this.props.setStopHandler) this.props.setStopHandler(this.handlePlayingEnd);
|
||||
}
|
||||
componentDidUpdate (prevProps, prevState) {
|
||||
if (prevState.filterQuery !== this.state.filterQuery ||
|
||||
prevState.selectedTag !== this.state.selectedTag) {
|
||||
@@ -61,22 +71,58 @@ class LibraryComponent extends React.Component {
|
||||
this.props.onRequestClose();
|
||||
}
|
||||
handleTagClick (tag) {
|
||||
this.setState({
|
||||
filterQuery: '',
|
||||
selectedTag: tag.toLowerCase()
|
||||
});
|
||||
if (this.state.playingItem === null) {
|
||||
this.setState({
|
||||
filterQuery: '',
|
||||
selectedTag: tag.toLowerCase()
|
||||
});
|
||||
} else {
|
||||
this.props.onItemMouseLeave(this.getFilteredData()[[this.state.playingItem]]);
|
||||
this.setState({
|
||||
filterQuery: '',
|
||||
playingItem: null,
|
||||
selectedTag: tag.toLowerCase()
|
||||
});
|
||||
}
|
||||
}
|
||||
handleMouseEnter (id) {
|
||||
if (this.props.onItemMouseEnter) this.props.onItemMouseEnter(this.getFilteredData()[id]);
|
||||
// don't restart if mouse over already playing item
|
||||
if (this.props.onItemMouseEnter && this.state.playingItem !== id) {
|
||||
this.props.onItemMouseEnter(this.getFilteredData()[id]);
|
||||
this.setState({
|
||||
playingItem: id
|
||||
});
|
||||
}
|
||||
}
|
||||
handleMouseLeave (id) {
|
||||
if (this.props.onItemMouseLeave) this.props.onItemMouseLeave(this.getFilteredData()[id]);
|
||||
if (this.props.onItemMouseLeave) {
|
||||
this.props.onItemMouseLeave(this.getFilteredData()[id]);
|
||||
this.setState({
|
||||
playingItem: null
|
||||
});
|
||||
}
|
||||
}
|
||||
handlePlayingEnd () {
|
||||
if (this.state.playingItem !== null) {
|
||||
this.setState({
|
||||
playingItem: null
|
||||
});
|
||||
}
|
||||
}
|
||||
handleFilterChange (event) {
|
||||
this.setState({
|
||||
filterQuery: event.target.value,
|
||||
selectedTag: ALL_TAG.tag
|
||||
});
|
||||
if (this.state.playingItem === null) {
|
||||
this.setState({
|
||||
filterQuery: event.target.value,
|
||||
selectedTag: ALL_TAG.tag
|
||||
});
|
||||
} else {
|
||||
this.props.onItemMouseLeave(this.getFilteredData()[[this.state.playingItem]]);
|
||||
this.setState({
|
||||
filterQuery: event.target.value,
|
||||
playingItem: null,
|
||||
selectedTag: ALL_TAG.tag
|
||||
});
|
||||
}
|
||||
}
|
||||
handleFilterClear () {
|
||||
this.setState({filterQuery: ''});
|
||||
@@ -162,7 +208,7 @@ class LibraryComponent extends React.Component {
|
||||
})}
|
||||
ref={this.setFilteredDataRef}
|
||||
>
|
||||
{this.getFilteredData().map((dataItem, index) => (
|
||||
{this.state.loaded ? this.getFilteredData().map((dataItem, index) => (
|
||||
<LibraryItem
|
||||
bluetoothRequired={dataItem.bluetoothRequired}
|
||||
collaborator={dataItem.collaborator}
|
||||
@@ -177,13 +223,22 @@ class LibraryComponent extends React.Component {
|
||||
id={index}
|
||||
insetIconURL={dataItem.insetIconURL}
|
||||
internetConnectionRequired={dataItem.internetConnectionRequired}
|
||||
key={`item_${index}`}
|
||||
isPlaying={this.state.playingItem === index}
|
||||
key={typeof dataItem.name === 'string' ? dataItem.name : dataItem.rawURL}
|
||||
name={dataItem.name}
|
||||
showPlayButton={this.props.showPlayButton}
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
onSelect={this.handleSelect}
|
||||
/>
|
||||
))}
|
||||
)) : (
|
||||
<div className={styles.spinnerWrapper}>
|
||||
<Spinner
|
||||
large
|
||||
level="primary"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
@@ -212,12 +267,15 @@ LibraryComponent.propTypes = {
|
||||
onItemMouseLeave: PropTypes.func,
|
||||
onItemSelected: PropTypes.func,
|
||||
onRequestClose: PropTypes.func,
|
||||
setStopHandler: PropTypes.func,
|
||||
showPlayButton: PropTypes.bool,
|
||||
tags: PropTypes.arrayOf(PropTypes.shape(TagButton.propTypes)),
|
||||
title: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
LibraryComponent.defaultProps = {
|
||||
filterable: true
|
||||
filterable: true,
|
||||
showPlayButton: false
|
||||
};
|
||||
|
||||
export default injectIntl(LibraryComponent);
|
||||
@@ -91,7 +91,8 @@ LoupeComponent.propTypes = {
|
||||
color: PropTypes.shape({
|
||||
r: PropTypes.number,
|
||||
g: PropTypes.number,
|
||||
b: PropTypes.number
|
||||
b: PropTypes.number,
|
||||
a: PropTypes.number
|
||||
}),
|
||||
data: PropTypes.instanceOf(Uint8Array),
|
||||
width: PropTypes.number,
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 48.1 (47250) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Feedback</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Feedback" fill="#4C97FF">
|
||||
<path d="M12.3811686,14.2770244 C12.3811686,14.3987659 12.3515563,14.506558 12.306433,14.6257633 L11.4998525,16.6002595 C11.2770558,16.478518 10.93299,16.3846755 10.4704753,16.3846755 C10.0234717,16.3846755 9.6949171,16.478518 9.47071028,16.58631 L8.66412979,14.6257633 C8.60490535,14.506558 8.59080429,14.3848164 8.59080429,14.2643429 C8.60490535,14.2503934 8.60490535,14.2237624 8.60490535,14.2098129 L8.60490535,14.1705005 C8.60490535,13.9828155 8.67964095,13.7938625 8.813601,13.6188589 C9.1576668,13.1775457 9.79926492,12.9086997 10.4859864,12.9086997 C11.1727079,12.9086997 11.8143061,13.1775457 12.1428607,13.6188589 C12.2472085,13.7684996 12.3360452,13.9409669 12.3670675,14.103289 C12.3811686,14.1565509 12.3811686,14.2098129 12.3811686,14.2643429 L12.3811686,14.2770244 Z M8.60490535,5.94153049 C8.60490535,5.50782611 8.89115681,5.12865181 9.33393002,4.89024122 C9.59056927,4.7520138 9.91771381,4.9143359 9.91771381,5.18445004 L9.91771381,11.6621167 C9.91771381,11.8168299 9.80490535,11.9550573 9.63851287,11.9918334 C9.46789007,12.0311458 9.3029077,12.0831396 9.14638596,12.1452786 C8.89115681,12.2467299 8.60490535,12.0844078 8.60490535,11.8345839 L8.60490535,5.94153049 Z M12.3670675,5.94153049 L12.3670675,11.8333158 C12.3670675,12.0780671 12.0892767,12.2505343 11.8410981,12.1516193 C11.6775258,12.0844078 11.5054929,12.032414 11.3278196,11.9931016 C11.1529665,11.9550573 11.0373378,11.8054166 11.0373378,11.6443627 L11.0373378,5.18191375 C11.0373378,4.9143359 11.3630722,4.75074565 11.6211216,4.88770493 C12.0709453,5.12611553 12.3670675,5.50528983 12.3670675,5.94153049 Z M10.4859864,1.48528137 C8.82911216,1.48528137 7.48528137,2.53149809 7.48528137,3.82119796 L7.48528137,14.2237624 C7.48528137,14.4786589 7.5148936,14.7335553 7.62065153,14.9757703 L9.1576668,18.7066425 C9.3522614,19.177123 9.87400053,19.4852814 10.4859864,19.4852814 C11.0979723,19.4852814 11.6197115,19.177123 11.8143061,18.7066425 L13.3513213,14.9757703 C13.4556692,14.7335553 13.4852814,14.4786589 13.4852814,14.2237624 L13.4852814,3.82119796 C13.4852814,2.53149809 12.1428607,1.48528137 10.4859864,1.48528137 Z" id="feedback" transform="translate(10.485281, 10.485281) rotate(45.000000) translate(-10.485281, -10.485281) "></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.7 KiB |
@@ -1,5 +1,6 @@
|
||||
import classNames from 'classnames';
|
||||
import {connect} from 'react-redux';
|
||||
import {compose} from 'redux';
|
||||
import {defineMessages, FormattedMessage, injectIntl, intlShape} from 'react-intl';
|
||||
import PropTypes from 'prop-types';
|
||||
import bindAll from 'lodash.bindall';
|
||||
@@ -27,6 +28,7 @@ import LoginDropdown from './login-dropdown.jsx';
|
||||
import SB3Downloader from '../../containers/sb3-downloader.jsx';
|
||||
import DeletionRestorer from '../../containers/deletion-restorer.jsx';
|
||||
import TurboMode from '../../containers/turbo-mode.jsx';
|
||||
import MenuBarHOC from '../../containers/menu-bar-hoc.jsx';
|
||||
|
||||
import {openTipsLibrary} from '../../reducers/modals';
|
||||
import {setPlayer} from '../../reducers/mode';
|
||||
@@ -63,7 +65,6 @@ import styles from './menu-bar.css';
|
||||
|
||||
import helpIcon from '../../lib/assets/icon--tutorials.svg';
|
||||
import mystuffIcon from './icon--mystuff.png';
|
||||
import feedbackIcon from './icon--feedback.svg';
|
||||
import profileIcon from './icon--profile.png';
|
||||
import remixIcon from './icon--remix.svg';
|
||||
import dropdownCaret from './dropdown-caret.svg';
|
||||
@@ -153,7 +154,7 @@ class MenuBar extends React.Component {
|
||||
'handleKeyPress',
|
||||
'handleLanguageMouseUp',
|
||||
'handleRestoreOption',
|
||||
'handleSaveToComputer',
|
||||
'getSaveToComputerHandler',
|
||||
'restoreOptionMessage'
|
||||
]);
|
||||
}
|
||||
@@ -164,17 +165,14 @@ class MenuBar extends React.Component {
|
||||
document.removeEventListener('keydown', this.handleKeyPress);
|
||||
}
|
||||
handleClickNew () {
|
||||
let readyToReplaceProject = true;
|
||||
// if the project is dirty, and user owns the project, we will autosave.
|
||||
// but if they are not logged in and can't save, user should consider
|
||||
// downloading or logging in first.
|
||||
// Note that if user is logged in and editing someone else's project,
|
||||
// they'll lose their work.
|
||||
if (this.props.projectChanged && !this.props.canCreateNew) {
|
||||
readyToReplaceProject = confirm( // eslint-disable-line no-alert
|
||||
this.props.intl.formatMessage(sharedMessages.replaceProjectWarning)
|
||||
);
|
||||
}
|
||||
const readyToReplaceProject = this.props.confirmReadyToReplaceProject(
|
||||
this.props.intl.formatMessage(sharedMessages.replaceProjectWarning)
|
||||
);
|
||||
this.props.onRequestCloseFile();
|
||||
if (readyToReplaceProject) {
|
||||
this.props.onClickNew(this.props.canSave && this.props.canCreateNew);
|
||||
@@ -194,8 +192,8 @@ class MenuBar extends React.Component {
|
||||
this.props.onRequestCloseFile();
|
||||
}
|
||||
handleClickSeeCommunity (waitForUpdate) {
|
||||
if (this.props.canSave) { // save before transitioning to project page
|
||||
this.props.autoUpdateProject();
|
||||
if (this.props.shouldSaveBeforeTransition()) {
|
||||
this.props.autoUpdateProject(); // save before transitioning to project page
|
||||
waitForUpdate(true); // queue the transition to project page
|
||||
} else {
|
||||
waitForUpdate(false); // immediately transition to project page
|
||||
@@ -227,7 +225,7 @@ class MenuBar extends React.Component {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
handleSaveToComputer (downloadProjectCallback) {
|
||||
getSaveToComputerHandler (downloadProjectCallback) {
|
||||
return () => {
|
||||
this.props.onRequestCloseFile();
|
||||
downloadProjectCallback();
|
||||
@@ -329,11 +327,11 @@ class MenuBar extends React.Component {
|
||||
[styles.clickable]: typeof this.props.onClickLogo !== 'undefined'
|
||||
})}
|
||||
draggable={false}
|
||||
src={scratchLogo}
|
||||
src={this.props.logo}
|
||||
onClick={this.props.onClickLogo}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
{(this.props.canChangeLanguage) && (<div
|
||||
className={classNames(styles.menuBarItem, styles.hoverable, styles.languageMenu)}
|
||||
>
|
||||
<div>
|
||||
@@ -347,84 +345,85 @@ class MenuBar extends React.Component {
|
||||
/>
|
||||
</div>
|
||||
<LanguageSelector label={this.props.intl.formatMessage(ariaMessages.language)} />
|
||||
</div>
|
||||
<div
|
||||
className={classNames(styles.menuBarItem, styles.hoverable, {
|
||||
[styles.active]: this.props.fileMenuOpen
|
||||
})}
|
||||
onMouseUp={this.props.onClickFile}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="File"
|
||||
description="Text for file dropdown menu"
|
||||
id="gui.menuBar.file"
|
||||
/>
|
||||
<MenuBarMenu
|
||||
className={classNames(styles.menuBarMenu)}
|
||||
open={this.props.fileMenuOpen}
|
||||
place={this.props.isRtl ? 'left' : 'right'}
|
||||
onRequestClose={this.props.onRequestCloseFile}
|
||||
</div>)}
|
||||
{(this.props.canManageFiles) && (
|
||||
<div
|
||||
className={classNames(styles.menuBarItem, styles.hoverable, {
|
||||
[styles.active]: this.props.fileMenuOpen
|
||||
})}
|
||||
onMouseUp={this.props.onClickFile}
|
||||
>
|
||||
<MenuSection>
|
||||
<MenuItem
|
||||
isRtl={this.props.isRtl}
|
||||
onClick={this.handleClickNew}
|
||||
>
|
||||
{newProjectMessage}
|
||||
</MenuItem>
|
||||
</MenuSection>
|
||||
{(this.props.canSave || this.props.canCreateCopy || this.props.canRemix) && (
|
||||
<FormattedMessage
|
||||
defaultMessage="File"
|
||||
description="Text for file dropdown menu"
|
||||
id="gui.menuBar.file"
|
||||
/>
|
||||
<MenuBarMenu
|
||||
className={classNames(styles.menuBarMenu)}
|
||||
open={this.props.fileMenuOpen}
|
||||
place={this.props.isRtl ? 'left' : 'right'}
|
||||
onRequestClose={this.props.onRequestCloseFile}
|
||||
>
|
||||
<MenuSection>
|
||||
{this.props.canSave ? (
|
||||
<MenuItem onClick={this.handleClickSave}>
|
||||
{saveNowMessage}
|
||||
</MenuItem>
|
||||
) : []}
|
||||
{this.props.canCreateCopy ? (
|
||||
<MenuItem onClick={this.handleClickSaveAsCopy}>
|
||||
{createCopyMessage}
|
||||
</MenuItem>
|
||||
) : []}
|
||||
{this.props.canRemix ? (
|
||||
<MenuItem onClick={this.handleClickRemix}>
|
||||
{remixMessage}
|
||||
</MenuItem>
|
||||
) : []}
|
||||
<MenuItem
|
||||
isRtl={this.props.isRtl}
|
||||
onClick={this.handleClickNew}
|
||||
>
|
||||
{newProjectMessage}
|
||||
</MenuItem>
|
||||
</MenuSection>
|
||||
)}
|
||||
<MenuSection>
|
||||
<SBFileUploader onUpdateProjectTitle={this.props.onUpdateProjectTitle}>
|
||||
{(className, renderFileInput, loadProject) => (
|
||||
{(this.props.canSave || this.props.canCreateCopy || this.props.canRemix) && (
|
||||
<MenuSection>
|
||||
{this.props.canSave && (
|
||||
<MenuItem onClick={this.handleClickSave}>
|
||||
{saveNowMessage}
|
||||
</MenuItem>
|
||||
)}
|
||||
{this.props.canCreateCopy && (
|
||||
<MenuItem onClick={this.handleClickSaveAsCopy}>
|
||||
{createCopyMessage}
|
||||
</MenuItem>
|
||||
)}
|
||||
{this.props.canRemix && (
|
||||
<MenuItem onClick={this.handleClickRemix}>
|
||||
{remixMessage}
|
||||
</MenuItem>
|
||||
)}
|
||||
</MenuSection>
|
||||
)}
|
||||
<MenuSection>
|
||||
<SBFileUploader
|
||||
canSave={this.props.canSave}
|
||||
userOwnsProject={this.props.userOwnsProject}
|
||||
>
|
||||
{(className, renderFileInput, handleLoadProject) => (
|
||||
<MenuItem
|
||||
className={className}
|
||||
onClick={handleLoadProject}
|
||||
>
|
||||
{/* eslint-disable max-len */}
|
||||
{this.props.intl.formatMessage(sharedMessages.loadFromComputerTitle)}
|
||||
{/* eslint-enable max-len */}
|
||||
{renderFileInput()}
|
||||
</MenuItem>
|
||||
)}
|
||||
</SBFileUploader>
|
||||
<SB3Downloader>{(className, downloadProjectCallback) => (
|
||||
<MenuItem
|
||||
className={className}
|
||||
onClick={loadProject}
|
||||
onClick={this.getSaveToComputerHandler(downloadProjectCallback)}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Load from your computer"
|
||||
description={
|
||||
'Menu bar item for uploading a project from your computer'
|
||||
}
|
||||
id="gui.menuBar.uploadFromComputer"
|
||||
defaultMessage="Save to your computer"
|
||||
description="Menu bar item for downloading a project to your computer" // eslint-disable-line max-len
|
||||
id="gui.menuBar.downloadToComputer"
|
||||
/>
|
||||
{renderFileInput()}
|
||||
</MenuItem>
|
||||
)}
|
||||
</SBFileUploader>
|
||||
<SB3Downloader>{(className, downloadProjectCallback) => (
|
||||
<MenuItem
|
||||
className={className}
|
||||
onClick={this.handleSaveToComputer(downloadProjectCallback)}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Save to your computer"
|
||||
description="Menu bar item for downloading a project to your computer"
|
||||
id="gui.menuBar.downloadToComputer"
|
||||
/>
|
||||
</MenuItem>
|
||||
)}</SB3Downloader>
|
||||
</MenuSection>
|
||||
</MenuBarMenu>
|
||||
</div>
|
||||
)}</SB3Downloader>
|
||||
</MenuSection>
|
||||
</MenuBarMenu>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={classNames(styles.menuBarItem, styles.hoverable, {
|
||||
[styles.active]: this.props.editMenuOpen
|
||||
@@ -495,7 +494,6 @@ class MenuBar extends React.Component {
|
||||
>
|
||||
<ProjectTitleInput
|
||||
className={classNames(styles.titleFieldGrowable)}
|
||||
onUpdateProjectTitle={this.props.onUpdateProjectTitle}
|
||||
/>
|
||||
</MenuBarItemTooltip>
|
||||
</div>
|
||||
@@ -646,25 +644,6 @@ class MenuBar extends React.Component {
|
||||
) : (
|
||||
// ******** no login session is available, so don't show login stuff
|
||||
<React.Fragment>
|
||||
<div className={classNames(styles.menuBarItem, styles.feedbackButtonWrapper)}>
|
||||
<a
|
||||
className={styles.feedbackLink}
|
||||
href="https://scratch.mit.edu/discuss/topic/312261/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<Button
|
||||
className={styles.feedbackButton}
|
||||
iconSrc={feedbackIcon}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Give Feedback"
|
||||
description="Label for feedback form modal button"
|
||||
id="gui.menuBar.giveFeedback"
|
||||
/>
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
{this.props.showComingSoon ? (
|
||||
<React.Fragment>
|
||||
<MenuBarItemTooltip id="mystuff">
|
||||
@@ -721,13 +700,16 @@ MenuBar.propTypes = {
|
||||
authorThumbnailUrl: PropTypes.string,
|
||||
authorUsername: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
|
||||
autoUpdateProject: PropTypes.func,
|
||||
canChangeLanguage: PropTypes.bool,
|
||||
canCreateCopy: PropTypes.bool,
|
||||
canCreateNew: PropTypes.bool,
|
||||
canEditTitle: PropTypes.bool,
|
||||
canManageFiles: PropTypes.bool,
|
||||
canRemix: PropTypes.bool,
|
||||
canSave: PropTypes.bool,
|
||||
canShare: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
confirmReadyToReplaceProject: PropTypes.func,
|
||||
editMenuOpen: PropTypes.bool,
|
||||
enableCommunity: PropTypes.bool,
|
||||
fileMenuOpen: PropTypes.bool,
|
||||
@@ -737,7 +719,9 @@ MenuBar.propTypes = {
|
||||
isShowingProject: PropTypes.bool,
|
||||
isUpdating: PropTypes.bool,
|
||||
languageMenuOpen: PropTypes.bool,
|
||||
locale: PropTypes.string.isRequired,
|
||||
loginMenuOpen: PropTypes.bool,
|
||||
logo: PropTypes.string,
|
||||
onClickAccount: PropTypes.func,
|
||||
onClickEdit: PropTypes.func,
|
||||
onClickFile: PropTypes.func,
|
||||
@@ -760,21 +744,22 @@ MenuBar.propTypes = {
|
||||
onSeeCommunity: PropTypes.func,
|
||||
onShare: PropTypes.func,
|
||||
onToggleLoginOpen: PropTypes.func,
|
||||
onUpdateProjectTitle: PropTypes.func,
|
||||
projectChanged: PropTypes.bool,
|
||||
projectTitle: PropTypes.string,
|
||||
renderLogin: PropTypes.func,
|
||||
sessionExists: PropTypes.bool,
|
||||
shouldSaveBeforeTransition: PropTypes.func,
|
||||
showComingSoon: PropTypes.bool,
|
||||
userOwnsProject: PropTypes.bool,
|
||||
username: PropTypes.string,
|
||||
vm: PropTypes.instanceOf(VM).isRequired
|
||||
};
|
||||
|
||||
MenuBar.defaultProps = {
|
||||
logo: scratchLogo,
|
||||
onShare: () => {}
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const loadingState = state.scratchGui.projectState.loadingState;
|
||||
const user = state.session && state.session.session && state.session.session.user;
|
||||
return {
|
||||
@@ -787,10 +772,11 @@ const mapStateToProps = state => {
|
||||
languageMenuOpen: languageMenuOpen(state),
|
||||
locale: state.locales.locale,
|
||||
loginMenuOpen: loginMenuOpen(state),
|
||||
projectChanged: state.scratchGui.projectChanged,
|
||||
projectTitle: state.scratchGui.projectTitle,
|
||||
sessionExists: state.session && typeof state.session.session !== 'undefined',
|
||||
username: user ? user.username : null,
|
||||
userOwnsProject: ownProps.authorUsername && user &&
|
||||
(ownProps.authorUsername === user.username),
|
||||
vm: state.scratchGui.vm
|
||||
};
|
||||
};
|
||||
@@ -815,7 +801,11 @@ const mapDispatchToProps = dispatch => ({
|
||||
onSeeCommunity: () => dispatch(setPlayer(true))
|
||||
});
|
||||
|
||||
export default injectIntl(connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(MenuBar));
|
||||
export default compose(
|
||||
injectIntl,
|
||||
MenuBarHOC,
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)
|
||||
)(MenuBar);
|
||||
@@ -1,9 +1,9 @@
|
||||
import classNames from 'classnames';
|
||||
import {connect} from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import bindAll from 'lodash.bindall';
|
||||
import React from 'react';
|
||||
import {defineMessages, intlShape, injectIntl} from 'react-intl';
|
||||
import {setProjectTitle} from '../../reducers/project-title';
|
||||
|
||||
import BufferedInputHOC from '../forms/buffered-input-hoc.jsx';
|
||||
import Input from '../forms/input.jsx';
|
||||
@@ -19,39 +19,27 @@ const messages = defineMessages({
|
||||
}
|
||||
});
|
||||
|
||||
class ProjectTitleInput extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'handleUpdateProjectTitle'
|
||||
]);
|
||||
}
|
||||
// call onUpdateProjectTitle if it is defined (only defined when gui
|
||||
// is used within scratch-www)
|
||||
handleUpdateProjectTitle (newTitle) {
|
||||
if (this.props.onUpdateProjectTitle) {
|
||||
this.props.onUpdateProjectTitle(newTitle);
|
||||
}
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<BufferedInput
|
||||
className={classNames(styles.titleField, this.props.className)}
|
||||
maxLength="100"
|
||||
placeholder={this.props.intl.formatMessage(messages.projectTitlePlaceholder)}
|
||||
tabIndex="0"
|
||||
type="text"
|
||||
value={this.props.projectTitle}
|
||||
onSubmit={this.handleUpdateProjectTitle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
const ProjectTitleInput = ({
|
||||
className,
|
||||
intl,
|
||||
onSubmit,
|
||||
projectTitle
|
||||
}) => (
|
||||
<BufferedInput
|
||||
className={classNames(styles.titleField, className)}
|
||||
maxLength="100"
|
||||
placeholder={intl.formatMessage(messages.projectTitlePlaceholder)}
|
||||
tabIndex="0"
|
||||
type="text"
|
||||
value={projectTitle}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
);
|
||||
|
||||
ProjectTitleInput.propTypes = {
|
||||
className: PropTypes.string,
|
||||
intl: intlShape.isRequired,
|
||||
onUpdateProjectTitle: PropTypes.func,
|
||||
onSubmit: PropTypes.func,
|
||||
projectTitle: PropTypes.string
|
||||
};
|
||||
|
||||
@@ -59,7 +47,9 @@ const mapStateToProps = state => ({
|
||||
projectTitle: state.scratchGui.projectTitle
|
||||
});
|
||||
|
||||
const mapDispatchToProps = () => ({});
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onSubmit: title => dispatch(setProjectTitle(title))
|
||||
});
|
||||
|
||||
export default injectIntl(connect(
|
||||
mapStateToProps,
|
||||
|
||||
@@ -58,7 +58,7 @@ MenuItem.propTypes = {
|
||||
|
||||
|
||||
const addDividerClassToFirstChild = (child, id) => (
|
||||
React.cloneElement(child, {
|
||||
child && React.cloneElement(child, {
|
||||
className: classNames(
|
||||
child.className,
|
||||
{[styles.menuSection]: id === 0}
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
margin: 0 5px;
|
||||
border-radius: calc($space / 2);
|
||||
padding: 0 2px;
|
||||
white-space: pre-wrap;
|
||||
transform: translateZ(0); /* Fixes flickering in Safari */
|
||||
}
|
||||
|
||||
@@ -53,6 +54,7 @@
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
white-space: pre-wrap;
|
||||
transform: translateZ(0); /* Fixes flickering in Safari */
|
||||
}
|
||||
|
||||
@@ -149,7 +151,7 @@
|
||||
overflow: hidden; /* Don't let long values escape container */
|
||||
text-overflow: ellipsis;
|
||||
user-select: text; /* Allow selecting list values for 2.0 compatibility, only relevant in player */
|
||||
white-space: nowrap;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.list-input {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="49px" height="52px" viewBox="-5 0 49 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>play</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
<path d="M17.2214651,8.34499443 C19.7766419,4.08636653 23.9234057,4.09303946 26.4745787,8.34499443 L40.1520974,31.140859 C42.7072741,35.3994869 40.7489672,38.8517869 35.7726637,38.8517869 L7.92338006,38.8517869 C2.9495089,38.8517869 0.992773429,35.392814 3.54394641,31.140859 L17.2214651,8.34499443 Z" id="path-1"></path>
|
||||
<filter x="-16.7%" y="-19.3%" width="133.5%" height="138.6%" filterUnits="objectBoundingBox" id="filter-2">
|
||||
<feMorphology radius="2.5" operator="dilate" in="SourceAlpha" result="shadowSpreadOuter1"></feMorphology>
|
||||
<feOffset dx="0" dy="0" in="shadowSpreadOuter1" result="shadowOffsetOuter1"></feOffset>
|
||||
<feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"></feComposite>
|
||||
<feColorMatrix values="0 0 0 0 0.298039216 0 0 0 0 0.592156863 0 0 0 0 1 0 0 0 0.1 0" type="matrix" in="shadowOffsetOuter1"></feColorMatrix>
|
||||
</filter>
|
||||
</defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Desktop---1280x720" transform="translate(-623.000000, -347.000000)">
|
||||
<g id="Step-3---Altering-Suggested-Trim" transform="translate(0.000000, 42.000000)">
|
||||
<g id="Play-/-Record-/-Stop" transform="translate(623.000000, 307.000000)">
|
||||
<g id="play" transform="translate(21.849131, 22.002656) rotate(90.000000) translate(-21.849131, -22.002656) ">
|
||||
<use fill="black" fill-opacity="1" filter="url(#filter-2)" xlink:href="#path-1"></use>
|
||||
<use stroke="#FFFFFF" stroke-width="1" fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-1"></use>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="52px" height="52px" viewBox="0 0 52 52" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>stop-playback</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
<rect id="path-1" x="0" y="0" width="44" height="44" rx="4"></rect>
|
||||
<filter x="-13.6%" y="-13.6%" width="127.3%" height="127.3%" filterUnits="objectBoundingBox" id="filter-2">
|
||||
<feMorphology radius="2" operator="dilate" in="SourceAlpha" result="shadowSpreadOuter1"></feMorphology>
|
||||
<feOffset dx="0" dy="0" in="shadowSpreadOuter1" result="shadowOffsetOuter1"></feOffset>
|
||||
<feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"></feComposite>
|
||||
<feColorMatrix values="0 0 0 0 0.298039216 0 0 0 0 0.592156863 0 0 0 0 1 0 0 0 0.1 0" type="matrix" in="shadowOffsetOuter1"></feColorMatrix>
|
||||
</filter>
|
||||
</defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Desktop---1280x720" transform="translate(-619.000000, -345.000000)">
|
||||
<g id="Step-4---Playing-Trim" transform="translate(0.000000, 42.000000)">
|
||||
<g id="Play-/-Record-/-Stop" transform="translate(623.000000, 307.000000)">
|
||||
<g id="stop-playback">
|
||||
<use fill="black" fill-opacity="1" filter="url(#filter-2)" xlink:href="#path-1"></use>
|
||||
<use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-1"></use>
|
||||
<rect stroke="#FFFFFF" stroke-width="1" x="0.5" y="0.5" width="43" height="43" rx="4"></rect>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,42 @@
|
||||
|
||||
@import "../../css/colors.css";
|
||||
@import "../../css/units.css";
|
||||
|
||||
.play-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
overflow: hidden; /* Mask the icon animation */
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
background-color: $sound-primary;
|
||||
color: $ui-white;
|
||||
border-radius: 50%;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease-out;
|
||||
}
|
||||
|
||||
.play-button {
|
||||
position: absolute;
|
||||
top: .5rem;
|
||||
z-index: auto;
|
||||
}
|
||||
|
||||
.play-button:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.play-icon {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
[dir="ltr"] .play-button {
|
||||
right: .5rem;
|
||||
}
|
||||
|
||||
[dir="rtl"] .play-button {
|
||||
left: .5rem;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import {defineMessages, injectIntl, intlShape} from 'react-intl';
|
||||
|
||||
import styles from './play-button.css';
|
||||
|
||||
import playIcon from './icon--play.svg';
|
||||
import stopIcon from './icon--stop.svg';
|
||||
|
||||
const messages = defineMessages({
|
||||
play: {
|
||||
id: 'gui.playButton.play',
|
||||
description: 'Title of the button to start playing the sound',
|
||||
defaultMessage: 'Play'
|
||||
},
|
||||
stop: {
|
||||
id: 'gui.playButton.stop',
|
||||
description: 'Title of the button to stop the sound',
|
||||
defaultMessage: 'Stop'
|
||||
}
|
||||
});
|
||||
|
||||
const PlayButtonComponent = ({
|
||||
className,
|
||||
intl,
|
||||
isPlaying,
|
||||
onClick,
|
||||
onMouseDown,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
setButtonRef,
|
||||
...props
|
||||
}) => {
|
||||
const label = isPlaying ?
|
||||
intl.formatMessage(messages.stop) :
|
||||
intl.formatMessage(messages.play);
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-label={label}
|
||||
className={classNames(styles.playButton, className, {
|
||||
[styles.playing]: isPlaying
|
||||
})}
|
||||
onClick={onClick}
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
ref={setButtonRef}
|
||||
{...props}
|
||||
>
|
||||
<img
|
||||
className={styles.playIcon}
|
||||
draggable={false}
|
||||
src={isPlaying ? stopIcon : playIcon}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
PlayButtonComponent.propTypes = {
|
||||
className: PropTypes.string,
|
||||
intl: intlShape,
|
||||
isPlaying: PropTypes.bool.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
onMouseDown: PropTypes.func.isRequired,
|
||||
onMouseEnter: PropTypes.func.isRequired,
|
||||
onMouseLeave: PropTypes.func.isRequired,
|
||||
setButtonRef: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(PlayButtonComponent);
|
||||
@@ -20,8 +20,10 @@
|
||||
border: 1px solid $ui-black-transparent;
|
||||
border-radius: 5px;
|
||||
padding: 3px;
|
||||
|
||||
/* Force these to be on their own render layer because they update often */
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.meter-container {
|
||||
margin-right: 5px;
|
||||
height: 180px;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 56.1 (81669) - https://sketch.com -->
|
||||
<title>Sound/General/Copy to New</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="Sound/General/Copy-to-New" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M15,11.0967026 L15,4 C15,3.44771525 14.5522847,3 14,3 L4,3 C3.44771525,3 3,3.44771525 3,4 L3,12 L12.6616037,12 L11.5236347,13.0430331 C11.1882084,13.3580017 11,13.7827132 11,14.2302292 C11,14.6767472 11.187801,15.1005394 11.5128606,15.4074032 L12.1595777,16 L4,16 C2.8954305,16 2,15.1045695 2,14 L2,4 C2,2.8954305 2.8954305,2 4,2 L14,2 C15.1045695,2 16,2.8954305 16,4 L16,12.0333493 C15.8307357,11.617882 15.4895036,11.283542 15.0505738,11.115203 C15.0337809,11.1087625 15.0169209,11.1025961 15,11.0967026 Z M10.3153901,9.43844107 C10.240142,9.43844107 10.1627842,9.41684419 10.0924589,9.37133647 C9.88359282,9.23558464 9.81397079,8.94017013 9.93774329,8.71108891 C10.173333,8.27298072 10.173333,7.72997337 9.93774329,7.2926365 C9.81397079,7.06278396 9.88359282,6.76736945 10.0924589,6.63161762 C10.3034348,6.49740842 10.5720773,6.57299751 10.6944433,6.80130741 C11.0931877,7.54177197 11.0931877,8.46118212 10.6944433,9.20164668 C10.612866,9.35436749 10.4658862,9.43844107 10.3153901,9.43844107 Z M11.6283181,10.2119951 C11.5530701,10.2119951 11.475009,10.1903983 11.405387,10.1448905 C11.1965209,10.0091387 11.1268989,9.7137242 11.2506714,9.48464298 C11.7436516,8.56986072 11.7436516,7.4329391 11.2506714,6.51892817 C11.1268989,6.28984695 11.1965209,5.99366112 11.405387,5.85790929 C11.6149563,5.7244714 11.8835989,5.79928918 12.0073714,6.02759908 C12.6642095,7.2447377 12.6642095,8.75806213 12.0073714,9.97520074 C11.9250908,10.1279216 11.7788142,10.2119951 11.6283181,10.2119951 Z M9.1892643,5.64286604 L9.1892643,10.3563857 C9.1892643,10.9148194 8.58587338,11.2071486 8.20963312,10.8307458 L7.39737611,10.0170061 C7.10974571,9.72930481 6.73491195,9.56964214 6.34530989,9.56964214 L6.20325282,9.56964214 C5.81505726,9.56964214 5.5,9.22486333 5.5,8.79832489 L5.5,7.20941137 C5.5,6.78364425 5.81505726,6.43809412 6.20325282,6.43809412 L6.33757411,6.43809412 C6.72717617,6.43809412 7.10200993,6.27843145 7.38964033,5.99073012 L8.20963312,5.16927725 C8.58587338,4.79287444 9.1892643,5.08520367 9.1892643,5.64286604 Z M13.937674,12.1868928 C14.1369962,12.0038895 14.4343459,11.9498886 14.6924846,12.0488903 C14.9506233,12.1478921 15.1172698,12.3758962 15.1172698,12.6339008 L15.1172698,13.2759122 C15.2675784,13.3119129 15.44076,13.3599137 15.6237444,13.428915 C15.9439671,13.545917 16.2968655,13.7259203 16.6236234,13.9686246 C16.7053128,14.0319257 16.7837347,14.0949268 16.8621566,14.1639281 C16.9307757,14.2239291 17.0418734,14.325931 17.0908871,14.385932 C17.2215902,14.5209344 17.3555609,14.6949375 17.4699262,14.8569404 C17.6921215,15.1989465 17.8456977,15.5679531 17.9273871,15.9186594 C18.005809,16.2699656 18.0123442,16.6029716 17.9862036,16.8789765 C17.9567954,17.1579815 17.8979789,17.3709853 17.8424301,17.5389883 C17.8162895,17.6229898 17.7934164,17.6589904 17.7803461,17.6979911 L17.7574731,17.7519921 C17.6921215,17.9109949 17.5156723,18.0159968 17.3196176,17.9979965 C17.0876195,17.9769961 16.9177054,17.7849927 16.9405785,17.5719889 L16.9471136,17.5149878 C16.9503812,17.4819873 16.9634515,17.4159861 16.960184,17.3649852 C16.960184,17.2629833 16.9569164,17.0889802 16.920973,16.8999769 C16.8817621,16.7079734 16.8098753,16.4919696 16.6922425,16.2849659 C16.5746097,16.0809623 16.4144984,15.8889588 16.2282464,15.730256 C16.1269515,15.6519546 16.0387269,15.5859534 15.921094,15.5139521 C15.8492073,15.4686513 15.8328694,15.4719514 15.774053,15.4389508 C15.7217718,15.4149504 15.6694905,15.39365 15.6172092,15.3729496 C15.4374924,15.3069485 15.2675784,15.2679478 15.1172698,15.2529475 L15.1172698,15.8259577 C15.1172698,16.0839623 14.9506233,16.3119664 14.6924846,16.4109681 C14.4343459,16.5099699 14.1369962,16.4559689 13.937674,16.2729657 L12.1993223,14.6802373 C12.0718867,14.5599351 12,14.3979322 12,14.2302292 C12,14.0619262 12.0718867,13.8999234 12.1993223,13.7802212 L13.937674,12.1868928 Z" id="Copy-to-New" fill="#4D97FF" fill-rule="nonzero"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 56.1 (81669) - https://sketch.com -->
|
||||
<title>Sound/General/Copy</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="Sound/General/Copy" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Secondary-Color" opacity="0.25" transform="translate(2.000000, 4.000000)" fill="#4D97FF">
|
||||
<rect id="selection" x="0" y="0" width="16" height="12"></rect>
|
||||
</g>
|
||||
<g id="Primary-Color" transform="translate(0.000000, 2.000000)" fill="#4D97FF" fill-rule="nonzero">
|
||||
<path d="M1.5,3.93699126 C0.637386394,3.71496986 0,2.93191971 0,2 C0,0.8954305 0.8954305,0 2,0 C3.1045695,0 4,0.8954305 4,2 C4,2.93191971 3.36261361,3.71496986 2.5,3.93699126 L2.5,12.0630087 C3.36261361,12.2850301 4,13.0680803 4,14 C4,15.1045695 3.1045695,16 2,16 C0.8954305,16 0,15.1045695 0,14 C0,13.0680803 0.637386394,12.2850301 1.5,12.0630087 L1.5,3.93699126 Z M17.5,12.0630087 L17.5,3.93699126 C16.6373864,3.71496986 16,2.93191971 16,2 C16,0.8954305 16.8954305,0 18,0 C19.1045695,0 20,0.8954305 20,2 C20,2.93191971 19.3626136,3.71496986 18.5,3.93699126 L18.5,12.0630087 C19.3626136,12.2850301 20,13.0680803 20,14 C20,15.1045695 19.1045695,16 18,16 C16.8954305,16 16,15.1045695 16,14 C16,13.0680803 16.6373864,12.2850301 17.5,12.0630087 Z M11,7 L12,7 C12.5522847,7 13,7.44771525 13,8 C13,8.55228475 12.5522847,9 12,9 L11,9 L11,10 C11,10.5522847 10.5522847,11 10,11 C9.44771525,11 9,10.5522847 9,10 L9,9 L8,9 C7.44771525,9 7,8.55228475 7,8 C7,7.44771525 7.44771525,7 8,7 L9,7 L9,6 C9,5.44771525 9.44771525,5 10,5 C10.5522847,5 11,5.44771525 11,6 L11,7 Z" id="handles-and-plus"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 55.2 (78181) - https://sketchapp.com -->
|
||||
<title>Sound/General/Trim</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
<path d="M6.54210671,14.8773591 C6.54210671,15.6756813 5.89668548,16.3230878 5.11194905,16.3230878 C4.31632862,16.3230878 3.6709074,15.6756813 3.6709074,14.8773591 C3.6709074,14.086692 4.31632862,13.440379 5.11194905,13.440379 C5.89668548,13.440379 6.54210671,14.086692 6.54210671,14.8773591 L6.54210671,14.8773591 Z M8.63401159,13.5836396 C8.82992359,13.3878866 8.82992359,13.0685577 8.63401159,12.8706175 L7.10916313,11.338495 C6.91216272,11.1405548 6.59434991,11.1405548 6.3984379,11.338495 L5.87709428,11.861232 C5.63546947,11.7912421 5.37425346,11.7595279 5.11194905,11.7595279 C3.39010019,11.7595279 2.00021333,13.1571387 2.00021333,14.8872014 C2.00021333,16.8414505 3.794985,18.3779474 5.83246988,17.9230131 C7.36929074,17.5796252 8.34776238,16.1568618 8.19973997,14.5820892 C8.18450237,14.4213311 8.15620397,14.2638539 8.11266797,14.1074702 L8.63401159,13.5836396 Z M6.54210671,5.11617453 C6.54210671,5.91559037 5.89668548,6.5629969 5.11194905,6.5629969 C4.31632862,6.5629969 3.6709074,5.91559037 3.6709074,5.11617453 C3.6709074,4.32660102 4.31632862,3.68028808 5.11194905,3.68028808 C5.89668548,3.68028808 6.54210671,4.32660102 6.54210671,5.11617453 M18.0021973,16.1706411 L17.9368933,16.235163 C16.926858,17.2500165 15.2877276,17.2500165 14.2766039,16.235163 L7.87898848,9.80702856 C7.70919807,9.6353346 7.70919807,9.3586558 7.87898848,9.18805543 L9.15785853,7.90199109 C9.32764893,7.73139072 9.60301414,7.73139072 9.77280455,7.90199109 L18.0021973,16.1706411 Z M18.0021973,3.82934469 L12.4045559,9.45368895 L10.5107398,7.54865148 L14.2766039,3.76482275 C15.2877276,2.74996927 16.926858,2.74996927 17.9368933,3.76482275 L18.0021973,3.82934469 Z M8.63401159,6.41864274 C8.82992359,6.61548933 8.82992359,6.93591182 8.63401159,7.1327584 L7.10916313,8.66488096 C6.91216272,8.86282113 6.59434991,8.86282113 6.3984379,8.66488096 L5.87709428,8.14214392 C5.63546947,8.21213382 5.37425346,8.2427544 5.11194905,8.2427544 C3.39010019,8.2427544 2.00021333,6.84623726 2.00021333,5.11617453 C2.00021333,3.16192542 3.794985,1.62542849 5.83246988,2.0814564 C7.36929074,2.42375074 8.34776238,3.84651408 8.19973997,5.42128673 C8.18450237,5.58313837 8.15620397,5.73842845 8.11266797,5.89590571 L8.63401159,6.41864274 Z M10.3929749,10.0447755 C10.3929749,10.2744298 10.2079469,10.4614341 9.97829448,10.4614341 C9.74973047,10.4614341 9.56361406,10.2744298 9.56361406,10.0447755 C9.56361406,9.81402755 9.74973047,9.62811689 9.97829448,9.62811689 C10.2079469,9.62811689 10.3929749,9.81402755 10.3929749,10.0447755 Z" id="path-1"></path>
|
||||
</defs>
|
||||
<g id="Sound/General/Trim" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<mask id="mask-2" fill="white">
|
||||
<use xlink:href="#path-1"></use>
|
||||
</mask>
|
||||
<use id="Mask" fill="#575E75" xlink:href="#path-1"></use>
|
||||
<g id="Blue/1_Blue" mask="url(#mask-2)" fill="#4D97FF">
|
||||
<rect id="Primary-Blue" x="0" y="0" width="20" height="20"></rect>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 56.2 (81672) - https://sketch.com -->
|
||||
<title>Sound/Effects/Fade-in</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="Sound/Effects/Fade-in" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M2,10.25 C1.86192881,10.25 1.75,10.1380712 1.75,10 C1.75,9.86192881 1.86192881,9.75 2,9.75 L7,9.75 C7.35324851,9.75 7.58270077,9.50876428 7.90437981,8.88536107 L8.04612021,8.6082292 C8.35582235,8.01071273 8.58718112,7.75 9,7.75 C9.3879427,7.75 9.64783618,7.89521764 10.0567568,8.24762061 L10.1299543,8.31093199 C10.5105228,8.64016497 10.7058526,8.75 11,8.75 C11.1999644,8.75 11.402585,8.39321162 11.7655628,7.41317141 C12.2359183,6.14321162 12.4592237,5.75 13,5.75 C13.4020616,5.75 13.6314717,5.8897049 13.9133418,6.21140451 C13.9679067,6.27426342 13.9973758,6.30824799 14.0201857,6.33390904 C14.2852046,6.63205537 14.5123572,6.75 15,6.75 C15.3765436,6.75 15.6926018,6.13162529 16.2628292,4.42094306 C16.9426018,2.38162529 17.2654325,1.75 18,1.75 C18.1380712,1.75 18.25,1.86192881 18.25,2 L18.25,18 C18.25,18.1380712 18.1380712,18.25 18,18.25 C17.2654325,18.25 16.9426018,17.6183747 16.2628292,15.5790569 C15.6926018,13.8683747 15.3765436,13.25 15,13.25 C14.5123572,13.25 14.2852046,13.3679446 14.0201857,13.666091 C13.9973758,13.691752 13.9679067,13.7257366 13.9133418,13.7885955 C13.6314717,14.1102951 13.4020616,14.25 13,14.25 C12.4592237,14.25 12.2359183,13.8567884 11.7655628,12.5868286 C11.402585,11.6067884 11.1999644,11.25 11,11.25 C10.7058526,11.25 10.5105228,11.359835 10.1299543,11.689068 L10.0567568,11.7523794 C9.64783618,12.1047824 9.3879427,12.25 9,12.25 C8.58718112,12.25 8.35582235,11.9892873 8.04612021,11.3917708 L7.90437981,11.1146389 C7.58270078,10.4912357 7.35324851,10.25 7.00000001,10.25 L2,10.25 Z" id="Path" fill="#CF63CF" fill-rule="nonzero"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 56.2 (81672) - https://sketch.com -->
|
||||
<title>Sound/Effects/Fade-out</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="Sound/Effects/Fade-out" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M13,10.25 C12.6467515,10.25 12.4172992,10.4912357 12.0956202,11.1146389 L11.9538798,11.3917708 C11.6441776,11.9892873 11.4128189,12.25 11,12.25 C10.6120573,12.25 10.3521638,12.1047824 9.94324322,11.7523794 L9.8700457,11.689068 C9.48947724,11.359835 9.29414743,11.25 9,11.25 C8.80003561,11.25 8.59741505,11.6067884 8.23443719,12.5868286 C7.76408171,13.8567884 7.54077635,14.25 7,14.25 C6.59793845,14.25 6.3685283,14.1102951 6.08665816,13.7885955 C6.03209329,13.7257366 6.00262416,13.691752 5.97981434,13.666091 C5.71479538,13.3679446 5.48764275,13.25 5,13.25 C4.62345642,13.25 4.30739824,13.8683747 3.73717082,15.5790569 C3.05739824,17.6183747 2.73456753,18.25 2,18.25 C1.86192881,18.25 1.75,18.1380712 1.75,18 L1.75,2 C1.75,1.86192881 1.86192881,1.75 2,1.75 C2.73456753,1.75 3.05739824,2.38162529 3.73717082,4.42094306 C4.30739824,6.13162529 4.62345642,6.75 5,6.75 C5.48764275,6.75 5.71479538,6.63205537 5.97981434,6.33390904 C6.00262416,6.30824799 6.03209329,6.27426342 6.08665816,6.21140451 C6.3685283,5.8897049 6.59793845,5.75 7,5.75 C7.54077635,5.75 7.76408171,6.14321162 8.23443719,7.41317141 C8.59741505,8.39321162 8.80003561,8.75 9,8.75 C9.29414743,8.75 9.48947724,8.64016497 9.8700457,8.31093199 L9.94324322,8.24762061 C10.3521638,7.89521764 10.6120573,7.75 11,7.75 C11.4128189,7.75 11.6441776,8.01071273 11.9538798,8.6082292 L12.0956202,8.88536107 C12.4172992,9.50876428 12.6467515,9.75 13,9.75 L18,9.75 C18.1380712,9.75 18.25,9.86192881 18.25,10 C18.25,10.1380712 18.1380712,10.25 18,10.25 L12.9999998,10.25 L13,10.25 Z" id="Path" fill="#CF63CF" fill-rule="nonzero"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>icon--lounder</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="lounder">
|
||||
<g id="louder" transform="translate(2.000000, 3.000000)">
|
||||
<path d="M7.54011246,2.3873625 L7.54011246,11.8531125 C7.54011246,12.2457375 7.22294659,12.5629875 6.83155041,12.5629875 L5.50777655,12.5629875 C5.2018577,12.5629875 4.9296799,12.3661125 4.83408026,12.0736125 C4.29422346,10.4266125 2.75788098,9.3128625 1.02471572,9.3128625 L0.709799251,9.3128625 C0.319527773,9.3128625 0.0001125,8.9933625 0.0001125,8.6029875 L0.0001125,5.6374875 C0.0001125,5.2471125 0.319527773,4.9276125 0.709799251,4.9276125 L1.02471572,4.9276125 C2.75788098,4.9276125 4.29422346,3.8138625 4.83408026,2.1668625 C4.9296799,1.8743625 5.2018577,1.6774875 5.50777655,1.6774875 L6.83155041,1.6774875 C7.22294659,1.6774875 7.54011246,1.9947375 7.54011246,2.3873625" id="Fill-1" fill="#CF63CF"></path>
|
||||
<path d="M9.25,5.8645125 C9.9385,6.5541375 9.9385,7.6858875 9.25,8.3755125" id="Stroke-3" stroke="#CF63CF" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<path d="M10.75,3.3541875 C12.829,5.4331875 12.829,8.8081875 10.75,10.8860625" id="Stroke-5" stroke="#CF63CF" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<path d="M12.5,0.8438625 C15.968375,4.3111125 15.968375,9.9293625 12.5,13.3966125" id="Stroke-7" stroke="#CF63CF" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 55.2 (78181) - https://sketchapp.com -->
|
||||
<title>Sound/Effects/Mute</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="Sound/Effects/Mute" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path fill="#CF63CF" d="M13.4541749,5.04815243 L15.6281168,2.87421054 C15.92101,2.58131732 16.3958837,2.58131732 16.688777,2.87421054 C16.9816702,3.16710376 16.9816702,3.64197749 16.688777,3.93487071 L4.53033009,16.0933176 C4.23743687,16.3862108 3.76256313,16.3862108 3.46966991,16.0933176 C3.1767767,15.8004244 3.1767767,15.3255506 3.46966991,15.0326574 L6.31214932,12.190178 C6.1241191,12.0621379 6,11.8463331 6,11.6029875 L6,8.6374875 C6,8.2471125 6.31941527,7.9276125 6.70968675,7.9276125 L7.02460322,7.9276125 C8.75776848,7.9276125 10.294111,6.8138625 10.8339678,5.1668625 C10.9295674,4.8743625 11.2017452,4.6774875 11.5076641,4.6774875 L12.8314379,4.6774875 C13.1002844,4.6774875 13.3341075,4.82717208 13.4541749,5.04815243 Z M13.54,9.08364771 L13.54,14.8531125 C13.54,15.2457375 13.2228341,15.5629875 12.8314379,15.5629875 L11.5076641,15.5629875 C11.2017452,15.5629875 10.9295674,15.3661125 10.8339678,15.0736125 C10.5773785,14.2908077 10.0956813,13.628465 9.47473531,13.1489124 L13.54,9.08364771 Z" id="path-1"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 56.1 (81669) - https://sketch.com -->
|
||||
<title>Sound/General/Paste</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="Sound/General/Paste" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M2.39776184,8.69699508 C2.82037272,8.57348907 3.12697047,8.23068036 3.44607843,7.63235294 C3.50283588,7.52593273 3.70653958,7.12084014 3.75472041,7.03050109 C4.03665487,6.50187397 4.27845388,6.25 4.66666667,6.25 C5.08719776,6.25 5.31070056,6.3254322 5.67812087,6.53210612 C5.97736723,6.7004322 6.1242348,6.75 6.44444444,6.75 C6.611898,6.75 6.70455317,6.61444047 6.85968251,6.13575567 C6.89349476,6.03068757 6.91177802,5.97409653 6.92949584,5.92094306 C7.19192893,5.1336438 7.51873661,4.75 8.22222222,4.75 C8.89163675,4.75 9.21925695,5.04485818 9.76356813,5.86132495 C10.2192569,6.54485818 10.4471923,6.75 10.8888889,6.75 C11.3661624,6.75 11.5487705,6.64281697 11.7364319,6.36132495 C11.7692531,6.31129826 11.7692531,6.31129826 11.8038172,6.25761214 C12.0332099,5.90369189 12.2433073,5.75 12.6666667,5.75 C13.1851992,5.75 13.4061497,6.04828312 13.7840084,6.89846538 C14.0728163,7.54828312 14.2222362,7.75 14.4444444,7.75 C14.7687569,7.75 14.9402848,7.69488783 15.2489243,7.52961059 L15.2905031,7.50732887 C15.6361057,7.32238348 15.857036,7.25 16.2222222,7.25 C16.6365912,7.25 16.8489777,7.46586974 17.1158999,7.96477614 C17.1606386,8.04903257 17.1606386,8.04903257 17.2045543,8.13123692 C17.3511621,8.40280419 17.4762139,8.57093888 17.631381,8.6621791 C17.7227849,8.5624959 17.8540941,8.5 18,8.5 C18.2761424,8.5 18.5,8.72385763 18.5,9 C18.5,9.27614237 18.2761424,9.5 18,9.5 C17.8540941,9.5 17.7227849,9.4375041 17.631381,9.3378209 C17.4762139,9.42906112 17.3511621,9.59719581 17.2045543,9.86876308 C17.1606386,9.95096743 17.1606386,9.95096743 17.1158999,10.0352239 C16.8489777,10.5341303 16.6365912,10.75 16.2222222,10.75 C15.857036,10.75 15.6361057,10.6776165 15.2905031,10.4926711 L15.2489243,10.4703894 C14.9402848,10.3051122 14.7687569,10.25 14.4444444,10.25 C14.2222362,10.25 14.0728163,10.4517169 13.7840084,11.1015346 C13.4061497,11.9517169 13.1851992,12.25 12.6666667,12.25 C12.2433073,12.25 12.0332099,12.0963081 11.8038172,11.7423879 C11.7692531,11.6887017 11.7692531,11.6887017 11.7364319,11.638675 C11.5487705,11.357183 11.3661624,11.25 10.8888889,11.25 C10.4471923,11.25 10.2192569,11.4551418 9.76356813,12.138675 C9.21925695,12.9551418 8.89163675,13.25 8.22222222,13.25 C7.51873661,13.25 7.19192893,12.8663562 6.92949584,12.0790569 C6.91177802,12.0259035 6.89349476,11.9693124 6.85968251,11.8642443 C6.70455317,11.3855595 6.611898,11.25 6.44444444,11.25 C6.1242348,11.25 5.97736723,11.2995678 5.67812087,11.4678939 C5.31070056,11.6745678 5.08719776,11.75 4.66666667,11.75 C4.2576716,11.75 4.07414298,11.4976192 3.8822771,10.9321841 L3.78515608,10.639352 C3.74630783,10.5239258 3.71290663,10.4315854 3.67737933,10.3436452 C3.42795496,9.72624902 3.07265238,9.37546644 2.4155666,9.27813036 C2.32583908,9.41193093 2.17320436,9.5 2,9.5 C1.72385763,9.5 1.5,9.27614237 1.5,9 C1.5,8.72385763 1.72385763,8.5 2,8.5 C2.16222865,8.5 2.306412,8.5772611 2.39776184,8.69699508 Z M9,15 L9.7763932,13.4472136 C9.80058413,13.3988317 9.83981475,13.3596011 9.8881966,13.3354102 C10.0116912,13.2736629 10.1618595,13.323719 10.2236068,13.4472136 L11,15 L12,15 C12.2761424,15 12.5,15.2238576 12.5,15.5 C12.5,15.6081851 12.4649111,15.7134519 12.4,15.8 L10.4,18.4666667 C10.2343146,18.6875806 9.9209139,18.7323521 9.7,18.5666667 C9.66209717,18.5382395 9.62842712,18.5045695 9.6,18.4666667 L7.6,15.8 C7.43431458,15.5790861 7.4790861,15.2656854 7.7,15.1 C7.78654809,15.0350889 7.89181489,15 8,15 L9,15 L9,15 Z M2,3.5 C1.72385763,3.5 1.5,3.27614237 1.5,3 C1.5,2.72385763 1.72385763,2.5 2,2.5 C2.27614237,2.5 2.5,2.72385763 2.5,3 C2.5,3.27614237 2.27614237,3.5 2,3.5 Z M4,3.5 C3.72385763,3.5 3.5,3.27614237 3.5,3 C3.5,2.72385763 3.72385763,2.5 4,2.5 C4.27614237,2.5 4.5,2.72385763 4.5,3 C4.5,3.27614237 4.27614237,3.5 4,3.5 Z M6,3.5 C5.72385763,3.5 5.5,3.27614237 5.5,3 C5.5,2.72385763 5.72385763,2.5 6,2.5 C6.27614237,2.5 6.5,2.72385763 6.5,3 C6.5,3.27614237 6.27614237,3.5 6,3.5 Z M8,3.5 C7.72385763,3.5 7.5,3.27614237 7.5,3 C7.5,2.72385763 7.72385763,2.5 8,2.5 C8.27614237,2.5 8.5,2.72385763 8.5,3 C8.5,3.27614237 8.27614237,3.5 8,3.5 Z M10,3.5 C9.72385763,3.5 9.5,3.27614237 9.5,3 C9.5,2.72385763 9.72385763,2.5 10,2.5 C10.2761424,2.5 10.5,2.72385763 10.5,3 C10.5,3.27614237 10.2761424,3.5 10,3.5 Z M12,3.5 C11.7238576,3.5 11.5,3.27614237 11.5,3 C11.5,2.72385763 11.7238576,2.5 12,2.5 C12.2761424,2.5 12.5,2.72385763 12.5,3 C12.5,3.27614237 12.2761424,3.5 12,3.5 Z M14,3.5 C13.7238576,3.5 13.5,3.27614237 13.5,3 C13.5,2.72385763 13.7238576,2.5 14,2.5 C14.2761424,2.5 14.5,2.72385763 14.5,3 C14.5,3.27614237 14.2761424,3.5 14,3.5 Z M16,3.5 C15.7238576,3.5 15.5,3.27614237 15.5,3 C15.5,2.72385763 15.7238576,2.5 16,2.5 C16.2761424,2.5 16.5,2.72385763 16.5,3 C16.5,3.27614237 16.2761424,3.5 16,3.5 Z M18,3.5 C17.7238576,3.5 17.5,3.27614237 17.5,3 C17.5,2.72385763 17.7238576,2.5 18,2.5 C18.2761424,2.5 18.5,2.72385763 18.5,3 C18.5,3.27614237 18.2761424,3.5 18,3.5 Z M18,5.5 C17.7238576,5.5 17.5,5.27614237 17.5,5 C17.5,4.72385763 17.7238576,4.5 18,4.5 C18.2761424,4.5 18.5,4.72385763 18.5,5 C18.5,Line truncated
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.5 KiB |
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="49px" height="52px" viewBox="-5 0 49 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>play</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
<path d="M17.2214651,8.34499443 C19.7766419,4.08636653 23.9234057,4.09303946 26.4745787,8.34499443 L40.1520974,31.140859 C42.7072741,35.3994869 40.7489672,38.8517869 35.7726637,38.8517869 L7.92338006,38.8517869 C2.9495089,38.8517869 0.992773429,35.392814 3.54394641,31.140859 L17.2214651,8.34499443 Z" id="path-1"></path>
|
||||
</defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Desktop---1280x720" transform="translate(-623.000000, -347.000000)">
|
||||
<g id="Step-3---Altering-Suggested-Trim" transform="translate(0.000000, 42.000000)">
|
||||
<g id="Play-/-Record-/-Stop" transform="translate(623.000000, 307.000000)">
|
||||
<g id="play" transform="translate(21.849131, 22.002656) rotate(90.000000) translate(-21.849131, -22.002656) ">
|
||||
<use stroke="#FFFFFF" stroke-width="1" fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-1"></use>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="52px" height="52px" viewBox="0 0 52 52" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>stop-playback</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
<rect id="path-1" x="0" y="0" width="44" height="44" rx="4"></rect>
|
||||
</defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Desktop---1280x720" transform="translate(-619.000000, -345.000000)">
|
||||
<g id="Step-4---Playing-Trim" transform="translate(0.000000, 42.000000)">
|
||||
<g id="Play-/-Record-/-Stop" transform="translate(623.000000, 307.000000)">
|
||||
<g id="stop-playback">
|
||||
<use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-1"></use>
|
||||
<rect stroke="#FFFFFF" stroke-width="1" x="0.5" y="0.5" width="43" height="43" rx="4"></rect>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -63,7 +63,9 @@
|
||||
background: hsla(300, 53%, 60%, 0.15);
|
||||
border: 1px solid $ui-black-transparent;
|
||||
border-radius: 5px;
|
||||
padding: 3px;
|
||||
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
$border-radius: 0.25rem;
|
||||
@@ -93,9 +95,9 @@ $border-radius: 0.25rem;
|
||||
height: 3rem;
|
||||
width: 3rem;
|
||||
outline: none;
|
||||
background: white;
|
||||
background: $motion-primary;
|
||||
border-radius: 100%;
|
||||
border: 1px solid $ui-black-transparent;
|
||||
border: 4px solid $ui-white-dim;
|
||||
cursor: pointer;
|
||||
padding: 0.75rem;
|
||||
user-select: none;
|
||||
@@ -118,6 +120,7 @@ $border-radius: 0.25rem;
|
||||
color: $text-primary;
|
||||
font-size: 0.625rem;
|
||||
user-select: none;
|
||||
|
||||
}
|
||||
|
||||
[dir="ltr"] .trim-button {
|
||||
@@ -130,7 +133,6 @@ $border-radius: 0.25rem;
|
||||
|
||||
.trim-button > img {
|
||||
width: 1.25rem;
|
||||
margin-bottom: -0.375rem;
|
||||
}
|
||||
|
||||
.effect-button {
|
||||
@@ -138,6 +140,7 @@ $border-radius: 0.25rem;
|
||||
color: $text-primary;
|
||||
font-size: 0.625rem;
|
||||
user-select: none;
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
|
||||
.effect-button + .effect-button {
|
||||
@@ -150,12 +153,25 @@ $border-radius: 0.25rem;
|
||||
margin-bottom: -0.375rem;
|
||||
}
|
||||
|
||||
/* mirror the louder/softer speaker icons when rtl */
|
||||
[dir="rtl"] .effect-button:nth-of-type(6) img {
|
||||
transform: scaleX(-1);
|
||||
.tool-button {
|
||||
flex-basis: 60px;
|
||||
color: $text-primary;
|
||||
font-size: 0.625rem;
|
||||
user-select: none;
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
|
||||
[dir="rtl"] .effect-button:nth-of-type(7) img {
|
||||
.tool-button + .tool-button {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tool-button img {
|
||||
width: 4rem;
|
||||
height: 1.5rem;
|
||||
margin-bottom: -0.375rem;
|
||||
}
|
||||
|
||||
[dir="rtl"] .flip-in-rtl img {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
@@ -167,6 +183,10 @@ $border-radius: 0.25rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.button-group .button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
@@ -8,24 +8,29 @@ import Label from '../forms/label.jsx';
|
||||
import Input from '../forms/input.jsx';
|
||||
|
||||
import BufferedInputHOC from '../forms/buffered-input-hoc.jsx';
|
||||
import AudioTrimmer from '../../containers/audio-trimmer.jsx';
|
||||
import AudioSelector from '../../containers/audio-selector.jsx';
|
||||
import IconButton from '../icon-button/icon-button.jsx';
|
||||
|
||||
import styles from './sound-editor.css';
|
||||
|
||||
import playIcon from '../record-modal/icon--play.svg';
|
||||
import stopIcon from '../record-modal/icon--stop-playback.svg';
|
||||
import trimIcon from './icon--trim.svg';
|
||||
import trimConfirmIcon from './icon--trim-confirm.svg';
|
||||
import playIcon from './icon--play.svg';
|
||||
import stopIcon from './icon--stop.svg';
|
||||
import redoIcon from './icon--redo.svg';
|
||||
import undoIcon from './icon--undo.svg';
|
||||
import echoIcon from './icon--echo.svg';
|
||||
import fasterIcon from './icon--faster.svg';
|
||||
import slowerIcon from './icon--slower.svg';
|
||||
import louderIcon from './icon--louder.svg';
|
||||
import softerIcon from './icon--softer.svg';
|
||||
import robotIcon from './icon--robot.svg';
|
||||
import reverseIcon from './icon--reverse.svg';
|
||||
import fadeOutIcon from './icon--fade-out.svg';
|
||||
import fadeInIcon from './icon--fade-in.svg';
|
||||
import muteIcon from './icon--mute.svg';
|
||||
|
||||
import deleteIcon from './icon--delete.svg';
|
||||
import copyIcon from './icon--copy.svg';
|
||||
import pasteIcon from './icon--paste.svg';
|
||||
import copyToNewIcon from './icon--copy-to-new.svg';
|
||||
|
||||
const BufferedInput = BufferedInputHOC(Input);
|
||||
|
||||
@@ -45,10 +50,25 @@ const messages = defineMessages({
|
||||
description: 'Title of the button to stop the sound',
|
||||
defaultMessage: 'Stop'
|
||||
},
|
||||
trim: {
|
||||
id: 'gui.soundEditor.trim',
|
||||
description: 'Title of the button to start trimminging the sound',
|
||||
defaultMessage: 'Trim'
|
||||
copy: {
|
||||
id: 'gui.soundEditor.copy',
|
||||
description: 'Title of the button to copy the sound',
|
||||
defaultMessage: 'Copy'
|
||||
},
|
||||
paste: {
|
||||
id: 'gui.soundEditor.paste',
|
||||
description: 'Title of the button to paste the sound',
|
||||
defaultMessage: 'Paste'
|
||||
},
|
||||
copyToNew: {
|
||||
id: 'gui.soundEditor.copyToNew',
|
||||
description: 'Title of the button to copy the selection into a new sound',
|
||||
defaultMessage: 'Copy to New'
|
||||
},
|
||||
delete: {
|
||||
id: 'gui.soundEditor.delete',
|
||||
description: 'Title of the button to delete the sound',
|
||||
defaultMessage: 'Delete'
|
||||
},
|
||||
save: {
|
||||
id: 'gui.soundEditor.save',
|
||||
@@ -99,11 +119,30 @@ const messages = defineMessages({
|
||||
id: 'gui.soundEditor.reverse',
|
||||
description: 'Title of the button to apply the reverse effect',
|
||||
defaultMessage: 'Reverse'
|
||||
},
|
||||
fadeOut: {
|
||||
id: 'gui.soundEditor.fadeOut',
|
||||
description: 'Title of the button to apply the fade out effect',
|
||||
defaultMessage: 'Fade out'
|
||||
},
|
||||
fadeIn: {
|
||||
id: 'gui.soundEditor.fadeIn',
|
||||
description: 'Title of the button to apply the fade in effect',
|
||||
defaultMessage: 'Fade in'
|
||||
},
|
||||
mute: {
|
||||
id: 'gui.soundEditor.mute',
|
||||
description: 'Title of the button to apply the mute effect',
|
||||
defaultMessage: 'Mute'
|
||||
}
|
||||
});
|
||||
|
||||
const SoundEditor = props => (
|
||||
<div className={styles.editorContainer}>
|
||||
<div
|
||||
className={styles.editorContainer}
|
||||
ref={props.setRef}
|
||||
onMouseDown={props.onContainerClick}
|
||||
>
|
||||
<div className={styles.row}>
|
||||
<div className={styles.inputGroup}>
|
||||
<Label text={props.intl.formatMessage(messages.sound)}>
|
||||
@@ -141,17 +180,33 @@ const SoundEditor = props => (
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.inputGroup}>
|
||||
<IconButton
|
||||
className={styles.toolButton}
|
||||
img={copyIcon}
|
||||
title={props.intl.formatMessage(messages.copy)}
|
||||
onClick={props.onCopy}
|
||||
/>
|
||||
<IconButton
|
||||
className={styles.toolButton}
|
||||
disabled={props.canPaste === false}
|
||||
img={pasteIcon}
|
||||
title={props.intl.formatMessage(messages.paste)}
|
||||
onClick={props.onPaste}
|
||||
/>
|
||||
<IconButton
|
||||
className={classNames(styles.toolButton, styles.flipInRtl)}
|
||||
img={copyToNewIcon}
|
||||
title={props.intl.formatMessage(messages.copyToNew)}
|
||||
onClick={props.onCopyToNew}
|
||||
/>
|
||||
</div>
|
||||
<IconButton
|
||||
className={classNames(styles.trimButton, {
|
||||
[styles.trimButtonActive]: props.trimStart !== null
|
||||
})}
|
||||
img={props.trimStart === null ? trimIcon : trimConfirmIcon}
|
||||
title={props.trimStart === null ? (
|
||||
<FormattedMessage {...messages.trim} />
|
||||
) : (
|
||||
<FormattedMessage {...messages.save} />
|
||||
)}
|
||||
onClick={props.onActivateTrim}
|
||||
className={styles.toolButton}
|
||||
disabled={props.trimStart === null}
|
||||
img={deleteIcon}
|
||||
title={props.intl.formatMessage(messages.delete)}
|
||||
onClick={props.onDelete}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.row}>
|
||||
@@ -161,12 +216,13 @@ const SoundEditor = props => (
|
||||
height={160}
|
||||
width={600}
|
||||
/>
|
||||
<AudioTrimmer
|
||||
<AudioSelector
|
||||
playhead={props.playhead}
|
||||
trimEnd={props.trimEnd}
|
||||
trimStart={props.trimStart}
|
||||
onSetTrimEnd={props.onSetTrimEnd}
|
||||
onSetTrimStart={props.onSetTrimStart}
|
||||
onPlay={props.onPlay}
|
||||
onSetTrim={props.onSetTrim}
|
||||
onStop={props.onStop}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -209,61 +265,83 @@ const SoundEditor = props => (
|
||||
onClick={props.onSlower}
|
||||
/>
|
||||
<IconButton
|
||||
className={styles.effectButton}
|
||||
img={echoIcon}
|
||||
title={<FormattedMessage {...messages.echo} />}
|
||||
onClick={props.onEcho}
|
||||
/>
|
||||
<IconButton
|
||||
className={styles.effectButton}
|
||||
img={robotIcon}
|
||||
title={<FormattedMessage {...messages.robot} />}
|
||||
onClick={props.onRobot}
|
||||
/>
|
||||
<IconButton
|
||||
className={styles.effectButton}
|
||||
disabled={props.tooLoud}
|
||||
className={classNames(styles.effectButton, styles.flipInRtl)}
|
||||
img={louderIcon}
|
||||
title={<FormattedMessage {...messages.louder} />}
|
||||
onClick={props.onLouder}
|
||||
/>
|
||||
<IconButton
|
||||
className={styles.effectButton}
|
||||
className={classNames(styles.effectButton, styles.flipInRtl)}
|
||||
img={softerIcon}
|
||||
title={<FormattedMessage {...messages.softer} />}
|
||||
onClick={props.onSofter}
|
||||
/>
|
||||
<IconButton
|
||||
className={classNames(styles.effectButton, styles.flipInRtl)}
|
||||
img={muteIcon}
|
||||
title={<FormattedMessage {...messages.mute} />}
|
||||
onClick={props.onMute}
|
||||
/>
|
||||
<IconButton
|
||||
className={styles.effectButton}
|
||||
img={fadeInIcon}
|
||||
title={<FormattedMessage {...messages.fadeIn} />}
|
||||
onClick={props.onFadeIn}
|
||||
/>
|
||||
<IconButton
|
||||
className={styles.effectButton}
|
||||
img={fadeOutIcon}
|
||||
title={<FormattedMessage {...messages.fadeOut} />}
|
||||
onClick={props.onFadeOut}
|
||||
/>
|
||||
<IconButton
|
||||
className={styles.effectButton}
|
||||
img={reverseIcon}
|
||||
title={<FormattedMessage {...messages.reverse} />}
|
||||
onClick={props.onReverse}
|
||||
/>
|
||||
<IconButton
|
||||
className={styles.effectButton}
|
||||
img={robotIcon}
|
||||
title={<FormattedMessage {...messages.robot} />}
|
||||
onClick={props.onRobot}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
SoundEditor.propTypes = {
|
||||
canPaste: PropTypes.bool.isRequired,
|
||||
canRedo: PropTypes.bool.isRequired,
|
||||
canUndo: PropTypes.bool.isRequired,
|
||||
chunkLevels: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
intl: intlShape,
|
||||
name: PropTypes.string.isRequired,
|
||||
onActivateTrim: PropTypes.func,
|
||||
onChangeName: PropTypes.func.isRequired,
|
||||
onContainerClick: PropTypes.func.isRequired,
|
||||
onCopy: PropTypes.func.isRequired,
|
||||
onCopyToNew: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func,
|
||||
onEcho: PropTypes.func.isRequired,
|
||||
onFadeIn: PropTypes.func.isRequired,
|
||||
onFadeOut: PropTypes.func.isRequired,
|
||||
onFaster: PropTypes.func.isRequired,
|
||||
onLouder: PropTypes.func.isRequired,
|
||||
onMute: PropTypes.func.isRequired,
|
||||
onPaste: PropTypes.func.isRequired,
|
||||
onPlay: PropTypes.func.isRequired,
|
||||
onRedo: PropTypes.func.isRequired,
|
||||
onReverse: PropTypes.func.isRequired,
|
||||
onRobot: PropTypes.func.isRequired,
|
||||
onSetTrimEnd: PropTypes.func,
|
||||
onSetTrimStart: PropTypes.func,
|
||||
onSetTrim: PropTypes.func,
|
||||
onSlower: PropTypes.func.isRequired,
|
||||
onSofter: PropTypes.func.isRequired,
|
||||
onStop: PropTypes.func.isRequired,
|
||||
onUndo: PropTypes.func.isRequired,
|
||||
playhead: PropTypes.number,
|
||||
setRef: PropTypes.func,
|
||||
tooLoud: PropTypes.bool.isRequired,
|
||||
trimEnd: PropTypes.number,
|
||||
trimStart: PropTypes.number
|
||||
};
|
||||
|
||||
@@ -39,6 +39,16 @@
|
||||
height: .5rem;
|
||||
}
|
||||
|
||||
.large {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
.large::before, .large::after {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
@@ -71,3 +81,10 @@
|
||||
.spinner.info::after {
|
||||
border-top-color: $ui-white;
|
||||
}
|
||||
|
||||
.spinner.primary {
|
||||
border-color: $motion-transparent;
|
||||
}
|
||||
.spinner.primary::after {
|
||||
border-top-color: $motion-primary;
|
||||
}
|
||||
@@ -8,7 +8,8 @@ const SpinnerComponent = function (props) {
|
||||
const {
|
||||
className,
|
||||
level,
|
||||
small
|
||||
small,
|
||||
large
|
||||
} = props;
|
||||
return (
|
||||
<div
|
||||
@@ -16,18 +17,24 @@ const SpinnerComponent = function (props) {
|
||||
className,
|
||||
styles.spinner,
|
||||
styles[level],
|
||||
{[styles.small]: small}
|
||||
{
|
||||
[styles.small]: small,
|
||||
[styles.large]: large
|
||||
}
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
SpinnerComponent.propTypes = {
|
||||
className: PropTypes.string,
|
||||
large: PropTypes.bool,
|
||||
level: PropTypes.string,
|
||||
small: PropTypes.bool
|
||||
};
|
||||
SpinnerComponent.defaultProps = {
|
||||
className: '',
|
||||
level: 'info'
|
||||
large: false,
|
||||
level: 'info',
|
||||
small: false
|
||||
};
|
||||
export default SpinnerComponent;
|
||||
@@ -20,7 +20,6 @@
|
||||
cursor: pointer;
|
||||
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.sprite-selector-item.is-selected {
|
||||
@@ -95,16 +94,16 @@
|
||||
|
||||
.delete-button {
|
||||
position: absolute;
|
||||
top: 0.125rem;
|
||||
top: -.625rem;
|
||||
z-index: auto;
|
||||
}
|
||||
|
||||
[dir="ltr"] .delete-button {
|
||||
right: 0.125rem;
|
||||
right: -.625rem;
|
||||
}
|
||||
|
||||
[dir="rtl"] .delete-button {
|
||||
left: 0.125rem;
|
||||
left: -.625rem;
|
||||
}
|
||||
|
||||
.number {
|
||||
|
||||
@@ -2,7 +2,7 @@ import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
import CloseButton from '../close-button/close-button.jsx';
|
||||
import DeleteButton from '../delete-button/delete-button.jsx';
|
||||
import styles from './sprite-selector-item.css';
|
||||
import {ContextMenuTrigger} from 'react-contextmenu';
|
||||
import {DangerousMenuItem, ContextMenu, MenuItem} from '../context-menu/context-menu.jsx';
|
||||
@@ -23,8 +23,9 @@ const SpriteSelectorItem = props => (
|
||||
onMouseDown: props.onMouseDown,
|
||||
onTouchStart: props.onMouseDown
|
||||
}}
|
||||
disable={props.dragging}
|
||||
disable={props.preventContextMenu}
|
||||
id={`${props.name}-${contextMenuId}`}
|
||||
ref={props.componentRef}
|
||||
>
|
||||
{typeof props.number === 'undefined' ? null : (
|
||||
<div className={styles.number}>{props.number}</div>
|
||||
@@ -47,9 +48,8 @@ const SpriteSelectorItem = props => (
|
||||
) : null}
|
||||
</div>
|
||||
{(props.selected && props.onDeleteButtonClick) ? (
|
||||
<CloseButton
|
||||
<DeleteButton
|
||||
className={styles.deleteButton}
|
||||
size={CloseButton.SIZE_SMALL}
|
||||
onClick={props.onDeleteButtonClick}
|
||||
/>
|
||||
) : null }
|
||||
@@ -89,9 +89,9 @@ const SpriteSelectorItem = props => (
|
||||
|
||||
SpriteSelectorItem.propTypes = {
|
||||
className: PropTypes.string,
|
||||
componentRef: PropTypes.func,
|
||||
costumeURL: PropTypes.string,
|
||||
details: PropTypes.string,
|
||||
dragging: PropTypes.bool,
|
||||
name: PropTypes.string.isRequired,
|
||||
number: PropTypes.number,
|
||||
onClick: PropTypes.func,
|
||||
@@ -101,6 +101,7 @@ SpriteSelectorItem.propTypes = {
|
||||
onMouseDown: PropTypes.func,
|
||||
onMouseEnter: PropTypes.func,
|
||||
onMouseLeave: PropTypes.func,
|
||||
preventContextMenu: PropTypes.bool,
|
||||
selected: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -110,7 +110,8 @@ SpriteList.propTypes = {
|
||||
editingTarget: PropTypes.string,
|
||||
hoveredTarget: PropTypes.shape({
|
||||
hoveredSprite: PropTypes.string,
|
||||
receivedBlocks: PropTypes.bool
|
||||
receivedBlocks: PropTypes.bool,
|
||||
sprite: PropTypes.string
|
||||
}),
|
||||
items: PropTypes.arrayOf(PropTypes.shape({
|
||||
costume: PropTypes.shape({
|
||||
|
||||
@@ -68,6 +68,8 @@
|
||||
padding-left: calc($space / 2);
|
||||
padding-right: calc($space / 2);
|
||||
padding-bottom: $space;
|
||||
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
|
||||
@@ -19,17 +19,21 @@ class Waveform extends React.PureComponent {
|
||||
// composite time when animating the playhead
|
||||
const takeEveryN = Math.ceil(data.length / width);
|
||||
|
||||
const filteredData = takeEveryN === 1 ? data :
|
||||
const filteredData = takeEveryN === 1 ? data.slice(0) :
|
||||
data.filter((_, i) => i % takeEveryN === 0);
|
||||
|
||||
const cappedData = [0, ...filteredData, 0];
|
||||
// Need at least two points to render waveform.
|
||||
if (filteredData.length === 1) {
|
||||
filteredData.push(filteredData[0]);
|
||||
}
|
||||
|
||||
const maxIndex = filteredData.length - 1;
|
||||
const points = [
|
||||
...cappedData.map((v, i) =>
|
||||
[width * i / cappedData.length, height * v / 2]
|
||||
...filteredData.map((v, i) =>
|
||||
[width * (i / maxIndex), height * v / 2]
|
||||
),
|
||||
...cappedData.reverse().map((v, i) =>
|
||||
[width * (cappedData.length - i - 1) / cappedData.length, -height * v / 2]
|
||||
...filteredData.reverse().map((v, i) =>
|
||||
[width * (1 - (i / maxIndex)), -height * v / 2]
|
||||
)
|
||||
];
|
||||
const pathComponents = points.map(([x, y], i) => {
|
||||
@@ -40,15 +44,8 @@ class Waveform extends React.PureComponent {
|
||||
return (
|
||||
<svg
|
||||
className={styles.container}
|
||||
viewBox={`-1 0 ${width} ${height}`}
|
||||
viewBox={`0 0 ${width} ${height}`}
|
||||
>
|
||||
<line
|
||||
className={styles.baseline}
|
||||
x1={-1}
|
||||
x2={width}
|
||||
y1={height / 2}
|
||||
y2={height / 2}
|
||||
/>
|
||||
<g transform={`scale(1, -1) translate(0, -${height / 2})`}>
|
||||
<path
|
||||
className={styles.waveformPath}
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import bindAll from 'lodash.bindall';
|
||||
import AudioSelectorComponent from '../components/audio-trimmer/audio-selector.jsx';
|
||||
import {getEventXY} from '../lib/touch-utils';
|
||||
import DragRecognizer from '../lib/drag-recognizer';
|
||||
|
||||
const MIN_LENGTH = 0.01;
|
||||
const MIN_DURATION = 500;
|
||||
|
||||
class AudioSelector extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'handleNewSelectionMouseDown',
|
||||
'handleTrimStartMouseDown',
|
||||
'handleTrimEndMouseDown',
|
||||
'handleTrimStartMouseMove',
|
||||
'handleTrimEndMouseMove',
|
||||
'handleTrimStartMouseUp',
|
||||
'handleTrimEndMouseUp',
|
||||
'storeRef'
|
||||
]);
|
||||
|
||||
this.state = {
|
||||
trimStart: props.trimStart,
|
||||
trimEnd: props.trimEnd
|
||||
};
|
||||
|
||||
this.clickStartTime = 0;
|
||||
|
||||
this.trimStartDragRecognizer = new DragRecognizer({
|
||||
onDrag: this.handleTrimStartMouseMove,
|
||||
onDragEnd: this.handleTrimStartMouseUp,
|
||||
touchDragAngle: 90,
|
||||
distanceThreshold: 0
|
||||
});
|
||||
this.trimEndDragRecognizer = new DragRecognizer({
|
||||
onDrag: this.handleTrimEndMouseMove,
|
||||
onDragEnd: this.handleTrimEndMouseUp,
|
||||
touchDragAngle: 90,
|
||||
distanceThreshold: 0
|
||||
});
|
||||
}
|
||||
componentWillReceiveProps (newProps) {
|
||||
const {trimStart, trimEnd} = this.props;
|
||||
if (newProps.trimStart === trimStart && newProps.trimEnd === trimEnd) return;
|
||||
this.setState({
|
||||
trimStart: newProps.trimStart,
|
||||
trimEnd: newProps.trimEnd
|
||||
});
|
||||
}
|
||||
clearSelection () {
|
||||
this.props.onSetTrim(null, null);
|
||||
}
|
||||
handleNewSelectionMouseDown (e) {
|
||||
const {width, left} = this.containerElement.getBoundingClientRect();
|
||||
this.initialTrimEnd = (getEventXY(e).x - left) / width;
|
||||
this.initialTrimStart = this.initialTrimEnd;
|
||||
this.props.onSetTrim(this.initialTrimStart, this.initialTrimEnd);
|
||||
|
||||
this.clickStartTime = Date.now();
|
||||
|
||||
this.containerSize = width;
|
||||
this.trimEndDragRecognizer.start(e);
|
||||
|
||||
e.preventDefault();
|
||||
}
|
||||
handleTrimStartMouseMove (currentOffset, initialOffset) {
|
||||
const dx = (currentOffset.x - initialOffset.x) / this.containerSize;
|
||||
const newTrim = Math.max(0, Math.min(1, this.initialTrimStart + dx));
|
||||
if (newTrim > this.initialTrimEnd) {
|
||||
this.setState({
|
||||
trimStart: this.initialTrimEnd,
|
||||
trimEnd: newTrim
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
trimStart: newTrim,
|
||||
trimEnd: this.initialTrimEnd
|
||||
});
|
||||
}
|
||||
}
|
||||
handleTrimEndMouseMove (currentOffset, initialOffset) {
|
||||
const dx = (currentOffset.x - initialOffset.x) / this.containerSize;
|
||||
const newTrim = Math.min(1, Math.max(0, this.initialTrimEnd + dx));
|
||||
if (newTrim < this.initialTrimStart) {
|
||||
this.setState({
|
||||
trimStart: newTrim,
|
||||
trimEnd: this.initialTrimStart
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
trimStart: this.initialTrimStart,
|
||||
trimEnd: newTrim
|
||||
});
|
||||
}
|
||||
}
|
||||
handleTrimStartMouseUp () {
|
||||
this.props.onSetTrim(this.state.trimStart, this.state.trimEnd);
|
||||
}
|
||||
handleTrimEndMouseUp () {
|
||||
// If the selection was made quickly (tooFast) and is small (tooShort),
|
||||
// deselect instead. This allows click-to-deselect even if you drag
|
||||
// a little bit by accident. It also allows very quickly making a
|
||||
// selection, as long as it is above a minimum length.
|
||||
const tooFast = (Date.now() - this.clickStartTime) < MIN_DURATION;
|
||||
const tooShort = (this.state.trimEnd - this.state.trimStart) < MIN_LENGTH;
|
||||
if (tooFast && tooShort) {
|
||||
this.clearSelection();
|
||||
} else {
|
||||
this.props.onSetTrim(this.state.trimStart, this.state.trimEnd);
|
||||
}
|
||||
}
|
||||
handleTrimStartMouseDown (e) {
|
||||
this.containerSize = this.containerElement.getBoundingClientRect().width;
|
||||
this.trimStartDragRecognizer.start(e);
|
||||
this.initialTrimStart = this.props.trimStart;
|
||||
this.initialTrimEnd = this.props.trimEnd;
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
handleTrimEndMouseDown (e) {
|
||||
this.containerSize = this.containerElement.getBoundingClientRect().width;
|
||||
this.trimEndDragRecognizer.start(e);
|
||||
this.initialTrimEnd = this.props.trimEnd;
|
||||
this.initialTrimStart = this.props.trimStart;
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
storeRef (el) {
|
||||
this.containerElement = el;
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<AudioSelectorComponent
|
||||
containerRef={this.storeRef}
|
||||
playhead={this.props.playhead}
|
||||
trimEnd={this.state.trimEnd}
|
||||
trimStart={this.state.trimStart}
|
||||
onNewSelectionMouseDown={this.handleNewSelectionMouseDown}
|
||||
onTrimEndMouseDown={this.handleTrimEndMouseDown}
|
||||
onTrimStartMouseDown={this.handleTrimStartMouseDown}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AudioSelector.propTypes = {
|
||||
onSetTrim: PropTypes.func,
|
||||
playhead: PropTypes.number,
|
||||
trimEnd: PropTypes.number,
|
||||
trimStart: PropTypes.number
|
||||
};
|
||||
|
||||
export default AudioSelector;
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import bindAll from 'lodash.bindall';
|
||||
import AudioTrimmerComponent from '../components/audio-trimmer/audio-trimmer.jsx';
|
||||
import {getEventXY} from '../lib/touch-utils';
|
||||
import DragRecognizer from '../lib/drag-recognizer';
|
||||
|
||||
const MIN_LENGTH = 0.01; // Used to stop sounds being trimmed smaller than 1%
|
||||
|
||||
@@ -14,52 +14,43 @@ class AudioTrimmer extends React.Component {
|
||||
'handleTrimEndMouseDown',
|
||||
'handleTrimStartMouseMove',
|
||||
'handleTrimEndMouseMove',
|
||||
'handleTrimStartMouseUp',
|
||||
'handleTrimEndMouseUp',
|
||||
'storeRef'
|
||||
]);
|
||||
this.trimStartDragRecognizer = new DragRecognizer({
|
||||
onDrag: this.handleTrimStartMouseMove,
|
||||
touchDragAngle: 90,
|
||||
distanceThreshold: 0
|
||||
});
|
||||
this.trimEndDragRecognizer = new DragRecognizer({
|
||||
onDrag: this.handleTrimEndMouseMove,
|
||||
touchDragAngle: 90,
|
||||
distanceThreshold: 0
|
||||
});
|
||||
|
||||
}
|
||||
handleTrimStartMouseMove (e) {
|
||||
const containerSize = this.containerElement.getBoundingClientRect().width;
|
||||
const dx = (getEventXY(e).x - this.initialX) / containerSize;
|
||||
handleTrimStartMouseMove (currentOffset, initialOffset) {
|
||||
const dx = (currentOffset.x - initialOffset.x) / this.containerSize;
|
||||
const newTrim = Math.max(0, Math.min(this.props.trimEnd - MIN_LENGTH, this.initialTrim + dx));
|
||||
this.props.onSetTrimStart(newTrim);
|
||||
e.preventDefault();
|
||||
}
|
||||
handleTrimEndMouseMove (e) {
|
||||
const containerSize = this.containerElement.getBoundingClientRect().width;
|
||||
const dx = (getEventXY(e).x - this.initialX) / containerSize;
|
||||
handleTrimEndMouseMove (currentOffset, initialOffset) {
|
||||
const dx = (currentOffset.x - initialOffset.x) / this.containerSize;
|
||||
const newTrim = Math.min(1, Math.max(this.props.trimStart + MIN_LENGTH, this.initialTrim + dx));
|
||||
this.props.onSetTrimEnd(newTrim);
|
||||
e.preventDefault();
|
||||
}
|
||||
handleTrimStartMouseUp () {
|
||||
window.removeEventListener('mousemove', this.handleTrimStartMouseMove);
|
||||
window.removeEventListener('mouseup', this.handleTrimStartMouseUp);
|
||||
window.removeEventListener('touchmove', this.handleTrimStartMouseMove);
|
||||
window.removeEventListener('touchend', this.handleTrimStartMouseUp);
|
||||
}
|
||||
handleTrimEndMouseUp () {
|
||||
window.removeEventListener('mousemove', this.handleTrimEndMouseMove);
|
||||
window.removeEventListener('mouseup', this.handleTrimEndMouseUp);
|
||||
window.removeEventListener('touchmove', this.handleTrimEndMouseMove);
|
||||
window.removeEventListener('touchend', this.handleTrimEndMouseUp);
|
||||
}
|
||||
handleTrimStartMouseDown (e) {
|
||||
this.initialX = getEventXY(e).x;
|
||||
this.containerSize = this.containerElement.getBoundingClientRect().width;
|
||||
this.trimStartDragRecognizer.start(e);
|
||||
this.initialTrim = this.props.trimStart;
|
||||
window.addEventListener('mousemove', this.handleTrimStartMouseMove);
|
||||
window.addEventListener('mouseup', this.handleTrimStartMouseUp);
|
||||
window.addEventListener('touchmove', this.handleTrimStartMouseMove);
|
||||
window.addEventListener('touchend', this.handleTrimStartMouseUp);
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
handleTrimEndMouseDown (e) {
|
||||
this.initialX = getEventXY(e).x;
|
||||
this.containerSize = this.containerElement.getBoundingClientRect().width;
|
||||
this.trimEndDragRecognizer.start(e);
|
||||
this.initialTrim = this.props.trimEnd;
|
||||
window.addEventListener('mousemove', this.handleTrimEndMouseMove);
|
||||
window.addEventListener('mouseup', this.handleTrimEndMouseUp);
|
||||
window.addEventListener('touchmove', this.handleTrimEndMouseMove);
|
||||
window.addEventListener('touchend', this.handleTrimEndMouseUp);
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
storeRef (el) {
|
||||
this.containerElement = el;
|
||||
|
||||
@@ -2,7 +2,6 @@ import bindAll from 'lodash.bindall';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import {defineMessages, injectIntl, intlShape} from 'react-intl';
|
||||
import {connect} from 'react-redux';
|
||||
import VM from 'scratch-vm';
|
||||
|
||||
import backdropLibraryContent from '../lib/libraries/backdrops.json';
|
||||
@@ -33,7 +32,7 @@ class BackdropLibrary extends React.Component {
|
||||
bitmapResolution: item.info.length > 2 ? item.info[2] : 1,
|
||||
skinId: null
|
||||
};
|
||||
this.props.vm.setEditingTarget(this.props.stageID);
|
||||
// Do not switch to stage, just add the backdrop
|
||||
this.props.vm.addBackdrop(item.md5, vmBackdrop);
|
||||
}
|
||||
render () {
|
||||
@@ -53,17 +52,7 @@ class BackdropLibrary extends React.Component {
|
||||
BackdropLibrary.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
onRequestClose: PropTypes.func,
|
||||
stageID: PropTypes.string.isRequired,
|
||||
vm: PropTypes.instanceOf(VM).isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
stageID: state.scratchGui.targets.stage.id
|
||||
});
|
||||
|
||||
const mapDispatchToProps = () => ({});
|
||||
|
||||
export default injectIntl(connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(BackdropLibrary));
|
||||
export default injectIntl(BackdropLibrary);
|
||||
@@ -83,12 +83,15 @@ class Backpack extends React.Component {
|
||||
}
|
||||
handleDrop (dragInfo) {
|
||||
let payloader = null;
|
||||
let presaveAsset = null;
|
||||
switch (dragInfo.dragType) {
|
||||
case DragConstants.COSTUME:
|
||||
payloader = costumePayload;
|
||||
presaveAsset = dragInfo.payload.asset;
|
||||
break;
|
||||
case DragConstants.SOUND:
|
||||
payloader = soundPayload;
|
||||
presaveAsset = dragInfo.payload.asset;
|
||||
break;
|
||||
case DragConstants.SPRITE:
|
||||
payloader = spritePayload;
|
||||
@@ -102,6 +105,19 @@ class Backpack extends React.Component {
|
||||
// Creating the payload is async, so set loading before starting
|
||||
this.setState({loading: true}, () => {
|
||||
payloader(dragInfo.payload, this.props.vm)
|
||||
.then(payload => {
|
||||
// Force the asset to save to the asset server before storing in backpack
|
||||
// Ensures any asset present in the backpack is also on the asset server
|
||||
if (presaveAsset && !presaveAsset.clean) {
|
||||
return storage.store(
|
||||
presaveAsset.assetType,
|
||||
presaveAsset.dataFormat,
|
||||
presaveAsset.data,
|
||||
presaveAsset.assetId
|
||||
).then(() => payload);
|
||||
}
|
||||
return payload;
|
||||
})
|
||||
.then(payload => saveBackpackObject({
|
||||
host: this.props.host,
|
||||
token: this.props.token,
|
||||
|
||||
@@ -17,6 +17,7 @@ import errorBoundaryHOC from '../lib/error-boundary-hoc.jsx';
|
||||
import {STAGE_DISPLAY_SIZES} from '../lib/layout-constants';
|
||||
import DropAreaHOC from '../lib/drop-area-hoc.jsx';
|
||||
import DragConstants from '../lib/drag-constants';
|
||||
import defineDynamicBlock from '../lib/define-dynamic-block';
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {updateToolbox} from '../reducers/toolbox';
|
||||
@@ -96,6 +97,21 @@ class Blocks extends React.Component {
|
||||
);
|
||||
this.workspace = this.ScratchBlocks.inject(this.blocks, workspaceConfig);
|
||||
|
||||
// Register buttons under new callback keys for creating variables,
|
||||
// lists, and procedures from extensions.
|
||||
|
||||
const toolboxWorkspace = this.workspace.getFlyout().getWorkspace();
|
||||
|
||||
const varListButtonCallback = type =>
|
||||
(() => this.ScratchBlocks.Variables.createVariable(this.workspace, null, type));
|
||||
const procButtonCallback = () => {
|
||||
this.ScratchBlocks.Procedures.createProcedureDefCallback_(this.workspace);
|
||||
};
|
||||
|
||||
toolboxWorkspace.registerButtonCallback('MAKE_A_VARIABLE', varListButtonCallback(''));
|
||||
toolboxWorkspace.registerButtonCallback('MAKE_A_LIST', varListButtonCallback('list'));
|
||||
toolboxWorkspace.registerButtonCallback('MAKE_A_PROCEDURE', procButtonCallback);
|
||||
|
||||
// Store the xml of the toolbox that is actually rendered.
|
||||
// This is used in componentDidUpdate instead of prevProps, because
|
||||
// the xml can change while e.g. on the costumes tab.
|
||||
@@ -324,9 +340,9 @@ class Blocks extends React.Component {
|
||||
const targetSounds = target.getSounds();
|
||||
const dynamicBlocksXML = this.props.vm.runtime.getBlocksXML();
|
||||
return makeToolboxXML(target.isStage, target.id, dynamicBlocksXML,
|
||||
targetCostumes[0].name,
|
||||
stageCostumes[0].name,
|
||||
targetSounds.length > 0 ? targetSounds[0].name : ''
|
||||
targetCostumes[targetCostumes.length - 1].name,
|
||||
stageCostumes[stageCostumes.length - 1].name,
|
||||
targetSounds.length > 0 ? targetSounds[targetSounds.length - 1].name : ''
|
||||
);
|
||||
} catch {
|
||||
return null;
|
||||
@@ -378,20 +394,50 @@ class Blocks extends React.Component {
|
||||
// workspace to be 'undone' here.
|
||||
this.workspace.clearUndo();
|
||||
}
|
||||
handleExtensionAdded (blocksInfo) {
|
||||
// select JSON from each block info object then reject the pseudo-blocks which don't have JSON, like separators
|
||||
// this actually defines blocks and MUST run regardless of the UI state
|
||||
this.ScratchBlocks.defineBlocksWithJsonArray(blocksInfo.map(blockInfo => blockInfo.json).filter(x => x));
|
||||
handleExtensionAdded (categoryInfo) {
|
||||
const defineBlocks = blockInfoArray => {
|
||||
if (blockInfoArray && blockInfoArray.length > 0) {
|
||||
const staticBlocksJson = [];
|
||||
const dynamicBlocksInfo = [];
|
||||
blockInfoArray.forEach(blockInfo => {
|
||||
if (blockInfo.info && blockInfo.info.isDynamic) {
|
||||
dynamicBlocksInfo.push(blockInfo);
|
||||
} else if (blockInfo.json) {
|
||||
staticBlocksJson.push(blockInfo.json);
|
||||
}
|
||||
// otherwise it's a non-block entry such as '---'
|
||||
});
|
||||
|
||||
// Update the toolbox with new blocks
|
||||
this.ScratchBlocks.defineBlocksWithJsonArray(staticBlocksJson);
|
||||
dynamicBlocksInfo.forEach(blockInfo => {
|
||||
// This is creating the block factory / constructor -- NOT a specific instance of the block.
|
||||
// The factory should only know static info about the block: the category info and the opcode.
|
||||
// Anything else will be picked up from the XML attached to the block instance.
|
||||
const extendedOpcode = `${categoryInfo.id}_${blockInfo.info.opcode}`;
|
||||
const blockDefinition =
|
||||
defineDynamicBlock(this.ScratchBlocks, categoryInfo, blockInfo, extendedOpcode);
|
||||
this.ScratchBlocks.Blocks[extendedOpcode] = blockDefinition;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// scratch-blocks implements a menu or custom field as a special kind of block ("shadow" block)
|
||||
// these actually define blocks and MUST run regardless of the UI state
|
||||
defineBlocks(
|
||||
Object.getOwnPropertyNames(categoryInfo.customFieldTypes)
|
||||
.map(fieldTypeName => categoryInfo.customFieldTypes[fieldTypeName].scratchBlocksDefinition));
|
||||
defineBlocks(categoryInfo.menus);
|
||||
defineBlocks(categoryInfo.blocks);
|
||||
|
||||
// Update the toolbox with new blocks if possible
|
||||
const toolboxXML = this.getToolboxXML();
|
||||
if (toolboxXML) {
|
||||
this.props.updateToolboxState(toolboxXML);
|
||||
}
|
||||
}
|
||||
handleBlocksInfoUpdate (blocksInfo) {
|
||||
handleBlocksInfoUpdate (categoryInfo) {
|
||||
// @todo Later we should replace this to avoid all the warnings from redefining blocks.
|
||||
this.handleExtensionAdded(blocksInfo);
|
||||
this.handleExtensionAdded(categoryInfo);
|
||||
}
|
||||
handleCategorySelected (categoryId) {
|
||||
const extension = extensionData.find(ext => ext.extensionId === categoryId);
|
||||
|
||||
@@ -5,6 +5,7 @@ import React from 'react';
|
||||
import {
|
||||
activateDeck,
|
||||
closeCards,
|
||||
shrinkExpandCards,
|
||||
nextStep,
|
||||
prevStep,
|
||||
dragCard,
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
|
||||
import CardsComponent from '../components/cards/cards.jsx';
|
||||
import {loadImageData} from '../lib/libraries/decks/translate-image.js';
|
||||
import {notScratchDesktop} from '../lib/isScratchDesktop';
|
||||
|
||||
class Cards extends React.Component {
|
||||
componentDidMount () {
|
||||
@@ -46,11 +48,13 @@ const mapStateToProps = state => ({
|
||||
content: state.scratchGui.cards.content,
|
||||
activeDeckId: state.scratchGui.cards.activeDeckId,
|
||||
step: state.scratchGui.cards.step,
|
||||
expanded: state.scratchGui.cards.expanded,
|
||||
x: state.scratchGui.cards.x,
|
||||
y: state.scratchGui.cards.y,
|
||||
isRtl: state.locales.isRtl,
|
||||
locale: state.locales.locale,
|
||||
dragging: state.scratchGui.cards.dragging
|
||||
dragging: state.scratchGui.cards.dragging,
|
||||
showVideos: notScratchDesktop()
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
@@ -60,6 +64,7 @@ const mapDispatchToProps = dispatch => ({
|
||||
dispatch(closeCards());
|
||||
},
|
||||
onCloseCards: () => dispatch(closeCards()),
|
||||
onShrinkExpandCards: () => dispatch(shrinkExpandCards()),
|
||||
onNextStep: () => dispatch(nextStep()),
|
||||
onPrevStep: () => dispatch(prevStep()),
|
||||
onDrag: (e_, data) => dispatch(dragCard(data.x, data.y)),
|
||||
|
||||
@@ -107,15 +107,15 @@ class ConnectionModal extends React.Component {
|
||||
render () {
|
||||
return (
|
||||
<ConnectionModalComponent
|
||||
connectingMessage={this.state.extension.connectingMessage}
|
||||
connectionIconURL={this.state.extension.connectionIconURL}
|
||||
connectionSmallIconURL={this.state.extension.connectionSmallIconURL}
|
||||
connectionTipIconURL={this.state.extension.connectionTipIconURL}
|
||||
connectingMessage={this.state.extension && this.state.extension.connectingMessage}
|
||||
connectionIconURL={this.state.extension && this.state.extension.connectionIconURL}
|
||||
connectionSmallIconURL={this.state.extension && this.state.extension.connectionSmallIconURL}
|
||||
connectionTipIconURL={this.state.extension && this.state.extension.connectionTipIconURL}
|
||||
extensionId={this.props.extensionId}
|
||||
name={this.state.extension.name}
|
||||
name={this.state.extension && this.state.extension.name}
|
||||
phase={this.state.phase}
|
||||
title={this.props.extensionId}
|
||||
useAutoScan={this.state.extension.useAutoScan}
|
||||
useAutoScan={this.state.extension && this.state.extension.useAutoScan}
|
||||
vm={this.props.vm}
|
||||
onCancel={this.handleCancel}
|
||||
onConnected={this.handleConnected}
|
||||
|
||||
@@ -4,14 +4,13 @@ import {compose} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
import ReactModal from 'react-modal';
|
||||
import VM from 'scratch-vm';
|
||||
import {defineMessages, injectIntl, intlShape} from 'react-intl';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
|
||||
import ErrorBoundaryHOC from '../lib/error-boundary-hoc.jsx';
|
||||
import {
|
||||
getIsError,
|
||||
getIsShowingProject
|
||||
} from '../reducers/project-state';
|
||||
import {setProjectTitle} from '../reducers/project-title';
|
||||
import {
|
||||
activateTab,
|
||||
BLOCKS_TAB_INDEX,
|
||||
@@ -29,6 +28,7 @@ import {
|
||||
import FontLoaderHOC from '../lib/font-loader-hoc.jsx';
|
||||
import LocalizationHOC from '../lib/localization-hoc.jsx';
|
||||
import ProjectFetcherHOC from '../lib/project-fetcher-hoc.jsx';
|
||||
import TitledHOC from '../lib/titled-hoc.jsx';
|
||||
import ProjectSaverHOC from '../lib/project-saver-hoc.jsx';
|
||||
import QueryParserHOC from '../lib/query-parser-hoc.jsx';
|
||||
import storage from '../lib/storage';
|
||||
@@ -39,42 +39,22 @@ import cloudManagerHOC from '../lib/cloud-manager-hoc.jsx';
|
||||
import GUIComponent from '../components/gui/gui.jsx';
|
||||
import {setIsScratchDesktop} from '../lib/isScratchDesktop.js';
|
||||
|
||||
const messages = defineMessages({
|
||||
defaultProjectTitle: {
|
||||
id: 'gui.gui.defaultProjectTitle',
|
||||
description: 'Default title for project',
|
||||
defaultMessage: 'Scratch Project'
|
||||
}
|
||||
});
|
||||
|
||||
class GUI extends React.Component {
|
||||
componentDidMount () {
|
||||
setIsScratchDesktop(this.props.isScratchDesktop);
|
||||
this.setReduxTitle(this.props.projectTitle);
|
||||
this.props.onStorageInit(storage);
|
||||
this.props.onVmInit(this.props.vm);
|
||||
}
|
||||
componentDidUpdate (prevProps) {
|
||||
if (this.props.projectId !== prevProps.projectId && this.props.projectId !== null) {
|
||||
this.props.onUpdateProjectId(this.props.projectId);
|
||||
}
|
||||
if (this.props.projectTitle !== prevProps.projectTitle) {
|
||||
this.setReduxTitle(this.props.projectTitle);
|
||||
}
|
||||
if (this.props.isShowingProject && !prevProps.isShowingProject) {
|
||||
// this only notifies container when a project changes from not yet loaded to loaded
|
||||
// At this time the project view in www doesn't need to know when a project is unloaded
|
||||
this.props.onProjectLoaded();
|
||||
}
|
||||
}
|
||||
setReduxTitle (newTitle) {
|
||||
if (newTitle === null || typeof newTitle === 'undefined') {
|
||||
this.props.onUpdateReduxProjectTitle(
|
||||
this.props.intl.formatMessage(messages.defaultProjectTitle)
|
||||
);
|
||||
} else {
|
||||
this.props.onUpdateReduxProjectTitle(newTitle);
|
||||
}
|
||||
}
|
||||
render () {
|
||||
if (this.props.isError) {
|
||||
throw new Error(
|
||||
@@ -91,10 +71,9 @@ class GUI extends React.Component {
|
||||
onProjectLoaded,
|
||||
onStorageInit,
|
||||
onUpdateProjectId,
|
||||
onUpdateReduxProjectTitle,
|
||||
onVmInit,
|
||||
projectHost,
|
||||
projectId,
|
||||
projectTitle,
|
||||
/* eslint-enable no-unused-vars */
|
||||
children,
|
||||
fetchingProject,
|
||||
@@ -129,11 +108,9 @@ GUI.propTypes = {
|
||||
onSeeCommunity: PropTypes.func,
|
||||
onStorageInit: PropTypes.func,
|
||||
onUpdateProjectId: PropTypes.func,
|
||||
onUpdateProjectTitle: PropTypes.func,
|
||||
onUpdateReduxProjectTitle: PropTypes.func,
|
||||
onVmInit: PropTypes.func,
|
||||
projectHost: PropTypes.string,
|
||||
projectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
projectTitle: PropTypes.string,
|
||||
telemetryModalVisible: PropTypes.bool,
|
||||
vm: PropTypes.instanceOf(VM).isRequired
|
||||
};
|
||||
@@ -142,7 +119,8 @@ GUI.defaultProps = {
|
||||
isScratchDesktop: false,
|
||||
onStorageInit: storageInstance => storageInstance.addOfficialScratchWebStores(),
|
||||
onProjectLoaded: () => {},
|
||||
onUpdateProjectId: () => {}
|
||||
onUpdateProjectId: () => {},
|
||||
onVmInit: (/* vm */) => {}
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
@@ -182,8 +160,7 @@ const mapDispatchToProps = dispatch => ({
|
||||
onActivateSoundsTab: () => dispatch(activateTab(SOUNDS_TAB_INDEX)),
|
||||
onRequestCloseBackdropLibrary: () => dispatch(closeBackdropLibrary()),
|
||||
onRequestCloseCostumeLibrary: () => dispatch(closeCostumeLibrary()),
|
||||
onRequestCloseTelemetryModal: () => dispatch(closeTelemetryModal()),
|
||||
onUpdateReduxProjectTitle: title => dispatch(setProjectTitle(title))
|
||||
onRequestCloseTelemetryModal: () => dispatch(closeTelemetryModal())
|
||||
});
|
||||
|
||||
const ConnectedGUI = injectIntl(connect(
|
||||
@@ -200,6 +177,7 @@ const WrappedGui = compose(
|
||||
FontLoaderHOC,
|
||||
QueryParserHOC,
|
||||
ProjectFetcherHOC,
|
||||
TitledHOC,
|
||||
ProjectSaverHOC,
|
||||
vmListenerHOC,
|
||||
vmManagerHOC,
|
||||
|
||||
@@ -15,6 +15,8 @@ class LibraryItem extends React.PureComponent {
|
||||
'handleKeyPress',
|
||||
'handleMouseEnter',
|
||||
'handleMouseLeave',
|
||||
'handlePlay',
|
||||
'handleStop',
|
||||
'rotateIcon',
|
||||
'startRotatingIcons',
|
||||
'stopRotatingIcons'
|
||||
@@ -37,7 +39,9 @@ class LibraryItem extends React.PureComponent {
|
||||
e.preventDefault();
|
||||
}
|
||||
handleFocus (id) {
|
||||
this.handleMouseEnter(id);
|
||||
if (!this.props.showPlayButton) {
|
||||
this.handleMouseEnter(id);
|
||||
}
|
||||
}
|
||||
handleKeyPress (e) {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
@@ -46,22 +50,34 @@ class LibraryItem extends React.PureComponent {
|
||||
}
|
||||
}
|
||||
handleMouseEnter () {
|
||||
this.props.onMouseEnter(this.props.id);
|
||||
if (this.props.icons && this.props.icons.length) {
|
||||
this.stopRotatingIcons();
|
||||
this.setState({
|
||||
isRotatingIcon: true
|
||||
}, this.startRotatingIcons);
|
||||
// only show hover effects on the item if not showing a play button
|
||||
if (!this.props.showPlayButton) {
|
||||
this.props.onMouseEnter(this.props.id);
|
||||
if (this.props.icons && this.props.icons.length) {
|
||||
this.stopRotatingIcons();
|
||||
this.setState({
|
||||
isRotatingIcon: true
|
||||
}, this.startRotatingIcons);
|
||||
}
|
||||
}
|
||||
}
|
||||
handleMouseLeave () {
|
||||
this.props.onMouseLeave(this.props.id);
|
||||
if (this.props.icons && this.props.icons.length) {
|
||||
this.setState({
|
||||
isRotatingIcon: false
|
||||
}, this.stopRotatingIcons);
|
||||
// only show hover effects on the item if not showing a play button
|
||||
if (!this.props.showPlayButton) {
|
||||
this.props.onMouseLeave(this.props.id);
|
||||
if (this.props.icons && this.props.icons.length) {
|
||||
this.setState({
|
||||
isRotatingIcon: false
|
||||
}, this.stopRotatingIcons);
|
||||
}
|
||||
}
|
||||
}
|
||||
handlePlay () {
|
||||
this.props.onMouseEnter(this.props.id);
|
||||
}
|
||||
handleStop () {
|
||||
this.props.onMouseLeave(this.props.id);
|
||||
}
|
||||
startRotatingIcons () {
|
||||
this.rotateIcon();
|
||||
this.intervalId = setInterval(this.rotateIcon, 300);
|
||||
@@ -104,13 +120,17 @@ class LibraryItem extends React.PureComponent {
|
||||
id={this.props.id}
|
||||
insetIconURL={this.props.insetIconURL}
|
||||
internetConnectionRequired={this.props.internetConnectionRequired}
|
||||
isPlaying={this.props.isPlaying}
|
||||
name={this.props.name}
|
||||
showPlayButton={this.props.showPlayButton}
|
||||
onBlur={this.handleBlur}
|
||||
onClick={this.handleClick}
|
||||
onFocus={this.handleFocus}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
onPlay={this.handlePlay}
|
||||
onStop={this.handleStop}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -137,13 +157,15 @@ LibraryItem.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
insetIconURL: PropTypes.string,
|
||||
internetConnectionRequired: PropTypes.bool,
|
||||
isPlaying: PropTypes.bool,
|
||||
name: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.node
|
||||
]),
|
||||
onMouseEnter: PropTypes.func.isRequired,
|
||||
onMouseLeave: PropTypes.func.isRequired,
|
||||
onSelect: PropTypes.func.isRequired
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
showPlayButton: PropTypes.bool
|
||||
};
|
||||
|
||||
export default injectIntl(LibraryItem);
|
||||
@@ -0,0 +1,66 @@
|
||||
import {connect} from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import bindAll from 'lodash.bindall';
|
||||
import React from 'react';
|
||||
|
||||
const MenuBarHOC = function (WrappedComponent) {
|
||||
class MenuBarContainer extends React.PureComponent {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
bindAll(this, [
|
||||
'confirmReadyToReplaceProject',
|
||||
'shouldSaveBeforeTransition'
|
||||
]);
|
||||
}
|
||||
confirmReadyToReplaceProject (message) {
|
||||
let readyToReplaceProject = true;
|
||||
if (this.props.projectChanged && !this.props.canCreateNew) {
|
||||
readyToReplaceProject = this.props.confirmWithMessage(message);
|
||||
}
|
||||
return readyToReplaceProject;
|
||||
}
|
||||
shouldSaveBeforeTransition () {
|
||||
return (this.props.canSave && this.props.projectChanged);
|
||||
}
|
||||
render () {
|
||||
const {
|
||||
/* eslint-disable no-unused-vars */
|
||||
projectChanged,
|
||||
/* eslint-enable no-unused-vars */
|
||||
...props
|
||||
} = this.props;
|
||||
return (<WrappedComponent
|
||||
confirmReadyToReplaceProject={this.confirmReadyToReplaceProject}
|
||||
shouldSaveBeforeTransition={this.shouldSaveBeforeTransition}
|
||||
{...props}
|
||||
/>);
|
||||
}
|
||||
}
|
||||
|
||||
MenuBarContainer.propTypes = {
|
||||
canCreateNew: PropTypes.bool,
|
||||
canSave: PropTypes.bool,
|
||||
confirmWithMessage: PropTypes.func,
|
||||
projectChanged: PropTypes.bool
|
||||
};
|
||||
MenuBarContainer.defaultProps = {
|
||||
// default to using standard js confirm
|
||||
confirmWithMessage: message => (confirm(message)) // eslint-disable-line no-alert
|
||||
};
|
||||
const mapStateToProps = state => ({
|
||||
projectChanged: state.scratchGui.projectChanged
|
||||
});
|
||||
const mapDispatchToProps = () => ({});
|
||||
// Allow incoming props to override redux-provided props. Used to mock in tests.
|
||||
const mergeProps = (stateProps, dispatchProps, ownProps) => Object.assign(
|
||||
{}, stateProps, dispatchProps, ownProps
|
||||
);
|
||||
return connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
mergeProps
|
||||
)(MenuBarContainer);
|
||||
};
|
||||
|
||||
export default MenuBarHOC;
|
||||
@@ -0,0 +1,115 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import bindAll from 'lodash.bindall';
|
||||
|
||||
import PlayButtonComponent from '../components/play-button/play-button.jsx';
|
||||
|
||||
class PlayButton extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'handleClick',
|
||||
'handleMouseDown',
|
||||
'handleMouseEnter',
|
||||
'handleMouseLeave',
|
||||
'handleTouchStart',
|
||||
'setButtonRef'
|
||||
]);
|
||||
this.state = {
|
||||
touchStarted: false
|
||||
};
|
||||
}
|
||||
getDerivedStateFromProps (props, state) {
|
||||
// if touchStarted is true and it's not playing, the sound must have ended.
|
||||
// reset the touchStarted state to allow the sound to be replayed
|
||||
if (state.touchStarted && !props.isPlaying) {
|
||||
return {
|
||||
touchStarted: false
|
||||
};
|
||||
}
|
||||
return null; // nothing changed
|
||||
}
|
||||
componentDidMount () {
|
||||
// Touch start
|
||||
this.buttonRef.addEventListener('touchstart', this.handleTouchStart);
|
||||
}
|
||||
componentWillUnmount () {
|
||||
this.buttonRef.removeEventListener('touchstart', this.handleTouchStart);
|
||||
}
|
||||
handleClick (e) {
|
||||
// stop the click from propagating out of the button
|
||||
e.stopPropagation();
|
||||
}
|
||||
handleMouseDown (e) {
|
||||
// prevent default (focus) on mouseDown
|
||||
e.preventDefault();
|
||||
if (this.props.isPlaying) {
|
||||
// stop sound and reset touch state
|
||||
this.props.onStop();
|
||||
if (this.state.touchstarted) this.setState({touchStarted: false});
|
||||
} else {
|
||||
this.props.onPlay();
|
||||
if (this.state.touchstarted) {
|
||||
// started on touch, but now clicked mouse
|
||||
this.setState({touchStarted: false});
|
||||
}
|
||||
}
|
||||
}
|
||||
handleTouchStart (e) {
|
||||
if (this.props.isPlaying) {
|
||||
// If playing, stop sound, and reset touch state
|
||||
e.preventDefault();
|
||||
this.setState({touchStarted: false});
|
||||
this.props.onStop();
|
||||
} else {
|
||||
// otherwise start playing, and set touch state
|
||||
e.preventDefault();
|
||||
this.setState({touchStarted: true});
|
||||
this.props.onPlay();
|
||||
}
|
||||
}
|
||||
handleMouseEnter (e) {
|
||||
// start the sound if it's not already playing
|
||||
e.preventDefault();
|
||||
if (!this.props.isPlaying) {
|
||||
this.props.onPlay();
|
||||
}
|
||||
}
|
||||
handleMouseLeave () {
|
||||
// stop the sound unless it was started by touch
|
||||
if (this.props.isPlaying && !this.state.touchstarted) {
|
||||
this.props.onStop();
|
||||
}
|
||||
}
|
||||
setButtonRef (ref) {
|
||||
this.buttonRef = ref;
|
||||
}
|
||||
render () {
|
||||
const {
|
||||
className,
|
||||
isPlaying,
|
||||
onPlay, // eslint-disable-line no-unused-vars
|
||||
onStop // eslint-disable-line no-unused-vars
|
||||
} = this.props;
|
||||
return (
|
||||
<PlayButtonComponent
|
||||
className={className}
|
||||
isPlaying={isPlaying}
|
||||
onClick={this.handleClick}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
setButtonRef={this.setButtonRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PlayButton.propTypes = {
|
||||
className: PropTypes.string,
|
||||
isPlaying: PropTypes.bool.isRequired,
|
||||
onPlay: PropTypes.func.isRequired,
|
||||
onStop: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default PlayButton;
|
||||
@@ -2,8 +2,8 @@ import bindAll from 'lodash.bindall';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import VM from 'scratch-vm';
|
||||
import WavEncoder from 'wav-encoder';
|
||||
import {connect} from 'react-redux';
|
||||
import {encodeAndAddSoundToVM} from '../lib/audio/audio-util.js';
|
||||
|
||||
import RecordModalComponent from '../components/record-modal/record-modal.jsx';
|
||||
|
||||
@@ -71,39 +71,12 @@ class RecordModal extends React.Component {
|
||||
const startIndex = Math.floor(this.state.trimStart * sampleCount);
|
||||
const endIndex = Math.floor(this.state.trimEnd * sampleCount);
|
||||
const clippedSamples = this.state.samples.slice(startIndex, endIndex);
|
||||
WavEncoder.encode({
|
||||
sampleRate: this.state.sampleRate,
|
||||
channelData: [clippedSamples]
|
||||
}).then(wavBuffer => {
|
||||
const vmSound = {
|
||||
format: '',
|
||||
dataFormat: 'wav',
|
||||
rate: this.state.sampleRate,
|
||||
sampleCount: clippedSamples.length
|
||||
};
|
||||
|
||||
// Create an asset from the encoded .wav and get resulting md5
|
||||
const storage = this.props.vm.runtime.storage;
|
||||
vmSound.asset = storage.createAsset(
|
||||
storage.AssetType.Sound,
|
||||
storage.DataFormat.WAV,
|
||||
new Uint8Array(wavBuffer),
|
||||
null,
|
||||
true // generate md5
|
||||
);
|
||||
vmSound.assetId = vmSound.asset.assetId;
|
||||
|
||||
// update vmSound object with md5 property
|
||||
vmSound.md5 = `${vmSound.assetId}.${vmSound.dataFormat}`;
|
||||
// The VM will update the sound name to a fresh name
|
||||
// if the following is already taken
|
||||
vmSound.name = 'recording1';
|
||||
|
||||
this.props.vm.addSound(vmSound).then(() => {
|
||||
encodeAndAddSoundToVM(this.props.vm, clippedSamples, this.state.sampleRate, 'recording1',
|
||||
() => {
|
||||
this.props.onClose();
|
||||
this.props.onNewSound();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
handleCancel () {
|
||||
|
||||
@@ -44,10 +44,10 @@ class RecordingStep extends React.Component {
|
||||
alert(this.props.intl.formatMessage(messages.alertMsg)); // eslint-disable-line no-alert
|
||||
}
|
||||
handleLevelUpdate (level) {
|
||||
this.setState({level});
|
||||
if (this.props.recording) {
|
||||
this.setState({levels: (this.state.levels || []).concat([level])});
|
||||
}
|
||||
this.setState({
|
||||
level: level,
|
||||
levels: this.props.recording ? (this.state.levels || []).concat([level]) : this.state.levels
|
||||
});
|
||||
}
|
||||
handleRecord () {
|
||||
this.audioRecorder.startRecording();
|
||||
|
||||
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {defineMessages, injectIntl, intlShape} from 'react-intl';
|
||||
import {setProjectTitle} from '../reducers/project-title';
|
||||
|
||||
import log from '../lib/log';
|
||||
import sharedMessages from '../lib/shared-messages';
|
||||
@@ -26,13 +27,14 @@ import {
|
||||
/**
|
||||
* SBFileUploader component passes a file input, load handler and props to its child.
|
||||
* It expects this child to be a function with the signature
|
||||
* function (renderFileInput, loadProject) {}
|
||||
* function (renderFileInput, handleLoadProject) {}
|
||||
* The component can then be used to attach project loading functionality
|
||||
* to any other component:
|
||||
*
|
||||
* <SBFileUploader>{(renderFileInput, loadProject) => (
|
||||
* <SBFileUploader>{(className, renderFileInput, handleLoadProject) => (
|
||||
* <MyCoolComponent
|
||||
* onClick={loadProject}
|
||||
* className={className}
|
||||
* onClick={handleLoadProject}
|
||||
* >
|
||||
* {renderFileInput()}
|
||||
* </MyCoolComponent>
|
||||
@@ -94,21 +96,28 @@ class SBFileUploader extends React.Component {
|
||||
intl,
|
||||
isShowingWithoutId,
|
||||
loadingState,
|
||||
projectChanged
|
||||
projectChanged,
|
||||
userOwnsProject
|
||||
} = this.props;
|
||||
|
||||
const thisFileInput = e.target;
|
||||
if (thisFileInput.files) { // Don't attempt to load if no file was selected
|
||||
this.fileToUpload = thisFileInput.files[0];
|
||||
|
||||
// Allow upload to continue only after confirmation if the project
|
||||
// has changed and is not showing with ID. If it has an ID, this operation
|
||||
// does not currently overwrite that project, so it is safe to do without confirmation.
|
||||
const uploadAllowed = (isShowingWithoutId && projectChanged) ?
|
||||
confirm(intl.formatMessage(sharedMessages.replaceProjectWarning)) : // eslint-disable-line no-alert
|
||||
true;
|
||||
|
||||
if (uploadAllowed) this.props.requestProjectUpload(loadingState);
|
||||
// If user owns the project, or user has changed the project,
|
||||
// we must confirm with the user that they really intend to replace it.
|
||||
// (If they don't own the project and haven't changed it, no need to confirm.)
|
||||
let uploadAllowed = true;
|
||||
if (userOwnsProject || (projectChanged && isShowingWithoutId)) {
|
||||
uploadAllowed = confirm( // eslint-disable-line no-alert
|
||||
intl.formatMessage(sharedMessages.replaceProjectWarning)
|
||||
);
|
||||
}
|
||||
if (uploadAllowed) {
|
||||
this.props.requestProjectUpload(loadingState);
|
||||
} else {
|
||||
this.props.closeFileMenu();
|
||||
}
|
||||
}
|
||||
}
|
||||
// called when file upload raw data is available in the reader
|
||||
@@ -118,18 +127,12 @@ class SBFileUploader extends React.Component {
|
||||
const filename = this.fileToUpload && this.fileToUpload.name;
|
||||
this.props.vm.loadProject(this.reader.result)
|
||||
.then(() => {
|
||||
// Remove the hash if any (without triggering a hash change event or a reload)
|
||||
try { // Can fail e.g. when GUI is loaded from static file (integration tests)
|
||||
history.replaceState({}, document.title, '.');
|
||||
} catch {
|
||||
// No fallback, just do not trigger promise catch below
|
||||
}
|
||||
this.props.onLoadingFinished(this.props.loadingState, true);
|
||||
// Reset the file input after project is loaded
|
||||
// This is necessary in case the user wants to reload a project
|
||||
if (filename) {
|
||||
const uploadedProjectTitle = this.getProjectTitleFromFilename(filename);
|
||||
this.props.onUpdateProjectTitle(uploadedProjectTitle);
|
||||
this.props.onReceivedProjectTitle(uploadedProjectTitle);
|
||||
}
|
||||
this.resetFileInput();
|
||||
})
|
||||
@@ -170,15 +173,17 @@ SBFileUploader.propTypes = {
|
||||
canSave: PropTypes.bool, // eslint-disable-line react/no-unused-prop-types
|
||||
children: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
closeFileMenu: PropTypes.func,
|
||||
intl: intlShape.isRequired,
|
||||
isLoadingUpload: PropTypes.bool,
|
||||
isShowingWithoutId: PropTypes.bool,
|
||||
loadingState: PropTypes.oneOf(LoadingStates),
|
||||
onLoadingFinished: PropTypes.func,
|
||||
onLoadingStarted: PropTypes.func,
|
||||
onUpdateProjectTitle: PropTypes.func,
|
||||
projectChanged: PropTypes.bool,
|
||||
requestProjectUpload: PropTypes.func,
|
||||
onReceivedProjectTitle: PropTypes.func,
|
||||
userOwnsProject: PropTypes.bool,
|
||||
vm: PropTypes.shape({
|
||||
loadProject: PropTypes.func
|
||||
})
|
||||
@@ -198,13 +203,15 @@ const mapStateToProps = state => {
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, ownProps) => ({
|
||||
closeFileMenu: () => dispatch(closeFileMenu()),
|
||||
onLoadingFinished: (loadingState, success) => {
|
||||
dispatch(onLoadedProject(loadingState, ownProps.canSave, success));
|
||||
dispatch(closeLoadingProject());
|
||||
dispatch(closeFileMenu());
|
||||
},
|
||||
requestProjectUpload: loadingState => dispatch(requestProjectUpload(loadingState)),
|
||||
onLoadingStarted: () => dispatch(openLoadingProject())
|
||||
onLoadingStarted: () => dispatch(openLoadingProject()),
|
||||
onReceivedProjectTitle: title => dispatch(setProjectTitle(title))
|
||||
});
|
||||
|
||||
// Allow incoming props to override redux-provided props. Used to mock in tests.
|
||||
|
||||
@@ -2,10 +2,11 @@ import bindAll from 'lodash.bindall';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import WavEncoder from 'wav-encoder';
|
||||
import VM from 'scratch-vm';
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {computeChunkedRMS, SOUND_BYTE_LIMIT} from '../lib/audio/audio-util.js';
|
||||
import {computeChunkedRMS, encodeAndAddSoundToVM, SOUND_BYTE_LIMIT} from '../lib/audio/audio-util.js';
|
||||
import AudioEffects from '../lib/audio/audio-effects.js';
|
||||
import SoundEditorComponent from '../components/sound-editor/sound-editor.jsx';
|
||||
import AudioBufferPlayer from '../lib/audio/audio-buffer-player.js';
|
||||
@@ -13,25 +14,35 @@ import log from '../lib/log.js';
|
||||
|
||||
const UNDO_STACK_SIZE = 99;
|
||||
|
||||
const MAX_RMS = 1.2;
|
||||
|
||||
class SoundEditor extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'copy',
|
||||
'copyCurrentBuffer',
|
||||
'handleCopyToNew',
|
||||
'handleStoppedPlaying',
|
||||
'handleChangeName',
|
||||
'handlePlay',
|
||||
'handleStopPlaying',
|
||||
'handleUpdatePlayhead',
|
||||
'handleActivateTrim',
|
||||
'handleUpdateTrimEnd',
|
||||
'handleUpdateTrimStart',
|
||||
'handleDelete',
|
||||
'handleUpdateTrim',
|
||||
'handleEffect',
|
||||
'handleUndo',
|
||||
'handleRedo',
|
||||
'submitNewSamples'
|
||||
'submitNewSamples',
|
||||
'handleCopy',
|
||||
'handlePaste',
|
||||
'paste',
|
||||
'handleKeyPress',
|
||||
'handleContainerClick',
|
||||
'setRef'
|
||||
]);
|
||||
this.state = {
|
||||
copyBuffer: null,
|
||||
chunkLevels: computeChunkedRMS(this.props.samples),
|
||||
playhead: null, // null is not playing, [0 -> 1] is playing percent
|
||||
trimStart: null,
|
||||
@@ -40,28 +51,84 @@ class SoundEditor extends React.Component {
|
||||
|
||||
this.redoStack = [];
|
||||
this.undoStack = [];
|
||||
|
||||
this.ref = null;
|
||||
}
|
||||
componentDidMount () {
|
||||
this.audioBufferPlayer = new AudioBufferPlayer(this.props.samples, this.props.sampleRate);
|
||||
|
||||
document.addEventListener('keydown', this.handleKeyPress);
|
||||
}
|
||||
componentWillReceiveProps (newProps) {
|
||||
if (newProps.soundId !== this.props.soundId) { // A different sound has been selected
|
||||
this.redoStack = [];
|
||||
this.undoStack = [];
|
||||
this.resetState(newProps.samples, newProps.sampleRate);
|
||||
this.setState({
|
||||
trimStart: null,
|
||||
trimEnd: null
|
||||
});
|
||||
}
|
||||
}
|
||||
componentWillUnmount () {
|
||||
this.audioBufferPlayer.stop();
|
||||
|
||||
document.removeEventListener('keydown', this.handleKeyPress);
|
||||
}
|
||||
handleKeyPress (event) {
|
||||
if (event.target instanceof HTMLInputElement) {
|
||||
// Ignore keyboard shortcuts if a text input field is focused
|
||||
return;
|
||||
}
|
||||
if (event.key === ' ') {
|
||||
event.preventDefault();
|
||||
if (this.state.playhead) {
|
||||
this.handleStopPlaying();
|
||||
} else {
|
||||
this.handlePlay();
|
||||
}
|
||||
}
|
||||
if (event.key === 'Delete' || event.key === 'Backspace') {
|
||||
event.preventDefault();
|
||||
if (event.shiftKey) {
|
||||
this.handleDeleteInverse();
|
||||
} else {
|
||||
this.handleDelete();
|
||||
}
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
this.handleUpdateTrim(null, null);
|
||||
}
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
if (event.shiftKey && event.key.toLowerCase() === 'z') {
|
||||
event.preventDefault();
|
||||
if (this.redoStack.length > 0) {
|
||||
this.handleRedo();
|
||||
}
|
||||
} else if (event.key === 'z') {
|
||||
if (this.undoStack.length > 0) {
|
||||
event.preventDefault();
|
||||
this.handleUndo();
|
||||
}
|
||||
} else if (event.key === 'c') {
|
||||
event.preventDefault();
|
||||
this.handleCopy();
|
||||
} else if (event.key === 'v') {
|
||||
event.preventDefault();
|
||||
this.handlePaste();
|
||||
} else if (event.key === 'a') {
|
||||
event.preventDefault();
|
||||
this.handleUpdateTrim(0, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
resetState (samples, sampleRate) {
|
||||
this.audioBufferPlayer.stop();
|
||||
this.audioBufferPlayer = new AudioBufferPlayer(samples, sampleRate);
|
||||
this.setState({
|
||||
chunkLevels: computeChunkedRMS(samples),
|
||||
playhead: null,
|
||||
trimStart: null,
|
||||
trimEnd: null
|
||||
playhead: null
|
||||
});
|
||||
}
|
||||
submitNewSamples (samples, sampleRate, skipUndo) {
|
||||
@@ -93,7 +160,7 @@ class SoundEditor extends React.Component {
|
||||
if (this.undoStack.length >= UNDO_STACK_SIZE) {
|
||||
this.undoStack.shift(); // Drop the first element off the array
|
||||
}
|
||||
this.undoStack.push(this.copyCurrentBuffer());
|
||||
this.undoStack.push(this.getUndoItem());
|
||||
}
|
||||
this.resetState(samples, sampleRate);
|
||||
this.props.vm.updateSoundBuffer(
|
||||
@@ -106,6 +173,7 @@ class SoundEditor extends React.Component {
|
||||
return false; // Update failed
|
||||
}
|
||||
handlePlay () {
|
||||
this.audioBufferPlayer.stop();
|
||||
this.audioBufferPlayer.play(
|
||||
this.state.trimStart || 0,
|
||||
this.state.trimEnd || 1,
|
||||
@@ -125,31 +193,46 @@ class SoundEditor extends React.Component {
|
||||
handleChangeName (name) {
|
||||
this.props.vm.renameSound(this.props.soundIndex, name);
|
||||
}
|
||||
handleActivateTrim () {
|
||||
if (this.state.trimStart === null && this.state.trimEnd === null) {
|
||||
this.setState({trimEnd: 0.95, trimStart: 0.05});
|
||||
handleDelete () {
|
||||
const {samples, sampleRate} = this.copyCurrentBuffer();
|
||||
const sampleCount = samples.length;
|
||||
const startIndex = Math.floor(this.state.trimStart * sampleCount);
|
||||
const endIndex = Math.floor(this.state.trimEnd * sampleCount);
|
||||
const firstPart = samples.slice(0, startIndex);
|
||||
const secondPart = samples.slice(endIndex, sampleCount);
|
||||
const newLength = firstPart.length + secondPart.length;
|
||||
let newSamples;
|
||||
if (newLength === 0) {
|
||||
newSamples = new Float32Array(1);
|
||||
} else {
|
||||
const {samples, sampleRate} = this.copyCurrentBuffer();
|
||||
const sampleCount = samples.length;
|
||||
const startIndex = Math.floor(this.state.trimStart * sampleCount);
|
||||
const endIndex = Math.floor(this.state.trimEnd * sampleCount);
|
||||
if (endIndex > startIndex) { // Strictly greater to prevent 0 sample sounds
|
||||
const clippedSamples = samples.slice(startIndex, endIndex);
|
||||
this.submitNewSamples(clippedSamples, sampleRate);
|
||||
} else {
|
||||
// Just clear the trim state, it cannot be completed
|
||||
this.setState({
|
||||
trimStart: null,
|
||||
trimEnd: null
|
||||
});
|
||||
}
|
||||
newSamples = new Float32Array(newLength);
|
||||
newSamples.set(firstPart, 0);
|
||||
newSamples.set(secondPart, firstPart.length);
|
||||
}
|
||||
this.submitNewSamples(newSamples, sampleRate);
|
||||
this.setState({
|
||||
trimStart: null,
|
||||
trimEnd: null
|
||||
});
|
||||
}
|
||||
handleUpdateTrimEnd (trimEnd) {
|
||||
this.setState({trimEnd});
|
||||
handleDeleteInverse () {
|
||||
const {samples, sampleRate} = this.copyCurrentBuffer();
|
||||
const sampleCount = samples.length;
|
||||
const startIndex = Math.floor(this.state.trimStart * sampleCount);
|
||||
const endIndex = Math.floor(this.state.trimEnd * sampleCount);
|
||||
let clippedSamples = samples.slice(startIndex, endIndex);
|
||||
if (clippedSamples.length === 0) {
|
||||
clippedSamples = new Float32Array(1);
|
||||
}
|
||||
this.submitNewSamples(clippedSamples, sampleRate);
|
||||
this.setState({
|
||||
trimStart: null,
|
||||
trimEnd: null
|
||||
});
|
||||
}
|
||||
handleUpdateTrimStart (trimStart) {
|
||||
this.setState({trimStart});
|
||||
handleUpdateTrim (trimStart, trimEnd) {
|
||||
this.setState({trimStart, trimEnd});
|
||||
this.handleStopPlaying();
|
||||
}
|
||||
effectFactory (name) {
|
||||
return () => this.handleEffect(name);
|
||||
@@ -162,52 +245,191 @@ class SoundEditor extends React.Component {
|
||||
};
|
||||
}
|
||||
handleEffect (name) {
|
||||
const effects = new AudioEffects(this.audioBufferPlayer.buffer, name);
|
||||
effects.process(({renderedBuffer}) => {
|
||||
const trimStart = this.state.trimStart === null ? 0.0 : this.state.trimStart;
|
||||
const trimEnd = this.state.trimEnd === null ? 1.0 : this.state.trimEnd;
|
||||
|
||||
// Offline audio context needs at least 2 samples
|
||||
if (this.audioBufferPlayer.buffer.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const effects = new AudioEffects(this.audioBufferPlayer.buffer, name, trimStart, trimEnd);
|
||||
effects.process((renderedBuffer, adjustedTrimStart, adjustedTrimEnd) => {
|
||||
const samples = renderedBuffer.getChannelData(0);
|
||||
const sampleRate = renderedBuffer.sampleRate;
|
||||
const success = this.submitNewSamples(samples, sampleRate);
|
||||
if (success) this.handlePlay();
|
||||
if (success) {
|
||||
if (this.state.trimStart === null) {
|
||||
this.handlePlay();
|
||||
} else {
|
||||
this.setState({trimStart: adjustedTrimStart, trimEnd: adjustedTrimEnd}, this.handlePlay);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
tooLoud () {
|
||||
const numChunks = this.state.chunkLevels.length;
|
||||
const startIndex = this.state.trimStart === null ?
|
||||
0 : Math.floor(this.state.trimStart * numChunks);
|
||||
const endIndex = this.state.trimEnd === null ?
|
||||
numChunks - 1 : Math.ceil(this.state.trimEnd * numChunks);
|
||||
const trimChunks = this.state.chunkLevels.slice(startIndex, endIndex);
|
||||
return Math.max(...trimChunks) > MAX_RMS;
|
||||
}
|
||||
getUndoItem () {
|
||||
return {
|
||||
...this.copyCurrentBuffer(),
|
||||
trimStart: this.state.trimStart,
|
||||
trimEnd: this.state.trimEnd
|
||||
};
|
||||
}
|
||||
handleUndo () {
|
||||
this.redoStack.push(this.copyCurrentBuffer());
|
||||
const {samples, sampleRate} = this.undoStack.pop();
|
||||
this.redoStack.push(this.getUndoItem());
|
||||
const {samples, sampleRate, trimStart, trimEnd} = this.undoStack.pop();
|
||||
if (samples) {
|
||||
this.submitNewSamples(samples, sampleRate, true);
|
||||
this.handlePlay();
|
||||
this.setState({trimStart: trimStart, trimEnd: trimEnd}, this.handlePlay);
|
||||
}
|
||||
}
|
||||
handleRedo () {
|
||||
const {samples, sampleRate} = this.redoStack.pop();
|
||||
const {samples, sampleRate, trimStart, trimEnd} = this.redoStack.pop();
|
||||
if (samples) {
|
||||
this.undoStack.push(this.copyCurrentBuffer());
|
||||
this.undoStack.push(this.getUndoItem());
|
||||
this.submitNewSamples(samples, sampleRate, true);
|
||||
this.setState({trimStart: trimStart, trimEnd: trimEnd}, this.handlePlay);
|
||||
}
|
||||
}
|
||||
handleCopy () {
|
||||
this.copy();
|
||||
}
|
||||
copy (callback) {
|
||||
const trimStart = this.state.trimStart === null ? 0.0 : this.state.trimStart;
|
||||
const trimEnd = this.state.trimEnd === null ? 1.0 : this.state.trimEnd;
|
||||
|
||||
const newCopyBuffer = this.copyCurrentBuffer();
|
||||
const trimStartSamples = trimStart * newCopyBuffer.samples.length;
|
||||
const trimEndSamples = trimEnd * newCopyBuffer.samples.length;
|
||||
newCopyBuffer.samples = newCopyBuffer.samples.slice(trimStartSamples, trimEndSamples);
|
||||
|
||||
this.setState({
|
||||
copyBuffer: newCopyBuffer
|
||||
}, callback);
|
||||
}
|
||||
handleCopyToNew () {
|
||||
this.copy(() => {
|
||||
encodeAndAddSoundToVM(this.props.vm, this.state.copyBuffer.samples,
|
||||
this.state.copyBuffer.sampleRate, this.props.name);
|
||||
});
|
||||
}
|
||||
resampleBufferToRate (buffer, newRate) {
|
||||
return new Promise(resolve => {
|
||||
if (window.OfflineAudioContext) {
|
||||
const sampleRateRatio = newRate / buffer.sampleRate;
|
||||
const newLength = sampleRateRatio * buffer.samples.length;
|
||||
const offlineContext = new window.OfflineAudioContext(1, newLength, newRate);
|
||||
const source = offlineContext.createBufferSource();
|
||||
const audioBuffer = offlineContext.createBuffer(1, buffer.samples.length, buffer.sampleRate);
|
||||
audioBuffer.getChannelData(0).set(buffer.samples);
|
||||
source.buffer = audioBuffer;
|
||||
source.connect(offlineContext.destination);
|
||||
source.start();
|
||||
offlineContext.startRendering();
|
||||
offlineContext.oncomplete = ({renderedBuffer}) => {
|
||||
resolve({
|
||||
samples: renderedBuffer.getChannelData(0),
|
||||
sampleRate: newRate
|
||||
});
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
paste () {
|
||||
// If there's no selection, paste at the end of the sound
|
||||
const {samples} = this.copyCurrentBuffer();
|
||||
if (this.state.trimStart === null) {
|
||||
const newLength = samples.length + this.state.copyBuffer.samples.length;
|
||||
const newSamples = new Float32Array(newLength);
|
||||
newSamples.set(samples, 0);
|
||||
newSamples.set(this.state.copyBuffer.samples, samples.length);
|
||||
this.submitNewSamples(newSamples, this.props.sampleRate, false);
|
||||
this.handlePlay();
|
||||
} else {
|
||||
// else replace the selection with the pasted sound
|
||||
const trimStartSamples = this.state.trimStart * samples.length;
|
||||
const trimEndSamples = this.state.trimEnd * samples.length;
|
||||
const firstPart = samples.slice(0, trimStartSamples);
|
||||
const lastPart = samples.slice(trimEndSamples);
|
||||
const newLength = firstPart.length + this.state.copyBuffer.samples.length + lastPart.length;
|
||||
const newSamples = new Float32Array(newLength);
|
||||
newSamples.set(firstPart, 0);
|
||||
newSamples.set(this.state.copyBuffer.samples, firstPart.length);
|
||||
newSamples.set(lastPart, firstPart.length + this.state.copyBuffer.samples.length);
|
||||
|
||||
const trimStartSeconds = trimStartSamples / this.props.sampleRate;
|
||||
const trimEndSeconds = trimStartSeconds +
|
||||
(this.state.copyBuffer.samples.length / this.state.copyBuffer.sampleRate);
|
||||
const newDurationSeconds = newSamples.length / this.state.copyBuffer.sampleRate;
|
||||
const adjustedTrimStart = trimStartSeconds / newDurationSeconds;
|
||||
const adjustedTrimEnd = trimEndSeconds / newDurationSeconds;
|
||||
this.submitNewSamples(newSamples, this.props.sampleRate, false);
|
||||
this.setState({
|
||||
trimStart: adjustedTrimStart,
|
||||
trimEnd: adjustedTrimEnd
|
||||
}, this.handlePlay);
|
||||
}
|
||||
}
|
||||
handlePaste () {
|
||||
if (!this.state.copyBuffer) return;
|
||||
if (this.state.copyBuffer.sampleRate === this.props.sampleRate) {
|
||||
this.paste();
|
||||
} else {
|
||||
this.resampleBufferToRate(this.state.copyBuffer, this.props.sampleRate).then(buffer => {
|
||||
this.setState({
|
||||
copyBuffer: buffer
|
||||
}, this.paste);
|
||||
});
|
||||
}
|
||||
}
|
||||
setRef (element) {
|
||||
this.ref = element;
|
||||
}
|
||||
handleContainerClick (e) {
|
||||
// If the click is on the sound editor's div (and not any other element), delesect
|
||||
if (e.target === this.ref && this.state.trimStart !== null) {
|
||||
this.handleUpdateTrim(null, null);
|
||||
}
|
||||
}
|
||||
render () {
|
||||
const {effectTypes} = AudioEffects;
|
||||
return (
|
||||
<SoundEditorComponent
|
||||
canPaste={this.state.copyBuffer !== null}
|
||||
canRedo={this.redoStack.length > 0}
|
||||
canUndo={this.undoStack.length > 0}
|
||||
chunkLevels={this.state.chunkLevels}
|
||||
name={this.props.name}
|
||||
playhead={this.state.playhead}
|
||||
setRef={this.setRef}
|
||||
tooLoud={this.tooLoud()}
|
||||
trimEnd={this.state.trimEnd}
|
||||
trimStart={this.state.trimStart}
|
||||
onActivateTrim={this.handleActivateTrim}
|
||||
onChangeName={this.handleChangeName}
|
||||
onContainerClick={this.handleContainerClick}
|
||||
onCopy={this.handleCopy}
|
||||
onCopyToNew={this.handleCopyToNew}
|
||||
onDelete={this.handleDelete}
|
||||
onEcho={this.effectFactory(effectTypes.ECHO)}
|
||||
onFadeIn={this.effectFactory(effectTypes.FADEIN)}
|
||||
onFadeOut={this.effectFactory(effectTypes.FADEOUT)}
|
||||
onFaster={this.effectFactory(effectTypes.FASTER)}
|
||||
onLouder={this.effectFactory(effectTypes.LOUDER)}
|
||||
onMute={this.effectFactory(effectTypes.MUTE)}
|
||||
onPaste={this.handlePaste}
|
||||
onPlay={this.handlePlay}
|
||||
onRedo={this.handleRedo}
|
||||
onReverse={this.effectFactory(effectTypes.REVERSE)}
|
||||
onRobot={this.effectFactory(effectTypes.ROBOT)}
|
||||
onSetTrimEnd={this.handleUpdateTrimEnd}
|
||||
onSetTrimStart={this.handleUpdateTrimStart}
|
||||
onSetTrim={this.handleUpdateTrim}
|
||||
onSlower={this.effectFactory(effectTypes.SLOWER)}
|
||||
onSofter={this.effectFactory(effectTypes.SOFTER)}
|
||||
onStop={this.handleStopPlaying}
|
||||
@@ -223,10 +445,7 @@ SoundEditor.propTypes = {
|
||||
samples: PropTypes.instanceOf(Float32Array),
|
||||
soundId: PropTypes.string,
|
||||
soundIndex: PropTypes.number,
|
||||
vm: PropTypes.shape({
|
||||
updateSoundBuffer: PropTypes.func,
|
||||
renameSound: PropTypes.func
|
||||
})
|
||||
vm: PropTypes.instanceOf(VM).isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, {soundIndex}) => {
|
||||
|
||||
@@ -7,8 +7,8 @@ import AudioEngine from 'scratch-audio';
|
||||
|
||||
import LibraryComponent from '../components/library/library.jsx';
|
||||
|
||||
import soundIcon from '../components/asset-panel/icon--sound.svg';
|
||||
import soundIconRtl from '../components/asset-panel/icon--sound-rtl.svg';
|
||||
import soundIcon from '../components/library-item/lib-icon--sound.svg';
|
||||
import soundIconRtl from '../components/library-item/lib-icon--sound-rtl.svg';
|
||||
|
||||
import soundLibraryContent from '../lib/libraries/sounds.json';
|
||||
import soundTags from '../lib/libraries/sound-tags';
|
||||
@@ -29,7 +29,9 @@ class SoundLibrary extends React.PureComponent {
|
||||
bindAll(this, [
|
||||
'handleItemSelected',
|
||||
'handleItemMouseEnter',
|
||||
'handleItemMouseLeave'
|
||||
'handleItemMouseLeave',
|
||||
'onStop',
|
||||
'setStopHandler'
|
||||
]);
|
||||
|
||||
/**
|
||||
@@ -43,6 +45,11 @@ class SoundLibrary extends React.PureComponent {
|
||||
* @type {Promise<SoundPlayer>}
|
||||
*/
|
||||
this.playingSoundPromise = null;
|
||||
|
||||
/**
|
||||
* function to call when the sound ends
|
||||
*/
|
||||
this.handleStop = null;
|
||||
}
|
||||
componentDidMount () {
|
||||
this.audioEngine = new AudioEngine();
|
||||
@@ -51,10 +58,22 @@ class SoundLibrary extends React.PureComponent {
|
||||
componentWillUnmount () {
|
||||
this.stopPlayingSound();
|
||||
}
|
||||
onStop () {
|
||||
if (this.playingSoundPromise !== null) {
|
||||
this.playingSoundPromise.then(soundPlayer => soundPlayer.removeListener('stop', this.onStop));
|
||||
if (this.handleStop) this.handleStop();
|
||||
}
|
||||
|
||||
}
|
||||
setStopHandler (func) {
|
||||
this.handleStop = func;
|
||||
}
|
||||
stopPlayingSound () {
|
||||
// Playback is queued, playing, or has played recently and finished
|
||||
// normally.
|
||||
if (this.playingSoundPromise !== null) {
|
||||
// Forcing sound to stop, so stop listening for sound ending:
|
||||
this.playingSoundPromise.then(soundPlayer => soundPlayer.removeListener('stop', this.onStop));
|
||||
// Queued playback began playing before this method.
|
||||
if (this.playingSoundPromise.isPlaying) {
|
||||
// Fetch the player from the promise and stop playback soon.
|
||||
@@ -102,6 +121,7 @@ class SoundLibrary extends React.PureComponent {
|
||||
// Play the sound. Playing the sound will always come before a
|
||||
// paired stop if the sound must stop early.
|
||||
soundPlayer.play();
|
||||
soundPlayer.addListener('stop', this.onStop);
|
||||
// Set that the sound is playing. This affects the type of stop
|
||||
// instruction given if the sound must stop early.
|
||||
if (this.playingSoundPromise !== null) {
|
||||
@@ -141,8 +161,10 @@ class SoundLibrary extends React.PureComponent {
|
||||
|
||||
return (
|
||||
<LibraryComponent
|
||||
showPlayButton
|
||||
data={soundLibraryThumbnailData}
|
||||
id="soundLibrary"
|
||||
setStopHandler={this.setStopHandler}
|
||||
tags={soundTags}
|
||||
title={this.props.intl.formatMessage(messages.libraryTitle)}
|
||||
onItemMouseEnter={this.handleItemMouseEnter}
|
||||
|
||||
@@ -6,19 +6,19 @@ import {connect} from 'react-redux';
|
||||
import {setHoveredSprite} from '../reducers/hovered-target';
|
||||
import {updateAssetDrag} from '../reducers/asset-drag';
|
||||
import storage from '../lib/storage';
|
||||
import {getEventXY} from '../lib/touch-utils';
|
||||
import VM from 'scratch-vm';
|
||||
import getCostumeUrl from '../lib/get-costume-url';
|
||||
import DragRecognizer from '../lib/drag-recognizer';
|
||||
import {getEventXY} from '../lib/touch-utils';
|
||||
|
||||
import SpriteSelectorItemComponent from '../components/sprite-selector-item/sprite-selector-item.jsx';
|
||||
|
||||
const dragThreshold = 3; // Same as the block drag threshold
|
||||
|
||||
class SpriteSelectorItem extends React.PureComponent {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'getCostumeData',
|
||||
'setRef',
|
||||
'handleClick',
|
||||
'handleDelete',
|
||||
'handleDuplicate',
|
||||
@@ -26,9 +26,22 @@ class SpriteSelectorItem extends React.PureComponent {
|
||||
'handleMouseEnter',
|
||||
'handleMouseLeave',
|
||||
'handleMouseDown',
|
||||
'handleMouseMove',
|
||||
'handleMouseUp'
|
||||
'handleDragEnd',
|
||||
'handleDrag',
|
||||
'handleTouchEnd'
|
||||
]);
|
||||
|
||||
this.dragRecognizer = new DragRecognizer({
|
||||
onDrag: this.handleDrag,
|
||||
onDragEnd: this.handleDragEnd
|
||||
});
|
||||
}
|
||||
componentDidMount () {
|
||||
document.addEventListener('touchend', this.handleTouchEnd);
|
||||
}
|
||||
componentWillUnmount () {
|
||||
document.removeEventListener('touchend', this.handleTouchEnd);
|
||||
this.dragRecognizer.reset();
|
||||
}
|
||||
getCostumeData () {
|
||||
if (this.props.costumeURL) return this.props.costumeURL;
|
||||
@@ -36,12 +49,7 @@ class SpriteSelectorItem extends React.PureComponent {
|
||||
|
||||
return getCostumeUrl(this.props.asset);
|
||||
}
|
||||
handleMouseUp () {
|
||||
this.initialOffset = null;
|
||||
window.removeEventListener('mouseup', this.handleMouseUp);
|
||||
window.removeEventListener('mousemove', this.handleMouseMove);
|
||||
window.removeEventListener('touchend', this.handleMouseUp);
|
||||
window.removeEventListener('touchmove', this.handleMouseMove);
|
||||
handleDragEnd () {
|
||||
if (this.props.dragging) {
|
||||
this.props.onDrag({
|
||||
img: null,
|
||||
@@ -55,29 +63,26 @@ class SpriteSelectorItem extends React.PureComponent {
|
||||
this.noClick = false;
|
||||
});
|
||||
}
|
||||
handleMouseMove (e) {
|
||||
const currentOffset = getEventXY(e);
|
||||
const dx = currentOffset.x - this.initialOffset.x;
|
||||
const dy = currentOffset.y - this.initialOffset.y;
|
||||
if (Math.sqrt((dx * dx) + (dy * dy)) > dragThreshold) {
|
||||
this.props.onDrag({
|
||||
img: this.getCostumeData(),
|
||||
currentOffset: currentOffset,
|
||||
dragging: true,
|
||||
dragType: this.props.dragType,
|
||||
index: this.props.index,
|
||||
payload: this.props.dragPayload
|
||||
});
|
||||
this.noClick = true;
|
||||
handleDrag (currentOffset) {
|
||||
this.props.onDrag({
|
||||
img: this.getCostumeData(),
|
||||
currentOffset: currentOffset,
|
||||
dragging: true,
|
||||
dragType: this.props.dragType,
|
||||
index: this.props.index,
|
||||
payload: this.props.dragPayload
|
||||
});
|
||||
this.noClick = true;
|
||||
}
|
||||
handleTouchEnd (e) {
|
||||
const {x, y} = getEventXY(e);
|
||||
const {top, left, bottom, right} = this.ref.getBoundingClientRect();
|
||||
if (x >= left && x <= right && y >= top && y <= bottom) {
|
||||
this.handleMouseEnter();
|
||||
}
|
||||
e.preventDefault();
|
||||
}
|
||||
handleMouseDown (e) {
|
||||
this.initialOffset = getEventXY(e);
|
||||
window.addEventListener('mouseup', this.handleMouseUp);
|
||||
window.addEventListener('mousemove', this.handleMouseMove);
|
||||
window.addEventListener('touchend', this.handleMouseUp);
|
||||
window.addEventListener('touchmove', this.handleMouseMove);
|
||||
this.dragRecognizer.start(e);
|
||||
}
|
||||
handleClick (e) {
|
||||
e.preventDefault();
|
||||
@@ -103,6 +108,10 @@ class SpriteSelectorItem extends React.PureComponent {
|
||||
handleMouseEnter () {
|
||||
this.props.dispatchSetHoveredSprite(this.props.id);
|
||||
}
|
||||
setRef (component) {
|
||||
// Access the DOM node using .elem because it is going through ContextMenuTrigger
|
||||
this.ref = component && component.elem;
|
||||
}
|
||||
render () {
|
||||
const {
|
||||
/* eslint-disable no-unused-vars */
|
||||
@@ -122,7 +131,9 @@ class SpriteSelectorItem extends React.PureComponent {
|
||||
} = this.props;
|
||||
return (
|
||||
<SpriteSelectorItemComponent
|
||||
componentRef={this.setRef}
|
||||
costumeURL={this.getCostumeData()}
|
||||
preventContextMenu={this.dragRecognizer.gestureInProgress()}
|
||||
onClick={this.handleClick}
|
||||
onDeleteButtonClick={onDeleteButtonClick ? this.handleDelete : null}
|
||||
onDuplicateButtonClick={onDuplicateButtonClick ? this.handleDuplicate : null}
|
||||
|
||||
@@ -15,6 +15,7 @@ import ThrottledPropertyHOC from '../lib/throttled-property-hoc.jsx';
|
||||
import {emptyCostume} from '../lib/empty-assets';
|
||||
import sharedMessages from '../lib/shared-messages';
|
||||
import {fetchCode} from '../lib/backpack-api';
|
||||
import {getEventXY} from '../lib/touch-utils';
|
||||
|
||||
import StageSelectorComponent from '../components/stage-selector/stage-selector.jsx';
|
||||
|
||||
@@ -46,11 +47,26 @@ class StageSelector extends React.Component {
|
||||
'handleBackdropUpload',
|
||||
'handleMouseEnter',
|
||||
'handleMouseLeave',
|
||||
'handleTouchEnd',
|
||||
'handleDrop',
|
||||
'setFileInput'
|
||||
'setFileInput',
|
||||
'setRef'
|
||||
]);
|
||||
}
|
||||
addBackdropFromLibraryItem (item) {
|
||||
componentDidMount () {
|
||||
document.addEventListener('touchend', this.handleTouchEnd);
|
||||
}
|
||||
componentWillUnmount () {
|
||||
document.removeEventListener('touchend', this.handleTouchEnd);
|
||||
}
|
||||
handleTouchEnd (e) {
|
||||
const {x, y} = getEventXY(e);
|
||||
const {top, left, bottom, right} = this.ref.getBoundingClientRect();
|
||||
if (x >= left && x <= right && y >= top && y <= bottom) {
|
||||
this.handleMouseEnter();
|
||||
}
|
||||
}
|
||||
addBackdropFromLibraryItem (item, shouldActivateTab = true) {
|
||||
const vmBackdrop = {
|
||||
name: item.name,
|
||||
md5: item.md5,
|
||||
@@ -59,25 +75,30 @@ class StageSelector extends React.Component {
|
||||
bitmapResolution: item.info.length > 2 ? item.info[2] : 1,
|
||||
skinId: null
|
||||
};
|
||||
this.handleNewBackdrop(vmBackdrop);
|
||||
this.handleNewBackdrop(vmBackdrop, shouldActivateTab);
|
||||
}
|
||||
handleClick () {
|
||||
this.props.onSelect(this.props.id);
|
||||
}
|
||||
handleNewBackdrop (backdrops_) {
|
||||
handleNewBackdrop (backdrops_, shouldActivateTab = true) {
|
||||
const backdrops = Array.isArray(backdrops_) ? backdrops_ : [backdrops_];
|
||||
return Promise.all(backdrops.map(backdrop =>
|
||||
this.props.vm.addBackdrop(backdrop.md5, backdrop)
|
||||
)).then(() =>
|
||||
this.props.onActivateTab(COSTUMES_TAB_INDEX)
|
||||
);
|
||||
)).then(() => {
|
||||
if (shouldActivateTab) {
|
||||
return this.props.onActivateTab(COSTUMES_TAB_INDEX);
|
||||
}
|
||||
});
|
||||
}
|
||||
handleSurpriseBackdrop () {
|
||||
handleSurpriseBackdrop (e) {
|
||||
e.stopPropagation(); // Prevent click from falling through to selecting stage.
|
||||
// @todo should this not add a backdrop you already have?
|
||||
const item = backdropLibraryContent[Math.floor(Math.random() * backdropLibraryContent.length)];
|
||||
this.addBackdropFromLibraryItem(item);
|
||||
this.addBackdropFromLibraryItem(item, false);
|
||||
}
|
||||
handleEmptyBackdrop () {
|
||||
handleEmptyBackdrop (e) {
|
||||
e.stopPropagation(); // Prevent click from falling through to stage selector, select it manually below
|
||||
this.props.vm.setEditingTarget(this.props.id);
|
||||
this.handleNewBackdrop(emptyCostume(this.props.intl.formatMessage(sharedMessages.backdrop, {index: 1})));
|
||||
}
|
||||
handleBackdropUpload (e) {
|
||||
@@ -85,6 +106,7 @@ class StageSelector extends React.Component {
|
||||
this.props.onShowImporting();
|
||||
handleFileUpload(e.target, (buffer, fileType, fileName, fileIndex, fileCount) => {
|
||||
costumeUpload(buffer, fileType, storage, vmCostumes => {
|
||||
this.props.vm.setEditingTarget(this.props.id);
|
||||
vmCostumes.forEach((costume, i) => {
|
||||
costume.name = `${fileName}${i ? i + 1 : ''}`;
|
||||
});
|
||||
@@ -96,7 +118,8 @@ class StageSelector extends React.Component {
|
||||
}, this.props.onCloseImporting);
|
||||
}, this.props.onCloseImporting);
|
||||
}
|
||||
handleFileUploadClick () {
|
||||
handleFileUploadClick (e) {
|
||||
e.stopPropagation(); // Prevent click from selecting the stage, that is handled manually in backdrop upload
|
||||
this.fileInput.click();
|
||||
}
|
||||
handleMouseEnter () {
|
||||
@@ -130,12 +153,16 @@ class StageSelector extends React.Component {
|
||||
setFileInput (input) {
|
||||
this.fileInput = input;
|
||||
}
|
||||
setRef (ref) {
|
||||
this.ref = ref;
|
||||
}
|
||||
render () {
|
||||
const componentProps = omit(this.props, [
|
||||
'asset', 'dispatchSetHoveredSprite', 'id', 'intl',
|
||||
'onActivateTab', 'onSelect', 'onShowImporting', 'onCloseImporting']);
|
||||
return (
|
||||
<DroppableThrottledStage
|
||||
componentRef={this.setRef}
|
||||
fileInputRef={this.setFileInput}
|
||||
onBackdropFileUpload={this.handleBackdropUpload}
|
||||
onBackdropFileUploadClick={this.handleFileUploadClick}
|
||||
|
||||
@@ -423,6 +423,7 @@ class Stage extends React.Component {
|
||||
Stage.propTypes = {
|
||||
isColorPicking: PropTypes.bool,
|
||||
isFullScreen: PropTypes.bool.isRequired,
|
||||
isStarted: PropTypes.bool,
|
||||
micIndicator: PropTypes.bool,
|
||||
onActivateColorPicker: PropTypes.func,
|
||||
onDeactivateColorPicker: PropTypes.func,
|
||||
|
||||
@@ -105,7 +105,10 @@ class TargetPane extends React.Component {
|
||||
}
|
||||
}
|
||||
handleSurpriseSpriteClick () {
|
||||
const item = spriteLibraryContent[Math.floor(Math.random() * spriteLibraryContent.length)];
|
||||
const surpriseSprites = spriteLibraryContent.filter(sprite =>
|
||||
(sprite.tags.indexOf('letters') === -1) && (sprite.tags.indexOf('numbers') === -1)
|
||||
);
|
||||
const item = surpriseSprites[Math.floor(Math.random() * surpriseSprites.length)];
|
||||
randomizeSpritePosition(item);
|
||||
this.props.vm.addSprite(JSON.stringify(item.json))
|
||||
.then(this.handleActivateBlocksTab);
|
||||
@@ -138,11 +141,13 @@ class TargetPane extends React.Component {
|
||||
this.props.onShowImporting();
|
||||
handleFileUpload(e.target, (buffer, fileType, fileName, fileIndex, fileCount) => {
|
||||
spriteUpload(buffer, fileType, fileName, storage, newSprite => {
|
||||
this.handleNewSprite(newSprite).then(() => {
|
||||
if (fileIndex === fileCount - 1) {
|
||||
this.props.onCloseImporting();
|
||||
}
|
||||
});
|
||||
this.handleNewSprite(newSprite)
|
||||
.then(() => {
|
||||
if (fileIndex === fileCount - 1) {
|
||||
this.props.onCloseImporting();
|
||||
}
|
||||
})
|
||||
.catch(this.props.onCloseImporting);
|
||||
}, this.props.onCloseImporting);
|
||||
}, this.props.onCloseImporting);
|
||||
}
|
||||
|
||||
@@ -62,10 +62,16 @@ class TipsLibrary extends React.PureComponent {
|
||||
}
|
||||
render () {
|
||||
const decksLibraryThumbnailData = Object.keys(decksLibraryContent)
|
||||
.filter(id =>
|
||||
.filter(id => {
|
||||
if (notScratchDesktop()) return true; // Do not filter anything in online editor
|
||||
const deck = decksLibraryContent[id];
|
||||
// Scratch Desktop doesn't want tutorials with `requiredProjectId`
|
||||
notScratchDesktop() || !decksLibraryContent[id].hasOwnProperty('requiredProjectId')
|
||||
)
|
||||
if (deck.hasOwnProperty('requiredProjectId')) return false;
|
||||
// Scratch Desktop should not load tutorials that are _only_ videos
|
||||
if (deck.steps.filter(s => s.title).length === 0) return false;
|
||||
// Allow any other tutorials
|
||||
return true;
|
||||
})
|
||||
.map(id => ({
|
||||
rawURL: decksLibraryContent[id].img,
|
||||
id: id,
|
||||
|
||||
@@ -113,7 +113,8 @@ const AppStateHOC = function (WrappedComponent, localesOnly) {
|
||||
}
|
||||
AppStateWrapper.propTypes = {
|
||||
isFullScreen: PropTypes.bool,
|
||||
isPlayerOnly: PropTypes.bool
|
||||
isPlayerOnly: PropTypes.bool,
|
||||
showTelemetryModal: PropTypes.bool
|
||||
};
|
||||
return AppStateWrapper;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import EchoEffect from './effects/echo-effect.js';
|
||||
import RobotEffect from './effects/robot-effect.js';
|
||||
import VolumeEffect from './effects/volume-effect.js';
|
||||
import FadeEffect from './effects/fade-effect.js';
|
||||
import MuteEffect from './effects/mute-effect.js';
|
||||
|
||||
const effectTypes = {
|
||||
ROBOT: 'robot',
|
||||
@@ -9,32 +11,56 @@ const effectTypes = {
|
||||
SOFTER: 'lower',
|
||||
FASTER: 'faster',
|
||||
SLOWER: 'slower',
|
||||
ECHO: 'echo'
|
||||
ECHO: 'echo',
|
||||
FADEIN: 'fade in',
|
||||
FADEOUT: 'fade out',
|
||||
MUTE: 'mute'
|
||||
};
|
||||
|
||||
class AudioEffects {
|
||||
static get effectTypes () {
|
||||
return effectTypes;
|
||||
}
|
||||
constructor (buffer, name) {
|
||||
constructor (buffer, name, trimStart, trimEnd) {
|
||||
this.trimStartSeconds = (trimStart * buffer.length) / buffer.sampleRate;
|
||||
this.trimEndSeconds = (trimEnd * buffer.length) / buffer.sampleRate;
|
||||
this.adjustedTrimStartSeconds = this.trimStartSeconds;
|
||||
this.adjustedTrimEndSeconds = this.trimEndSeconds;
|
||||
|
||||
// Some effects will modify the playback rate and/or number of samples.
|
||||
// Need to precompute those values to create the offline audio context.
|
||||
const pitchRatio = Math.pow(2, 4 / 12); // A major third
|
||||
let sampleCount = buffer.length;
|
||||
let playbackRate = 1;
|
||||
const affectedSampleCount = Math.floor((this.trimEndSeconds - this.trimStartSeconds) *
|
||||
buffer.sampleRate);
|
||||
let adjustedAffectedSampleCount = affectedSampleCount;
|
||||
const unaffectedSampleCount = sampleCount - affectedSampleCount;
|
||||
|
||||
this.playbackRate = 1;
|
||||
switch (name) {
|
||||
case effectTypes.ECHO:
|
||||
sampleCount = buffer.length + (0.25 * 3 * buffer.sampleRate);
|
||||
sampleCount = Math.max(sampleCount,
|
||||
Math.floor((this.trimEndSeconds + EchoEffect.TAIL_SECONDS) * buffer.sampleRate));
|
||||
break;
|
||||
case effectTypes.FASTER:
|
||||
playbackRate = pitchRatio;
|
||||
sampleCount = Math.floor(buffer.length / playbackRate);
|
||||
this.playbackRate = pitchRatio;
|
||||
adjustedAffectedSampleCount = Math.floor(affectedSampleCount / this.playbackRate);
|
||||
sampleCount = unaffectedSampleCount + adjustedAffectedSampleCount;
|
||||
|
||||
break;
|
||||
case effectTypes.SLOWER:
|
||||
playbackRate = 1 / pitchRatio;
|
||||
sampleCount = Math.floor(buffer.length / playbackRate);
|
||||
this.playbackRate = 1 / pitchRatio;
|
||||
adjustedAffectedSampleCount = Math.floor(affectedSampleCount / this.playbackRate);
|
||||
sampleCount = unaffectedSampleCount + adjustedAffectedSampleCount;
|
||||
break;
|
||||
}
|
||||
|
||||
const durationSeconds = sampleCount / buffer.sampleRate;
|
||||
this.adjustedTrimEndSeconds = this.trimStartSeconds +
|
||||
(adjustedAffectedSampleCount / buffer.sampleRate);
|
||||
this.adjustedTrimStart = this.adjustedTrimStartSeconds / durationSeconds;
|
||||
this.adjustedTrimEnd = this.adjustedTrimEndSeconds / durationSeconds;
|
||||
|
||||
if (window.OfflineAudioContext) {
|
||||
this.audioContext = new window.OfflineAudioContext(1, sampleCount, buffer.sampleRate);
|
||||
} else {
|
||||
@@ -52,8 +78,17 @@ class AudioEffects {
|
||||
const newBuffer = this.audioContext.createBuffer(1, buffer.length, buffer.sampleRate);
|
||||
const newBufferData = newBuffer.getChannelData(0);
|
||||
const bufferLength = buffer.length;
|
||||
|
||||
const startSamples = Math.floor(this.trimStartSeconds * buffer.sampleRate);
|
||||
const endSamples = Math.floor(this.trimEndSeconds * buffer.sampleRate);
|
||||
let counter = 0;
|
||||
for (let i = 0; i < bufferLength; i++) {
|
||||
newBufferData[i] = originalBufferData[bufferLength - i - 1];
|
||||
if (i >= startSamples && i < endSamples) {
|
||||
newBufferData[i] = originalBufferData[endSamples - counter - 1];
|
||||
counter++;
|
||||
} else {
|
||||
newBufferData[i] = originalBufferData[i];
|
||||
}
|
||||
}
|
||||
this.buffer = newBuffer;
|
||||
} else {
|
||||
@@ -63,7 +98,6 @@ class AudioEffects {
|
||||
|
||||
this.source = this.audioContext.createBufferSource();
|
||||
this.source.buffer = this.buffer;
|
||||
this.source.playbackRate.value = playbackRate;
|
||||
this.name = name;
|
||||
}
|
||||
process (done) {
|
||||
@@ -71,17 +105,38 @@ class AudioEffects {
|
||||
let input;
|
||||
let output;
|
||||
switch (this.name) {
|
||||
case effectTypes.FASTER:
|
||||
case effectTypes.SLOWER:
|
||||
this.source.playbackRate.setValueAtTime(this.playbackRate, this.adjustedTrimStartSeconds);
|
||||
this.source.playbackRate.setValueAtTime(1.0, this.adjustedTrimEndSeconds);
|
||||
break;
|
||||
case effectTypes.LOUDER:
|
||||
({input, output} = new VolumeEffect(this.audioContext, 1.25));
|
||||
({input, output} = new VolumeEffect(this.audioContext, 1.25,
|
||||
this.adjustedTrimStartSeconds, this.adjustedTrimEndSeconds));
|
||||
break;
|
||||
case effectTypes.SOFTER:
|
||||
({input, output} = new VolumeEffect(this.audioContext, 0.75));
|
||||
({input, output} = new VolumeEffect(this.audioContext, 0.75,
|
||||
this.adjustedTrimStartSeconds, this.adjustedTrimEndSeconds));
|
||||
break;
|
||||
case effectTypes.ECHO:
|
||||
({input, output} = new EchoEffect(this.audioContext, 0.25));
|
||||
({input, output} = new EchoEffect(this.audioContext,
|
||||
this.adjustedTrimStartSeconds, this.adjustedTrimEndSeconds));
|
||||
break;
|
||||
case effectTypes.ROBOT:
|
||||
({input, output} = new RobotEffect(this.audioContext, 0.25));
|
||||
({input, output} = new RobotEffect(this.audioContext,
|
||||
this.adjustedTrimStartSeconds, this.adjustedTrimEndSeconds));
|
||||
break;
|
||||
case effectTypes.FADEIN:
|
||||
({input, output} = new FadeEffect(this.audioContext, true,
|
||||
this.adjustedTrimStartSeconds, this.adjustedTrimEndSeconds));
|
||||
break;
|
||||
case effectTypes.FADEOUT:
|
||||
({input, output} = new FadeEffect(this.audioContext, false,
|
||||
this.adjustedTrimStartSeconds, this.adjustedTrimEndSeconds));
|
||||
break;
|
||||
case effectTypes.MUTE:
|
||||
({input, output} = new MuteEffect(this.audioContext,
|
||||
this.adjustedTrimStartSeconds, this.adjustedTrimEndSeconds));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -96,7 +151,10 @@ class AudioEffects {
|
||||
this.source.start();
|
||||
|
||||
this.audioContext.startRendering();
|
||||
this.audioContext.oncomplete = done;
|
||||
this.audioContext.oncomplete = ({renderedBuffer}) => {
|
||||
done(renderedBuffer, this.adjustedTrimStart, this.adjustedTrimEnd);
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import 'get-float-time-domain-data';
|
||||
import getUserMedia from 'get-user-media-promise';
|
||||
import SharedAudioContext from './shared-audio-context.js';
|
||||
import {computeRMS} from './audio-util.js';
|
||||
import {computeRMS, computeChunkedRMS} from './audio-util.js';
|
||||
|
||||
class AudioRecorder {
|
||||
constructor () {
|
||||
this.audioContext = new SharedAudioContext();
|
||||
this.bufferLength = 1024;
|
||||
this.bufferLength = 8192;
|
||||
|
||||
this.userMediaStream = null;
|
||||
this.mediaStreamSource = null;
|
||||
@@ -51,7 +51,7 @@ class AudioRecorder {
|
||||
this.userMediaStream = userMediaStream;
|
||||
this.mediaStreamSource = this.audioContext.createMediaStreamSource(userMediaStream);
|
||||
this.sourceNode = this.audioContext.createGain();
|
||||
this.scriptProcessorNode = this.audioContext.createScriptProcessor(this.bufferLength, 2, 2);
|
||||
this.scriptProcessorNode = this.audioContext.createScriptProcessor(this.bufferLength, 1, 1);
|
||||
|
||||
this.scriptProcessorNode.onaudioprocess = processEvent => {
|
||||
if (this.recording && !this.disposed) {
|
||||
@@ -68,9 +68,9 @@ class AudioRecorder {
|
||||
|
||||
const update = () => {
|
||||
if (this.disposed) return;
|
||||
requestAnimationFrame(update);
|
||||
this.analyserNode.getFloatTimeDomainData(dataArray);
|
||||
onUpdate(computeRMS(dataArray));
|
||||
requestAnimationFrame(update);
|
||||
};
|
||||
|
||||
requestAnimationFrame(update);
|
||||
@@ -83,7 +83,16 @@ class AudioRecorder {
|
||||
}
|
||||
|
||||
stop () {
|
||||
const chunkLevels = this.buffers.map(buffer => computeRMS(buffer));
|
||||
const buffer = new Float32Array(this.buffers.length * this.bufferLength);
|
||||
|
||||
let offset = 0;
|
||||
for (let i = 0; i < this.buffers.length; i++) {
|
||||
const bufferChunk = this.buffers[i];
|
||||
buffer.set(bufferChunk, offset);
|
||||
offset += bufferChunk.length;
|
||||
}
|
||||
|
||||
const chunkLevels = computeChunkedRMS(buffer);
|
||||
const maxRMS = Math.max.apply(null, chunkLevels);
|
||||
const threshold = maxRMS / 8;
|
||||
|
||||
@@ -105,15 +114,6 @@ class AudioRecorder {
|
||||
trimEnd = 1;
|
||||
}
|
||||
|
||||
const buffer = new Float32Array(this.buffers.length * this.bufferLength);
|
||||
|
||||
let offset = 0;
|
||||
for (let i = 0; i < this.buffers.length; i++) {
|
||||
const bufferChunk = this.buffers[i];
|
||||
buffer.set(bufferChunk, offset);
|
||||
offset += bufferChunk.length;
|
||||
}
|
||||
|
||||
return {
|
||||
levels: chunkLevels,
|
||||
samples: buffer,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import WavEncoder from 'wav-encoder';
|
||||
|
||||
const SOUND_BYTE_LIMIT = 10 * 1000 * 1000; // 10mb
|
||||
|
||||
const computeRMS = function (samples, scaling = 0.55) {
|
||||
@@ -23,8 +25,43 @@ const computeChunkedRMS = function (samples, chunkSize = 1024) {
|
||||
return chunkLevels;
|
||||
};
|
||||
|
||||
const encodeAndAddSoundToVM = function (vm, samples, sampleRate, name, callback) {
|
||||
WavEncoder.encode({
|
||||
sampleRate: sampleRate,
|
||||
channelData: [samples]
|
||||
}).then(wavBuffer => {
|
||||
const vmSound = {
|
||||
format: '',
|
||||
dataFormat: 'wav',
|
||||
rate: sampleRate,
|
||||
sampleCount: samples.length
|
||||
};
|
||||
|
||||
// Create an asset from the encoded .wav and get resulting md5
|
||||
const storage = vm.runtime.storage;
|
||||
vmSound.asset = storage.createAsset(
|
||||
storage.AssetType.Sound,
|
||||
storage.DataFormat.WAV,
|
||||
new Uint8Array(wavBuffer),
|
||||
null,
|
||||
true // generate md5
|
||||
);
|
||||
vmSound.assetId = vmSound.asset.assetId;
|
||||
|
||||
// update vmSound object with md5 property
|
||||
vmSound.md5 = `${vmSound.assetId}.${vmSound.dataFormat}`;
|
||||
// The VM will update the sound name to a fresh name
|
||||
vmSound.name = name;
|
||||
|
||||
vm.addSound(vmSound).then(() => {
|
||||
if (callback) callback();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export {
|
||||
computeRMS,
|
||||
computeChunkedRMS,
|
||||
encodeAndAddSoundToVM,
|
||||
SOUND_BYTE_LIMIT
|
||||
};
|
||||
@@ -1,15 +1,23 @@
|
||||
class EchoEffect {
|
||||
constructor (audioContext, delayTime) {
|
||||
static get DELAY_TIME () {
|
||||
return 0.25;
|
||||
}
|
||||
static get TAIL_SECONDS () {
|
||||
return 0.75;
|
||||
}
|
||||
constructor (audioContext, startTime, endTime) {
|
||||
this.audioContext = audioContext;
|
||||
this.delayTime = delayTime;
|
||||
this.input = this.audioContext.createGain();
|
||||
this.output = this.audioContext.createGain();
|
||||
|
||||
this.effectInput = this.audioContext.createGain();
|
||||
this.effectInput.gain.value = 0.75;
|
||||
this.effectInput.gain.value = 0;
|
||||
|
||||
this.effectInput.gain.setValueAtTime(0.75, startTime);
|
||||
this.effectInput.gain.setValueAtTime(0, endTime);
|
||||
|
||||
this.delay = this.audioContext.createDelay(1);
|
||||
this.delay.delayTime.value = delayTime;
|
||||
this.delay.delayTime.value = EchoEffect.DELAY_TIME;
|
||||
this.decay = this.audioContext.createGain();
|
||||
this.decay.gain.value = 0.3;
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
class FadeEffect {
|
||||
constructor (audioContext, fadeIn, startSeconds, endSeconds) {
|
||||
this.audioContext = audioContext;
|
||||
|
||||
this.input = this.audioContext.createGain();
|
||||
this.output = this.audioContext.createGain();
|
||||
|
||||
this.gain = this.audioContext.createGain();
|
||||
|
||||
this.gain.gain.setValueAtTime(1, 0);
|
||||
|
||||
if (fadeIn) {
|
||||
this.gain.gain.setValueAtTime(0, startSeconds);
|
||||
this.gain.gain.linearRampToValueAtTime(1, endSeconds);
|
||||
} else {
|
||||
this.gain.gain.setValueAtTime(1, startSeconds);
|
||||
this.gain.gain.linearRampToValueAtTime(0, endSeconds);
|
||||
}
|
||||
|
||||
this.gain.gain.setValueAtTime(1, endSeconds);
|
||||
|
||||
this.input.connect(this.gain);
|
||||
this.gain.connect(this.output);
|
||||
}
|
||||
}
|
||||
|
||||
export default FadeEffect;
|
||||
@@ -0,0 +1,22 @@
|
||||
class MuteEffect {
|
||||
constructor (audioContext, startSeconds, endSeconds) {
|
||||
this.audioContext = audioContext;
|
||||
|
||||
this.input = this.audioContext.createGain();
|
||||
this.output = this.audioContext.createGain();
|
||||
|
||||
this.gain = this.audioContext.createGain();
|
||||
|
||||
// Smoothly ramp the gain down before the start time, and up after the end time.
|
||||
this.rampLength = 0.001;
|
||||
this.gain.gain.setValueAtTime(1.0, Math.max(0, startSeconds - this.rampLength));
|
||||
this.gain.gain.linearRampToValueAtTime(0, startSeconds);
|
||||
this.gain.gain.setValueAtTime(0, endSeconds);
|
||||
this.gain.gain.linearRampToValueAtTime(1.0, endSeconds + this.rampLength);
|
||||
|
||||
this.input.connect(this.gain);
|
||||
this.gain.connect(this.output);
|
||||
}
|
||||
}
|
||||
|
||||
export default MuteEffect;
|
||||
@@ -1,9 +1,20 @@
|
||||
class RobotEffect {
|
||||
constructor (audioContext) {
|
||||
constructor (audioContext, startTime, endTime) {
|
||||
this.audioContext = audioContext;
|
||||
|
||||
this.input = this.audioContext.createGain();
|
||||
this.output = this.audioContext.createGain();
|
||||
this.passthrough = this.audioContext.createGain();
|
||||
this.effectInput = this.audioContext.createGain();
|
||||
|
||||
this.passthrough.gain.value = 1;
|
||||
this.effectInput.gain.value = 0;
|
||||
|
||||
this.passthrough.gain.setValueAtTime(0, startTime);
|
||||
this.passthrough.gain.setValueAtTime(1, endTime);
|
||||
|
||||
this.effectInput.gain.setValueAtTime(1, startTime);
|
||||
this.effectInput.gain.setValueAtTime(0, endTime);
|
||||
|
||||
// Ring modulator inspired by BBC Dalek voice
|
||||
// http://recherche.ircam.fr/pub/dafx11/Papers/66_e.pdf
|
||||
@@ -70,8 +81,13 @@ class RobotEffect {
|
||||
biquadFilter.frequency.value = 1000;
|
||||
biquadFilter.gain.value = 1.25;
|
||||
|
||||
this.input.connect(vcInverter1);
|
||||
this.input.connect(vcDiode4);
|
||||
this.input.connect(this.effectInput);
|
||||
this.input.connect(this.passthrough);
|
||||
|
||||
this.passthrough.connect(this.output);
|
||||
|
||||
this.effectInput.connect(vcInverter1);
|
||||
this.effectInput.connect(vcDiode4);
|
||||
|
||||
vcInverter1.connect(vcDiode3);
|
||||
|
||||
@@ -91,7 +107,7 @@ class RobotEffect {
|
||||
vcDiode3.connect(compressor);
|
||||
vcDiode4.connect(compressor);
|
||||
|
||||
this.input.connect(biquadFilter);
|
||||
this.effectInput.connect(biquadFilter);
|
||||
biquadFilter.connect(compressor);
|
||||
|
||||
compressor.connect(this.output);
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
class VolumeEffect {
|
||||
constructor (audioContext, volume) {
|
||||
constructor (audioContext, volume, startSeconds, endSeconds) {
|
||||
this.audioContext = audioContext;
|
||||
|
||||
this.input = this.audioContext.createGain();
|
||||
this.output = this.audioContext.createGain();
|
||||
|
||||
this.gain = this.audioContext.createGain();
|
||||
this.gain.gain.value = volume;
|
||||
|
||||
// Smoothly ramp the gain up before the start time, and down after the end time.
|
||||
this.rampLength = 0.01;
|
||||
this.gain.gain.setValueAtTime(1.0, Math.max(0, startSeconds - this.rampLength));
|
||||
this.gain.gain.exponentialRampToValueAtTime(volume, startSeconds);
|
||||
this.gain.gain.setValueAtTime(volume, endSeconds);
|
||||
this.gain.gain.exponentialRampToValueAtTime(1.0, endSeconds + this.rampLength);
|
||||
|
||||
this.input.connect(this.gain);
|
||||
this.gain.connect(this.output);
|
||||
|
||||
@@ -3,15 +3,20 @@ const jpegThumbnail = dataUrl => new Promise((resolve, reject) => {
|
||||
image.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
// TODO we may want to draw to a smaller, fixed size to optimize file size
|
||||
canvas.width = image.width;
|
||||
canvas.height = image.height;
|
||||
|
||||
const maxDimension = 96; // 3x the maximum displayed size of 32px
|
||||
|
||||
if (image.height > image.width) {
|
||||
canvas.height = maxDimension;
|
||||
canvas.width = (maxDimension / image.height) * image.width;
|
||||
} else {
|
||||
canvas.width = maxDimension;
|
||||
canvas.height = (maxDimension / image.width) * image.height;
|
||||
}
|
||||
|
||||
ctx.fillStyle = 'white'; // Create white background, since jpeg doesn't have transparency
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
ctx.drawImage(image, 0, 0);
|
||||
// TODO we can play with the `quality` option here to optimize file size
|
||||
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
|
||||
resolve(canvas.toDataURL('image/jpeg', 0.92 /* quality */)); // Default quality is 0.92
|
||||
};
|
||||
image.onerror = err => {
|
||||
|
||||
@@ -246,10 +246,7 @@ export default function (vm) {
|
||||
lookupBlocks = vm.runtime.flyoutBlocks;
|
||||
}
|
||||
const sort = function (options) {
|
||||
options.sort((str1, str2) => str1.localeCompare(str2, [], {
|
||||
sensitivity: 'base',
|
||||
numeric: true
|
||||
}));
|
||||
options.sort(ScratchBlocks.scratchBlocksUtils.compareStrings);
|
||||
};
|
||||
// Get all the stage variables (no lists) so we can add them to menu when the stage is selected.
|
||||
const stageVariableOptions = vm.runtime.getTargetForStage().getAllVariableNamesInScopeByType('');
|
||||
@@ -323,5 +320,26 @@ export default function (vm) {
|
||||
vm.runtime.emit('PLAY_NOTE', noteNum, extensionId);
|
||||
};
|
||||
|
||||
// Use a collator's compare instead of localeCompare which internally
|
||||
// creates a collator. Using this is a lot faster in browsers that create a
|
||||
// collator for every localeCompare call.
|
||||
const collator = new Intl.Collator([], {
|
||||
sensitivity: 'base',
|
||||
numeric: true
|
||||
});
|
||||
ScratchBlocks.scratchBlocksUtils.compareStrings = function (str1, str2) {
|
||||
return collator.compare(str1, str2);
|
||||
};
|
||||
|
||||
// Blocks wants to know if 3D CSS transforms are supported. The cross
|
||||
// section of browsers Scratch supports and browsers that support 3D CSS
|
||||
// transforms will make the return always true.
|
||||
//
|
||||
// Shortcutting to true lets us skip an expensive style recalculation when
|
||||
// first loading the Scratch editor.
|
||||
ScratchBlocks.utils.is3dSupported = function () {
|
||||
return true;
|
||||
};
|
||||
|
||||
return ScratchBlocks;
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
<svg version="1.1" id="cat" x="0px" y="0px" width="95px" height="111px" viewBox="0 0 95 111" enable-background="new 0 0 95 111" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g>
|
||||
<g id="Layer_3">
|
||||
<path fill="#FAA51D" stroke="#000000" d="M22.462,79.039c-2.415-0.451-5.304-1.309-7.742-3.503
		C9.268,70.629,7.526,62.535,3.672,64.622c-3.856,2.088-3.782,15.165,8.353,19.194c4.182,1.391,7.998,1.396,11.091,1.312
		c0.811-0.025,7.717-0.654,10.079-4.074c2.361-3.42,0.719-4.272-0.09-4.744C32.295,75.838,25.878,79.677,22.462,79.039z"/>
|
||||
<path fill="#FFFFFF" d="M4.236,64.877c-1.989,0.613-3.075,4.998-2.076,8.484c0.998,3.49,2.634,5.022,3.863,6.398
		c1.528,1.038-0.72-2.402,1.361-4.15c2.075-1.744,5.733-0.914,5.733-0.914s-2.909-3.987-4.57-6.396
		C6.975,65.988,6.359,64.375,4.236,64.877z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path fill="#FAA51D" d="M38.217,86.756c0,0-8.832,6.2-17.071,8.412l0.086,0.215c1.247,1.824,5.87,7.497-0.334,9.496
		c-5.333,1.717-15.12-13.104-10.821-15.902c2.626-1.713,4.892-0.252,4.892-0.252s3.474-1.07,6.001-2.345
		c4.303-2.161,5.784-3.453,5.784-3.453s4.184-4.306,6.856-4.137C36.281,78.96,41.669,83.504,38.217,86.756z"/>
|
||||
<path fill="none" stroke="#231F20" stroke-width="1.2" d="M21.232,95.383c1.247,1.824,5.87,7.497-0.334,9.496
		c-5.333,1.717-15.329-13.344-11.03-16.145c2.626-1.713,5.101-0.01,5.101-0.01s3.474-1.072,6.001-2.348
		c4.303-2.161,5.784-3.453,5.784-3.453"/>
|
||||
<path fill="none" stroke="#231F20" stroke-width="1.2" d="M38.217,86.756c0,0-10.123,7.107-18.804,8.819"/>
|
||||
</g>
|
||||
<path fill="#FAA51D" stroke="#231F20" stroke-width="1.2" d="M52.169,74.885c0,0,1.235,0.165,4.744,3.676
	c3.509,3.508,6.026,2.16,8.911,0.724c2.877-1.443,10.537-6.126,6.49-9.817c-4.049-3.688-6.207,1.146-9.715,2.405
	c-3.512,1.26-5.061-2.487-6.858-4.287c-0.589-0.593-1.188-1.099-1.729-1.505c0,0-0.971-0.76-1.906,2.79
	C51.172,72.412,50.162,73.415,52.169,74.885z"/>
|
||||
<g id="Layer_2_1_">
|
||||
<path fill="#FAA51D" stroke="#231F20" stroke-width="1.2" d="M46.753,82.012c1.188-0.912,2.397-2.402,3.951-4.713
		c1.296-1.927,2.7-5.578,2.7-5.578c0.875-2.521,1.934-6.576-1.902-7.296c-1.553-0.291-4.079-0.098-7.67-0.776
		c-3.593-0.681-6.798-2.522-9.517,2.233c-2.718,4.757-9.59,8.271-1.056,16.563c0,0,4.901,3.842,10.764,9.639
		c4.831,4.775,12.045,10.602,12.045,10.602s18.972,2.188,19.535-0.693c1.922-9.79-14.777-6.911-14.777-6.911
		s-4.605-3.933-6.725-5.794c-3.478-3.059-11.125-10.771-11.125-10.771"/>
|
||||
<path fill="#FFFFFF" d="M51.253,75.434c0,0,2.47-2.66-2.469-5.317c-4.939-2.657-7.213-0.017-8.739,1.521
		c-2.644,2.655,3.443,6.611,3.443,6.611l3.176,3.204c0,0,1.738-1.647,2.499-2.979C50.036,77.26,51.253,75.434,51.253,75.434"/>
|
||||
</g>
|
||||
<g id="Layer_8"/>
|
||||
<path fill="#FAA51D" stroke="#231F20" stroke-width="1.2" d="M29.926,73.218c0.749-0.571,2.889-2.202,4.854-3.657
	c2.428-1.799,6.117-5.849,1.077-7.646c-5.04-1.801-7.507,1.604-11.519,4.946c-2.159,1.801-5.308,2.699-4.319,6.209
	c0.993,3.511,4.862,13.408,11.789,10.17c6.929-3.239-1.799-9.18-3.06-11.157"/>
|
||||
<g id="Layer_2">
|
||||
<path fill="#FAA51D" stroke="#231F20" stroke-width="1.2" d="M52.709,14.156c-1.54-0.143-4.75-0.316-6.518-0.231
		c-4.728,0.225-9.224,1.928-9.224,1.928L23.949,7.357l2.235,18.906c0.646-0.782-10.555,12.804-3.479,24.224
		c7.08,11.426,22.233,16.518,40.988,12.792c18.755-3.729,23.229-14.531,21.986-20.246c-1.242-5.714-8.322-7.823-8.322-7.823
		s-0.09-4.48-3.328-9.97c-1.926-3.268-8.348-8.041-8.348-8.041L62.822,5.647l-7.452,7.204L52.709,14.156z"/>
|
||||
<path fill="#FFFFFF" d="M76.42,35.066l-2.482-2.064l-9.115,2.661c0,0,0,3.419-4.367,4.367c-4.37,0.951-11.211-2.277-11.211-2.277
		L41.46,41.17c0,0-8.437,0.928-8.739,6.081C32.048,58.704,46.1,63.479,51.425,63.783c2.905,0.167,8.235-0.338,12.277-1.141
		c17.752-3.234,22.551-13.919,21.31-19.635c-1.242-5.714-7.978-7.196-7.978-7.196L76.42,35.066z"/>
|
||||
<path fill="none" stroke="#231F20" stroke-width="1.2" d="M10.673,46.155c0,0,4.107,0.374,5.974,0.268
		c1.865-0.107,5.492-0.587,5.492-0.587"/>
|
||||
<path fill="none" stroke="#231F20" stroke-width="1.2" d="M81.656,40.671c0,0,4.549-0.743,6.859-1.549
		c2.715-0.942,4.543-2.545,4.543-2.545"/>
|
||||
<path fill="none" stroke="#231F20" stroke-width="1.2" d="M22.337,41.909c0,0-2.384-1.777-6.117-3.43
		c-4.134-1.831-6.405-2.303-6.405-2.303"/>
|
||||
<path fill="none" stroke="#231F20" stroke-width="1.2" d="M82.117,46.622c0,0,2.726,1.104,5.533,1.385
		c2.77,0.276,4.646,0.11,4.646,0.11"/>
|
||||
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-miterlimit="10" d="M52.35,14.212
		c2.84,0.7,3.887,1.469,3.887,1.469"/>
|
||||
<line fill="none" stroke="#000000" x1="33.898" y1="13.684" x2="39.956" y2="18.042"/>
|
||||
</g>
|
||||
<g id="Layer_5">
|
||||
<path fill="#FFFFFF" stroke="#231F20" d="M71.84,25.366c2.924,4.479,3.033,9.591,0.242,11.415
		c-2.793,1.825-7.426-0.332-10.354-4.813c-2.933-4.48-3.037-9.589-0.244-11.415C64.275,18.73,68.913,20.884,71.84,25.366z"/>
|
||||
<path fill="#231F20" d="M71.089,32.522c0,1.08-0.802,1.956-1.8,1.956c-0.993,0-1.803-0.877-1.803-1.956
		c0-1.08,0.81-1.958,1.803-1.958C70.287,30.564,71.089,31.442,71.089,32.522"/>
|
||||
</g>
|
||||
<g id="Layer_7">
|
||||
<path fill="#FFFFFF" stroke="#231F20" d="M47.867,28.619c2.926,4.48,2.619,9.862-0.681,12.015
		c-3.302,2.159-8.351,0.272-11.276-4.208c-2.928-4.48-2.624-9.86,0.678-12.017C39.891,22.253,44.938,24.137,47.867,28.619z"/>
|
||||
<path fill="#231F20" d="M46.079,34.507c0,1.081-0.803,1.957-1.801,1.957c-0.992,0-1.803-0.878-1.803-1.957
		c0-1.08,0.811-1.957,1.803-1.957C45.274,32.55,46.079,33.427,46.079,34.507"/>
|
||||
</g>
|
||||
<path fill="#5E4A42" stroke="#000000" d="M59.766,37.926c1.854,0,4.555-0.284,4.697,0.569c0.143,0.855-1.709,4.203-2.988,4.345
	c-1.283,0.142-6.125-2.353-6.195-3.919C55.206,37.355,58.055,37.926,59.766,37.926z"/>
|
||||
<g id="Layer_4">
|
||||
<path fill="none" stroke="#231F20" stroke-width="1.2" d="M46.774,45.235c0,0,10.347,3.054,14.217,3.897
		c3.868,0.842,10.851,1.684,10.851,1.684s-7.99,10.245-17.328,7.644C45.176,55.863,45.345,49.975,46.774,45.235z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 6.5 KiB |
@@ -1,37 +0,0 @@
|
||||
<svg version="1.1" id="cat" x="0px" y="0px" width="95px" height="111px" viewBox="0 0 95 111" enable-background="new 0 0 95 111" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g>
|
||||
<g id="Layer_8">
|
||||
<path fill="#FAA51D" stroke="#231F20" stroke-width="1.2" d="M23.754,82.506c-2.871-1.402-10.511-2.518-14.771-6.695
		c-9.431-9.246-3.824-21.732,0.704-21.797c3.611-0.061,3.363,5.482,4.622,11.145c0.875,3.939,1.673,6.988,6.407,9.674
		c4.735,2.688,6.918,2.916,6.918,2.916s3.258,0.791,1.596,2.562C27.568,82.076,23.754,82.506,23.754,82.506z"/>
|
||||
<path fill="#FFFFFF" d="M14.291,66.369c-0.115-0.459-0.222-0.928-0.33-1.413c-1.172-5.271-0.941-10.432-4.303-10.377
		c-3.433,0.05-7.53,7.771-4.19,15.285c0.193-0.143,0.399-0.336,0.569-0.588c0.451-0.664,0.821-2.969,2.654-3.688
		C9.989,65.082,12.778,65.869,14.291,66.369z"/>
|
||||
</g>
|
||||
<path fill="#FAA51D" stroke="#231F20" stroke-width="1.2" d="M52.543,61.473c0,0,1.455,2.342,1.519,4.969
	c0.065,2.723-0.351,6.984,1.804,8.158c3.381,1.842,9.959-0.463,10.174,5.191c0.211,5.652-10.152,3.623-13.299,2.547
	c-2.695-0.926-4.559-1.758-5.18-5.182c0,0-3.764-13.969-0.196-15.59C50.933,59.945,52.543,61.473,52.543,61.473z"/>
|
||||
<path fill="#FAA51D" stroke="#231F20" stroke-width="1.2" d="M14.61,89.584c0,0,1.504,10.391-5.922,8.387
	c-5.586-1.51-5.705-20.68-0.406-20.691c3.237-0.006,4.425,2.859,4.425,2.859s4.385,4.812,6.95,4.312
	c2.565-0.502,7.978-6.703,7.978-6.703s2.875-2.539,6.146,0.023c3.271,2.561,1.521,6.178,1.521,6.178s-6.07,7.072-11.014,8.072
	c-4.945,1.002-9.375-2.568-11.58-3.451"/>
|
||||
<g id="Layer_6">
|
||||
<path fill="#FAA51D" stroke="#231F20" stroke-width="1.2" d="M42.435,76.238c0,0,4.866,4.062,8.166,7.52
		c4.384,4.592-3.339,15.039-3.339,15.039s11.234,6.467,2.998,11c-2.657,1.467-15.661-13.225-15.661-13.225s7.979-7.475,4.972-10.172
		c-4.964-4.449-10.771-4.959-11.6-9.824c-0.502-2.957,0.613-4.502,2.658-7.053c1.354-1.689,2.084-3.182,2.084-3.182
		s1.135-7.238,4.289-8.254c3.153-1.014,8.842,0.057,10.758,0.395c1.914,0.338,6.645,1.877,4.617,9.922c0,0-1.451,3.771-2.789,5.762
		c-1.604,2.389-2.854,3.924-4.08,4.867"/>
|
||||
<path fill="#FFFFFF" d="M50.182,72.631c0,0,2.55-2.748-2.551-5.494c-5.102-2.746-7.449-0.016-9.024,1.572
		c-2.731,2.742,3.334,7.453,3.334,7.453l3.383,2.564c0,0,1.916-1.582,2.701-2.957C48.807,74.396,50.182,72.631,50.182,72.631"/>
|
||||
</g>
|
||||
<path fill="#FAA51D" stroke="#231F20" stroke-width="1.2" d="M24.428,65.395c0.939,0.057,3.625,0.213,6.064,0.391
	c3.014,0.217,8.451-0.451,5.801-5.098c-2.649-4.65-6.74-3.674-11.961-3.754c-2.812-0.043-5.785-1.412-7.322,1.895
	c-1.534,3.312-5.051,13.338,2.313,15.396c7.366,2.057,4.618-8.137,4.95-10.457"/>
|
||||
<g id="Layer_2">
|
||||
<path fill="#FAA51D" stroke="#231F20" stroke-width="1.2" d="M51.709,10.156c-1.54-0.143-4.75-0.316-6.518-0.231
		c-4.728,0.225-9.224,1.928-9.224,1.928L22.949,3.357l2.235,18.906c0.646-0.782-10.555,12.804-3.479,24.224
		c7.08,11.426,22.233,16.518,40.988,12.792c18.755-3.728,23.229-14.531,21.986-20.246c-1.242-5.714-8.322-7.823-8.322-7.823
		s-0.09-4.48-3.328-9.97c-1.926-3.268-8.348-8.041-8.348-8.041L61.822,1.647L54.37,8.851L51.709,10.156z"/>
|
||||
<path fill="#FFFFFF" d="M75.42,31.066l-2.483-2.064l-9.115,2.661c0,0,0,3.419-4.367,4.367c-4.37,0.951-11.211-2.277-11.211-2.277
		l-7.784,3.417c0,0-8.437,0.928-8.739,6.081C31.048,54.704,45.1,59.478,50.425,59.783c2.905,0.167,8.235-0.337,12.277-1.14
		c17.752-3.235,22.551-13.92,21.309-19.635c-1.242-5.714-7.977-7.196-7.977-7.196L75.42,31.066z"/>
|
||||
<path fill="none" stroke="#231F20" stroke-width="1.2" d="M9.673,42.155c0,0,4.107,0.374,5.974,0.268
		c1.865-0.107,5.492-0.587,5.492-0.587"/>
|
||||
<path fill="none" stroke="#231F20" stroke-width="1.2" d="M80.656,36.671c0,0,4.549-0.743,6.859-1.549
		c2.715-0.942,4.543-2.545,4.543-2.545"/>
|
||||
<path fill="none" stroke="#231F20" stroke-width="1.2" d="M21.337,37.909c0,0-2.384-1.777-6.117-3.43
		c-4.134-1.831-6.405-2.303-6.405-2.303"/>
|
||||
<path fill="none" stroke="#231F20" stroke-width="1.2" d="M81.117,42.622c0,0,2.726,1.104,5.533,1.385
		c2.77,0.276,4.647,0.11,4.647,0.11"/>
|
||||
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-miterlimit="10" d="M51.349,10.212
		c2.84,0.7,3.888,1.469,3.888,1.469"/>
|
||||
<line fill="none" stroke="#000000" x1="32.898" y1="9.684" x2="38.956" y2="14.042"/>
|
||||
</g>
|
||||
<g id="Layer_5">
|
||||
<path fill="#FFFFFF" stroke="#231F20" d="M70.84,21.366c2.924,4.479,3.033,9.591,0.242,11.415
		c-2.793,1.825-7.426-0.332-10.354-4.813c-2.932-4.48-3.037-9.589-0.244-11.415C63.275,14.73,67.913,16.884,70.84,21.366z"/>
|
||||
<path fill="#231F20" d="M70.089,28.522c0,1.08-0.802,1.956-1.8,1.956c-0.993,0-1.803-0.877-1.803-1.956
		c0-1.08,0.81-1.958,1.803-1.958C69.287,26.564,70.089,27.442,70.089,28.522"/>
|
||||
</g>
|
||||
<g id="Layer_7">
|
||||
<path fill="#FFFFFF" stroke="#231F20" d="M46.867,24.619c2.926,4.48,2.619,9.862-0.681,12.015
		c-3.302,2.159-8.351,0.272-11.276-4.208c-2.928-4.48-2.624-9.86,0.678-12.017C38.891,18.253,43.938,20.137,46.867,24.619z"/>
|
||||
<path fill="#231F20" d="M45.079,30.507c0,1.081-0.803,1.957-1.801,1.957c-0.992,0-1.803-0.878-1.803-1.957
		c0-1.08,0.811-1.957,1.803-1.957C44.274,28.55,45.079,29.427,45.079,30.507"/>
|
||||
</g>
|
||||
<path fill="#5E4A42" stroke="#000000" d="M58.766,33.926c1.854,0,4.555-0.284,4.697,0.569c0.143,0.855-1.709,4.203-2.988,4.345
	c-1.283,0.142-6.125-2.353-6.195-3.919C54.206,33.355,57.055,33.926,58.766,33.926z"/>
|
||||
<g id="Layer_4">
|
||||
<path fill="none" stroke="#231F20" stroke-width="1.2" d="M45.774,41.235c0,0,10.347,3.054,14.217,3.897
		c3.868,0.842,10.851,1.684,10.851,1.684s-7.99,10.245-17.328,7.644C44.176,51.863,44.345,45.975,45.774,41.235z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 6.1 KiB |
@@ -0,0 +1,42 @@
|
||||
<svg width="96px" height="101px" viewBox="0 0 96 101" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 52.5 (67469) - http://www.bohemiancoding.com/sketch -->
|
||||
<g>
|
||||
<title>costume1.1</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="costume1" transform="translate(-13.000000, -10.000000)">
|
||||
<g id="costume1.1" transform="translate(13.000000, 10.000000)">
|
||||
<g id="tail" transform="translate(0.000000, 59.000000)">
|
||||
<path d="M21.9,14.8 C19.5,14.3 16.6,13.5 14.2,11.3 C8.7,6.4 7,-1.7 3.2,0.4 C-0.7,2.5 -0.6,15.6 11.6,19.6 C15.8,21 19.6,21 22.7,20.9 C23.5,20.9 30.4,20.2 32.8,16.8 C35.2,13.4 33.5,12.5 32.7,12.1 C31.8,11.6 25.3,15.4 21.9,14.8 Z" stroke="#001026" stroke-width="1.2" fill="#FFAB19" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3.8,0.6 C1.8,1.2 0.8,5.4 1.8,8.9 C2.8,12.4 4.4,14.2 5.7,15.5 C5.5,14.8 5.1,12.6 6.8,11.3 C8.9,9.6 12.6,10.5 12.6,10.5 C12.6,10.5 9.5,6.7 7.9,4 C6.3,1.7 5.8,0.2 3.8,0.6 Z" id="detail" fill="#FFFFFF"/>
|
||||
</g>
|
||||
<path d="M37.7,81.5 C35.9,82.7 29.7,87.1 21.8,89.6 L21.4,89.7 C21,89.8 20.8,90.3 21,90.7 C22.7,93.1 25.8,97.9 20.3,99.6 C15,101.3 5.1,87.2 9.3,83.5 C11.2,82.1 12.9,82.8 13.8,83.2 C14.3,83.4 14.8,83.4 15.3,83.3 C16.5,82.9 18.7,82.1 20.4,81.2 C24.7,79 25.7,78.1 27.7,76.6 C29.7,75.1 34.3,71.4 38,74.6 C41.2,77.3 39.4,80.3 37.7,81.5 Z" id="leg" stroke="#001026" stroke-width="1.2" fill="#FFAB19" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M53.6,60.7 C54.1,61.1 60.2,68.3 62.2,66.5 C64.6,64.4 67.9,60.3 71.5,63.6 C75.1,66.9 68.3,72.5 65.4,74 C58.5,77.1 52.9,71.2 51.7,69.6 C50.5,68 48.4,65.3 48.4,62.7 C48.5,59.9 51.9,59.2 53.6,60.7 Z" id="arm" stroke="#001026" stroke-width="1.2" fill="#FFAB19" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<g id="body-and-leg" transform="translate(28.000000, 57.000000)">
|
||||
<path d="M18.2,19.7 C19.4,18.8 20.6,17.3 22.2,15 C23.5,13.1 24.9,9.4 24.9,9.4 C25.8,6.9 26.4,2.1 23.1,2.2 C20.9,2.3 18.9,2 15.5,1.5 C9.5,0.3 8.4,-0.5 5.9,3.6 C3.2,8.4 -3.7,11.9 4.8,20.2 C4.8,20.2 9.7,24 15.6,29.8 C19.6,33.7 25.9,39.3 28.1,41.2 C28.6,41.6 29.2,41.8 29.8,41.9 C39.5,42.8 46.7,41.8 46.7,37.5 C46.7,30.3 32.4,32.8 32.4,32.8 C32.4,32.8 27.8,28.9 25.7,27 L18.2,19.7 Z" id="body" stroke="#001026" stroke-width="1.2" fill="#FFAB19" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M22.6,13 C22.6,13 24.5,10.5 20.2,7.8 C15.7,4.9 14,8.1 12.2,10.5 C10.2,13.6 12.2,15.1 14.2,16.9 C15.8,18.4 17.3,19.6 17.3,19.6 C17.3,19.6 20.4,17.5 22.6,13 Z" id="tummy" fill="#FFFFFF"/>
|
||||
</g>
|
||||
<path d="M30.2,68.4 C32.4,71.2 35.8,74.7 31.5,77.6 C25.6,80.9 20.7,70.9 19.7,67.4 C18.8,64.3 21.4,62.3 23.6,60.6 C27.9,57.5 31.5,54.7 35.5,56.2 C40.5,58 36.9,62 34.4,63.8 C32.9,64.9 31.4,66.1 30.3,66.8 C30,67.3 29.9,67.9 30.2,68.4 Z" id="arm" stroke="#001026" stroke-width="1.2" fill="#FFAB19" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<g id="head" transform="translate(14.000000, 0.000000)">
|
||||
<path d="M39.1,9 C36.8,8.6 34.4,8.4 31.6,8.6 C26.9,8.8 22.4,10.5 22.4,10.5 L10.3,2.6 C9.9,2.4 9.4,2.7 9.5,3.1 L11.6,21 C12.2,20.2 1,33.8 8.1,45.2 C15.2,56.6 30.3,61.7 49.1,58 C67.9,54.3 72.3,43.5 71.1,37.8 C69.9,32.1 62.8,30 62.8,30 C62.8,30 62.7,25.5 59.5,20 C57.6,16.7 51.2,12 51.2,12 L48.6,1.3 C48.5,0.9 48,0.8 47.7,1 L39.1,9 Z" stroke="#001026" stroke-width="1.2" fill="#FFAB19"/>
|
||||
<path d="M62.5,30.4 C62.5,30.4 69.4,32.2 70.6,37.9 C71.8,43.6 67,53.9 48.4,57.5 C24.2,62.5 12.7,48.1 19.4,37.5 C26.1,26.8 37.6,35.9 46,35.3 C53.2,34.8 54,28.5 62.5,30.4 Z" id="face" fill="#FFFFFF"/>
|
||||
<path d="M31,41.1 C31,40.7 31.4,40.4 31.8,40.5 C33.7,41.2 39.1,42.8 45.1,43.2 C50.5,43.5 53.7,43.2 55.2,42.9 C55.7,42.8 56.1,43.3 55.9,43.8 C55,46.5 51.2,54 40.7,53.4 C31.6,52.4 30.7,46 31,41.1 Z" id="mouth" stroke="#001026" stroke-width="1.2" fill="#FFFFFF" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M69,35.4 C69,35.4 76.2,35.3 80.9,31.5" id="whisker" stroke="#001026" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M69.4,41.3 C69.4,41.3 73.3,43.2 79.6,42.7" id="whisker" stroke="#001026" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M45.6,32.7 C47.7,32.7 49.9,32.9 50,33.6 C50.1,35 48.6,37.8 47,37.9 C45.2,38.1 41,35.6 41,34 C40.9,32.8 43.6,32.7 45.6,32.7 Z" id="nose" stroke="#001026" stroke-width="1.2" fill="#001026" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M0.6,31.2 C0.6,31.2 9.2,34 12.7,37.1" id="whisker" stroke="#001026" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M1.3,41.2 C1.3,41.2 8.7,42.3 13,40.6" id="whisker" stroke="#001026" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<g id="eye" transform="translate(44.000000, 15.000000)">
|
||||
<path d="M13.4,6 C16.3,10.5 16.4,15.6 13.6,17.4 C10.8,19.2 6.2,17.1 3.2,12.6 C0.3,8.1 0.2,3 3,1.2 C5.8,-0.7 10.5,1.5 13.4,6 Z" id="pupil" stroke="#001026" stroke-width="1.2" fill="#FFFFFF" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13,11.7 C13,12.8 12.2,13.7 11.2,13.7 C10.2,13.7 9.4,12.8 9.4,11.7 C9.4,10.6 10.2,9.7 11.2,9.7 C12.2,9.7 13,10.6 13,11.7" id="pupil" fill="#001026"/>
|
||||
</g>
|
||||
<g id="eye" transform="translate(19.000000, 18.000000)">
|
||||
<path d="M13.6,5.8 C16.6,10.2 16.4,15.6 13.7,17.5 C10.4,19.4 6,18 3,13.6 C-0.1,9.2 -0.3,3.5 2.8,1.3 C5.9,-1 10.6,1.4 13.6,5.8 Z" stroke="#001026" stroke-width="1.2" fill="#FFFFFF" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13,11.6 C13,12.7 12.2,13.6 11.2,13.6 C10.2,13.6 9.4,12.7 9.4,11.6 C9.4,10.5 10.2,9.6 11.2,9.6 C12.2,9.7 13,10.5 13,11.6" id="pupil" fill="#001026"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.9 KiB |