Compare commits
640 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d658dc159 | ||
|
|
fb8e0afd85 | ||
|
|
5263839731 | ||
|
|
484063a596 | ||
|
|
5650c7f778 | ||
|
|
74728782f3 | ||
|
|
0a0980f9e3 | ||
|
|
26c46b8488 | ||
|
|
c469696687 | ||
|
|
27550a7781 | ||
|
|
e79bacd268 | ||
|
|
fd2dfcc4f2 | ||
|
|
74b9e364fe | ||
|
|
d2412679ea | ||
|
|
98e6fc3b4a | ||
|
|
b64b2a3091 | ||
|
|
6275f3abf7 | ||
|
|
22e836c98a | ||
|
|
d6c0489fa3 | ||
|
|
78728ce4ca | ||
|
|
6803a34b51 | ||
|
|
42b6383283 | ||
|
|
147f009279 | ||
|
|
b899781b69 | ||
|
|
64d0819ab4 | ||
|
|
3ea4af816a | ||
|
|
037d2c6974 | ||
|
|
fe2c664474 | ||
|
|
a3d429b9c3 | ||
|
|
d0e6f4118d | ||
|
|
75fb58e454 | ||
|
|
6563b7fd11 | ||
|
|
e1b78174e2 | ||
|
|
aab7e25f80 | ||
|
|
24b2ba7b35 | ||
|
|
20ae667b3b | ||
|
|
d8cb9da483 | ||
|
|
8abce2ccf3 | ||
|
|
aa3ebd3bd2 | ||
|
|
4929437283 | ||
|
|
389dd19a8a | ||
|
|
a8b6b3335b | ||
|
|
a925e2fae0 | ||
|
|
dc79b5923e | ||
|
|
b39bbaf6fb | ||
|
|
14b40d820b | ||
|
|
354d5963ec | ||
|
|
6a32b94336 | ||
|
|
5c68437b1c | ||
|
|
0106f280f0 | ||
|
|
b567aaac69 | ||
|
|
36b419202b | ||
|
|
c80d01c648 | ||
|
|
1d00646b17 | ||
|
|
e4b98fe65a | ||
|
|
12443db891 | ||
|
|
03f08de32f | ||
|
|
c18f488be7 | ||
|
|
099012fac9 | ||
|
|
fa510af65a | ||
|
|
46b514cc61 | ||
|
|
019c8814f6 | ||
|
|
46249935b2 | ||
|
|
ebecc6bb81 | ||
|
|
130ddca135 | ||
|
|
f2661989dc | ||
|
|
3f6cb8c356 | ||
|
|
228d9cca6c | ||
|
|
d08a1dd2ca | ||
|
|
7d76a60db7 | ||
|
|
e4f0865067 | ||
|
|
cad4bd7c04 | ||
|
|
f85aaf0370 | ||
|
|
312d78617d | ||
|
|
af5aa12fa1 | ||
|
|
1c7e2edae0 | ||
|
|
a49eee9685 | ||
|
|
61938c8e66 | ||
|
|
76d10f5920 | ||
|
|
2cf85926e9 | ||
|
|
afb099f9df | ||
|
|
343627eb82 | ||
|
|
86f2477c63 | ||
|
|
2684150141 | ||
|
|
3f8d8d055d | ||
|
|
e775e0542e | ||
|
|
7992ba5bc1 | ||
|
|
d24bbaa65a | ||
|
|
58a91e1b86 | ||
|
|
cfed1d0230 | ||
|
|
b14c70f4b6 | ||
|
|
ad77fc20c6 | ||
|
|
029633f3d3 | ||
|
|
78459d759c | ||
|
|
a0d5a3dd07 | ||
|
|
d76d983d5d | ||
|
|
68b8e7dc7c | ||
|
|
1a7aff5102 | ||
|
|
677edffb80 | ||
|
|
2cd90c998b | ||
|
|
346e604f34 | ||
|
|
8d41111cd6 | ||
|
|
3163edcbe4 | ||
|
|
18df43c9cb | ||
|
|
4f2179c4d7 | ||
|
|
b89546ac22 | ||
|
|
c0ad7635f2 | ||
|
|
a82688163e | ||
|
|
29c36ee110 | ||
|
|
43698f8d61 | ||
|
|
b5b29aeb17 | ||
|
|
fdcf4c152a | ||
|
|
dcfda9521b | ||
|
|
950c9cdaeb | ||
|
|
1e9641a40e | ||
|
|
3fd2537311 | ||
|
|
6d470b8eba | ||
|
|
71ad81a67d | ||
|
|
9a1852ea05 | ||
|
|
629a86de99 | ||
|
|
3539868683 | ||
|
|
71d4566df5 | ||
|
|
caaa613ce9 | ||
|
|
cf36a33aea | ||
|
|
a777a808ee | ||
|
|
a78150b7ad | ||
|
|
deb177c6bb | ||
|
|
1c5e47b4c4 | ||
|
|
aae61f9451 | ||
|
|
807c44f057 | ||
|
|
9782007f7e | ||
|
|
77d5f1e603 | ||
|
|
81ee6de0a3 | ||
|
|
82fe65ada2 | ||
|
|
ce79d7b745 | ||
|
|
755cc4f5ec | ||
|
|
fde4d311e3 | ||
|
|
b08f40aaa3 | ||
|
|
e12ade6b31 | ||
|
|
43fc80ef41 | ||
|
|
6ca96157f6 | ||
|
|
856181ea54 | ||
|
|
f84bd46cdc | ||
|
|
0b43924ee2 | ||
|
|
16d3fd3828 | ||
|
|
cf5defa6a9 | ||
|
|
3de760db12 | ||
|
|
1366f0b68e | ||
|
|
be498e0bd3 | ||
|
|
a4e13f032a | ||
|
|
fef3136b1b | ||
|
|
6b318c248f | ||
|
|
fd6a3a5579 | ||
|
|
2292267e39 | ||
|
|
dbfae53222 | ||
|
|
d39ed267f3 | ||
|
|
9d04ad704a | ||
|
|
11cbb4df23 | ||
|
|
232911f725 | ||
|
|
41a0c6c73f | ||
|
|
10e80edb1d | ||
|
|
af3848586c | ||
|
|
48807bf030 | ||
|
|
21a14cb225 | ||
|
|
b725d07744 | ||
|
|
94e707da8a | ||
|
|
4fb4041f13 | ||
|
|
545da556d2 | ||
|
|
9f3da3454f | ||
|
|
15105426b1 | ||
|
|
b2d2e27945 | ||
|
|
d93a4cf9f3 | ||
|
|
8c3ec5da3d | ||
|
|
d067b1d55a | ||
|
|
55070ace8b | ||
|
|
0f35961173 | ||
|
|
b63f68bf74 | ||
|
|
8c6d6db07e | ||
|
|
a74a70fd8c | ||
|
|
97ad674be2 | ||
|
|
81534d2c13 | ||
|
|
51b7036456 | ||
|
|
e40fdad251 | ||
|
|
e4b0e97ac0 | ||
|
|
003320ca34 | ||
|
|
780addc50b | ||
|
|
8892a5f43d | ||
|
|
75e52ebca7 | ||
|
|
e7b3bbcd0e | ||
|
|
b9d60639e2 | ||
|
|
2ce0ce65e1 | ||
|
|
a32bdcb06a | ||
|
|
f9f8dff8b4 | ||
|
|
5c5cd1e501 | ||
|
|
89bcd1c4a8 | ||
|
|
6b7b142961 | ||
|
|
565e47aef8 | ||
|
|
dd448cb3ed | ||
|
|
7a9a19e3b9 | ||
|
|
a6e94441aa | ||
|
|
68d66d8ab3 | ||
|
|
aa5cc7ff9f | ||
|
|
eb8ede8376 | ||
|
|
4dc92bae4e | ||
|
|
9e1a975b23 | ||
|
|
e5a26fa0be | ||
|
|
4035a3a1e5 | ||
|
|
0380d286cd | ||
|
|
91dc3aa5d2 | ||
|
|
4bbffcf35e | ||
|
|
bb73eeb7ea | ||
|
|
546f8a12e4 | ||
|
|
3a865d4aaa | ||
|
|
05feb6b1b0 | ||
|
|
efa6fe3ef1 | ||
|
|
e917927979 | ||
|
|
61689ed451 | ||
|
|
b74919c376 | ||
|
|
67cc4f9600 | ||
|
|
ec749de3fa | ||
|
|
b99cd7ed3f | ||
|
|
87b9c955ca | ||
|
|
00aee2771e | ||
|
|
291276494d | ||
|
|
fc3823f0af | ||
|
|
855ef73a9a | ||
|
|
517a8d2104 | ||
|
|
a042795ec1 | ||
|
|
e427d65bf5 | ||
|
|
5ce319456f | ||
|
|
03828bfdaa | ||
|
|
7c836d121c | ||
|
|
b2ee3c5f30 | ||
|
|
04205783b8 | ||
|
|
f27e71fbd8 | ||
|
|
fbca679334 | ||
|
|
5b594ba0b4 | ||
|
|
3d4b5b0c1e | ||
|
|
c7574de7dc | ||
|
|
c8a3bb5f81 | ||
|
|
118edef773 | ||
|
|
050a1aff83 | ||
|
|
9ad74e9cbf | ||
|
|
09f8decc59 | ||
|
|
0decdf156b | ||
|
|
9316655b8c | ||
|
|
1bcc4fd2d7 | ||
|
|
239a4da145 | ||
|
|
d8e66c9b6a | ||
|
|
72279f4995 | ||
|
|
48b21de011 | ||
|
|
f7dc86ab2b | ||
|
|
39bfe6d2cb | ||
|
|
d6f534c3c0 | ||
|
|
45067d0354 | ||
|
|
da1ff63f72 | ||
|
|
5d715c50de | ||
|
|
47072ae1fe | ||
|
|
72f301fa45 | ||
|
|
e9b89629a6 | ||
|
|
3ef5ef166f | ||
|
|
37ca7a706a | ||
|
|
734c65fbda | ||
|
|
145605d628 | ||
|
|
7ac432fbc5 | ||
|
|
0c92cec2ea | ||
|
|
f6a788b36f | ||
|
|
72dffcf46b | ||
|
|
3140cdd148 | ||
|
|
d87adbce63 | ||
|
|
ffdf2bc0cd | ||
|
|
be5d7a1c9f | ||
|
|
94288b5cef | ||
|
|
63abbf5949 | ||
|
|
49214281f7 | ||
|
|
9688dde1a4 | ||
|
|
fdcc31f049 | ||
|
|
55ed6100e0 | ||
|
|
7fb11ba912 | ||
|
|
f3d77fdcf2 | ||
|
|
6489ab6a56 | ||
|
|
bace117ada | ||
|
|
76175d61af | ||
|
|
87110095a0 | ||
|
|
50ba8bec5a | ||
|
|
1741b1c686 | ||
|
|
99097baf9d | ||
|
|
bac1cc8243 | ||
|
|
096489d486 | ||
|
|
9811a9a3e1 | ||
|
|
910cde4380 | ||
|
|
4f1ccf83c8 | ||
|
|
9501c1ce4b | ||
|
|
069f0e53e1 | ||
|
|
aedfba795e | ||
|
|
9f162c0703 | ||
|
|
7b96c46e39 | ||
|
|
3d48ea71b9 | ||
|
|
24ee984a2e | ||
|
|
4255cbe540 | ||
|
|
fe16f24c41 | ||
|
|
a6e1fc5c44 | ||
|
|
8434312728 | ||
|
|
46f641aaec | ||
|
|
c246d8d517 | ||
|
|
4ec1aeafaf | ||
|
|
27cfd04ea7 | ||
|
|
797cfcb98d | ||
|
|
96a9b52e6d | ||
|
|
3b7462070b | ||
|
|
f08dd5960b | ||
|
|
9972196f70 | ||
|
|
ebbf06787c | ||
|
|
1d2b0cb093 | ||
|
|
6f27c6e4aa | ||
|
|
7b6008c37e | ||
|
|
cf5405fbe4 | ||
|
|
b4ec7402fc | ||
|
|
ff9a107a29 | ||
|
|
417ad87bcc | ||
|
|
265f99f327 | ||
|
|
0f8c3caf18 | ||
|
|
3a7677b46d | ||
|
|
3fd64d9956 | ||
|
|
1d26f4b24f | ||
|
|
1a4cea7b4f | ||
|
|
bbb1c42641 | ||
|
|
7ce2320eda | ||
|
|
1729b30f89 | ||
|
|
25d6595d5e | ||
|
|
3b31b9d65b | ||
|
|
e9e64fed3f | ||
|
|
3b88ea9b1c | ||
|
|
623f76fa6c | ||
|
|
19efcb1a0d | ||
|
|
31c8916622 | ||
|
|
7a0b189a1d | ||
|
|
fa0d56d57a | ||
|
|
85670bbc6a | ||
|
|
fc9a85b6ad | ||
|
|
8760b132da | ||
|
|
b80ee8d778 | ||
|
|
c8e168aa3e | ||
|
|
106aef579f | ||
|
|
d7084829c3 | ||
|
|
243d72eed5 | ||
|
|
aa60e97d5d | ||
|
|
4ae12db99c | ||
|
|
98ad058e2e | ||
|
|
58cc381abf | ||
|
|
d039c38f00 | ||
|
|
81f3347981 | ||
|
|
b96d2949f7 | ||
|
|
a2e745a349 | ||
|
|
0fe02b18ce | ||
|
|
655eb8c253 | ||
|
|
d9356f8171 | ||
|
|
be7d23163c | ||
|
|
7ac2e57484 | ||
|
|
727ea1283a | ||
|
|
3314057059 | ||
|
|
df1b00fa2c | ||
|
|
6434329f61 | ||
|
|
454853dc93 | ||
|
|
b663bf94e4 | ||
|
|
081cf081c5 | ||
|
|
f86b164c62 | ||
|
|
92b79e8272 | ||
|
|
335086c9d7 | ||
|
|
60806a3954 | ||
|
|
e0e96e7b9b | ||
|
|
cda35eb127 | ||
|
|
bf1ef345a2 | ||
|
|
e54ae7a51f | ||
|
|
a5a2d654ae | ||
|
|
b6340d8657 | ||
|
|
cdb87e2ac2 | ||
|
|
a0baed4e14 | ||
|
|
573a2d4fb1 | ||
|
|
df3ae9e22d | ||
|
|
52bffa024e | ||
|
|
459499d5f5 | ||
|
|
7cc94f6829 | ||
|
|
53d5b08559 | ||
|
|
c517b074f1 | ||
|
|
503ba43ebc | ||
|
|
b0755f4bc5 | ||
|
|
89ece1ec4c | ||
|
|
9fcaefbd99 | ||
|
|
81f33c7bbd | ||
|
|
0ce1bf2360 | ||
|
|
bb10c1aa9d | ||
|
|
b2c1daa6c7 | ||
|
|
da4338ed69 | ||
|
|
1849d712b1 | ||
|
|
28a7e0f717 | ||
|
|
ddbfc79cae | ||
|
|
ec8900d38f | ||
|
|
8aadd5d775 | ||
|
|
268f9306d9 | ||
|
|
866552254c | ||
|
|
42f87424c6 | ||
|
|
4899fb27ec | ||
|
|
c2c8d2bdaa | ||
|
|
daa373967f | ||
|
|
dccc9bfbcd | ||
|
|
f51a873752 | ||
|
|
bad5efc5e8 | ||
|
|
edf8384348 | ||
|
|
65c2f0e191 | ||
|
|
427a09296c | ||
|
|
ba11d62003 | ||
|
|
900dbe781e | ||
|
|
ee19aa8f2e | ||
|
|
e9cff66852 | ||
|
|
f63894bea7 | ||
|
|
35a285d71a | ||
|
|
ba80b01150 | ||
|
|
c9be359db4 | ||
|
|
46a3933dbb | ||
|
|
7b4060f9e1 | ||
|
|
beb3a6e67b | ||
|
|
b393c35284 | ||
|
|
ad371553ba | ||
|
|
0107fb9486 | ||
|
|
7cf70317e8 | ||
|
|
7d8aaac5d2 | ||
|
|
2b5f7d126f | ||
|
|
3cbc71ca1b | ||
|
|
9c4ac38a6c | ||
|
|
9ae64a97ba | ||
|
|
2807b1cad5 | ||
|
|
157e832c95 | ||
|
|
d78fcd3721 | ||
|
|
ac7ab79aef | ||
|
|
00fb353465 | ||
|
|
f0ec5a9496 | ||
|
|
7a31082da1 | ||
|
|
b54f4575ee | ||
|
|
58840e2c00 | ||
|
|
490a1e88eb | ||
|
|
2f8a3c9904 | ||
|
|
e7fdf76120 | ||
|
|
d0d41b743a | ||
|
|
a2995411d6 | ||
|
|
3246c4a621 | ||
|
|
48faf929a4 | ||
|
|
b7f922a999 | ||
|
|
bfcdf9340d | ||
|
|
4ed515f5a3 | ||
|
|
84b2737ffb | ||
|
|
deabca5a94 | ||
|
|
e9a49e23e8 | ||
|
|
cfdef23365 | ||
|
|
7c4b6a9de4 | ||
|
|
bb8866f73f | ||
|
|
33c42648ef | ||
|
|
7feddd2eee | ||
|
|
1344b84cf5 | ||
|
|
ef03b750c5 | ||
|
|
0c07d78a37 | ||
|
|
b5885e446c | ||
|
|
0fa3d4481a | ||
|
|
49e7c2e05b | ||
|
|
2dc7d046ef | ||
|
|
3e0bd41efd | ||
|
|
929eaca2d8 | ||
|
|
60517c5ab6 | ||
|
|
97a8a2b305 | ||
|
|
8fc54bdbe2 | ||
|
|
6ff251b24a | ||
|
|
7237800a91 | ||
|
|
b4cc8e92c7 | ||
|
|
2e233da16d | ||
|
|
b703f78db9 | ||
|
|
13e792cf4d | ||
|
|
552c2d74f3 | ||
|
|
593f23b021 | ||
|
|
1baf83bac3 | ||
|
|
d43ca3190e | ||
|
|
c4c7860876 | ||
|
|
0decdeb37c | ||
|
|
47505dcc31 | ||
|
|
e71d6e792b | ||
|
|
29796cfec8 | ||
|
|
4bdf255c3a | ||
|
|
11efe8b8d1 | ||
|
|
3dd2d09584 | ||
|
|
ed4e8e8f25 | ||
|
|
181a74df88 | ||
|
|
166b2f3a52 | ||
|
|
bd5cdc52f9 | ||
|
|
5d0318c102 | ||
|
|
17adc644fb | ||
|
|
f48159dc0b | ||
|
|
360697c034 | ||
|
|
45dd833b27 | ||
|
|
98491eed01 | ||
|
|
fdafc1c59e | ||
|
|
a32638ed4c | ||
|
|
39080a6046 | ||
|
|
a124d36714 | ||
|
|
7f9e619643 | ||
|
|
db12f2f5c8 | ||
|
|
415e0b70f3 | ||
|
|
0e4b1e5ec7 | ||
|
|
054a97371c | ||
|
|
2b25b6a6ea | ||
|
|
780ed3120e | ||
|
|
bdd3cbd4c7 | ||
|
|
0c8013038f | ||
|
|
c020d59c56 | ||
|
|
a5c336494b | ||
|
|
aaa4655f45 | ||
|
|
ed18ec0bc5 | ||
|
|
1de43f31b6 | ||
|
|
1a04c86edd | ||
|
|
2bbdcae82e | ||
|
|
9b0aa5d601 | ||
|
|
cdc261e30a | ||
|
|
09133a66b0 | ||
|
|
64f1a31533 | ||
|
|
9a92a50a5f | ||
|
|
2afd93e82f | ||
|
|
cb62cc1e9d | ||
|
|
2cd3fc5af9 | ||
|
|
bac710a17f | ||
|
|
fb8b0f78ca | ||
|
|
e18ce15753 | ||
|
|
ed8ce9e3ca | ||
|
|
af5ef04115 | ||
|
|
9530a3df52 | ||
|
|
2794ac653f | ||
|
|
43eb758d73 | ||
|
|
d0364cd101 | ||
|
|
6a008bf312 | ||
|
|
dfb271410c | ||
|
|
789d67209c | ||
|
|
a31f6b75d9 | ||
|
|
f814427a7d | ||
|
|
b0307e92d4 | ||
|
|
7fa3d69aa1 | ||
|
|
58555a6b85 | ||
|
|
52113395db | ||
|
|
8dd1309c21 | ||
|
|
202e428412 | ||
|
|
6e7ed3cea3 | ||
|
|
41cb49141b | ||
|
|
82a8283b6e | ||
|
|
a5d28adc44 | ||
|
|
82e206bccf | ||
|
|
1e4d6646c6 | ||
|
|
acbf9fc32f | ||
|
|
046f227003 | ||
|
|
50ac9e32be | ||
|
|
3459dcaa15 | ||
|
|
9410defab6 | ||
|
|
b5a26e11f8 | ||
|
|
c51481628d | ||
|
|
24fa51a12c | ||
|
|
7328520d05 | ||
|
|
409d206f1e | ||
|
|
b0b393f3d9 | ||
|
|
c4499088c8 | ||
|
|
4cccd6ac5c | ||
|
|
188b28fce3 | ||
|
|
24adda6c7d | ||
|
|
4b49302fbe | ||
|
|
402ab350de | ||
|
|
47b68770af | ||
|
|
60aa16a327 | ||
|
|
5702a4806b | ||
|
|
74c4bdb660 | ||
|
|
203a2cf7fb | ||
|
|
1faa2733b3 | ||
|
|
ffa432a876 | ||
|
|
009fd29265 | ||
|
|
8f05c2324e | ||
|
|
e1ab515883 | ||
|
|
a68a7a60a7 | ||
|
|
b4dc274646 | ||
|
|
b31892bdc6 | ||
|
|
f388a1348d | ||
|
|
52dacbddf9 | ||
|
|
cc52f60aa1 | ||
|
|
1af818b691 | ||
|
|
d76f7758e7 | ||
|
|
48ab2f2400 | ||
|
|
0aa844eebc | ||
|
|
481b02ccf2 | ||
|
|
67e6ef6fda | ||
|
|
7d19f86d7a | ||
|
|
2d27d8a47c | ||
|
|
10fac130ef | ||
|
|
c6b632543d | ||
|
|
32eb8157eb | ||
|
|
3628f22114 | ||
|
|
177fa37041 | ||
|
|
717f6576ea | ||
|
|
f1b2ffa0fa | ||
|
|
ac73c23c73 | ||
|
|
ac40308b1c | ||
|
|
6fb81aa78c | ||
|
|
92430c78c2 | ||
|
|
cb7ddaa295 | ||
|
|
786d079632 | ||
|
|
4af25a505a | ||
|
|
3218803aae | ||
|
|
2311d5bcef | ||
|
|
e56d92334f | ||
|
|
bc24a069da | ||
|
|
837747f8f7 | ||
|
|
a8c32ae49c | ||
|
|
32c5b414de | ||
|
|
12c81a22e8 | ||
|
|
0c5d0d4bb2 | ||
|
|
234f9c624d | ||
|
|
da669b44ff | ||
|
|
3c39f5f085 | ||
|
|
6de91b5872 | ||
|
|
ff9a0979f6 | ||
|
|
e1e8af2489 | ||
|
|
1eb000f615 | ||
|
|
e20fd97e59 | ||
|
|
d10ceacd67 | ||
|
|
208c28ee01 | ||
|
|
cdd1bb3c29 | ||
|
|
3d9c4fa320 | ||
|
|
9c4d18ef3b | ||
|
|
ce9ff3959f | ||
|
|
51aef4e1e5 | ||
|
|
90247059d0 | ||
|
|
c97abb46ed | ||
|
|
b8f5e371c7 | ||
|
|
401311a05f | ||
|
|
652b8e4e15 | ||
|
|
99b7e7c0f1 | ||
|
|
81442bb6f2 | ||
|
|
137f474b69 | ||
|
|
8e14d3f8f7 |
@@ -6,3 +6,5 @@ assets
|
|||||||
docs
|
docs
|
||||||
public
|
public
|
||||||
test
|
test
|
||||||
|
coverage
|
||||||
|
.nyc_output
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
dist
|
dist
|
||||||
assets
|
assets
|
||||||
firefox
|
firefox
|
||||||
|
coverage
|
||||||
|
|||||||
4
.gitignore
vendored
@@ -1,2 +1,6 @@
|
|||||||
node_modules
|
node_modules
|
||||||
|
coverage
|
||||||
dist
|
dist
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
.nyc_output
|
||||||
3
.prettierignore
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
dist
|
||||||
|
assets/*.js
|
||||||
|
coverage
|
||||||
82
CHANGELOG.md
@@ -1,5 +1,87 @@
|
|||||||
## Change Log
|
## Change Log
|
||||||
|
|
||||||
|
### upcoming (2018/02/27 01:52 +00:00)
|
||||||
|
- [#769](https://github.com/mozilla/send/pull/769) removed unsafe-inline styles via svgo-loader (@dannycoates)
|
||||||
|
- [#767](https://github.com/mozilla/send/pull/767) added coverage artifact to circleci (@dannycoates)
|
||||||
|
- [#766](https://github.com/mozilla/send/pull/766) Some frontend unit tests [WIP] (@dannycoates)
|
||||||
|
- [#761](https://github.com/mozilla/send/pull/761) added maxPasswordLength and passwordError messages (@dannycoates)
|
||||||
|
- [#764](https://github.com/mozilla/send/pull/764) added indefinite progress mode (@dannycoates)
|
||||||
|
- [#760](https://github.com/mozilla/send/pull/760) refactored css: phase 1 (@dannycoates)
|
||||||
|
- [#759](https://github.com/mozilla/send/pull/759) Switch en-US FTL file to new syntax (@flodolo)
|
||||||
|
- [#758](https://github.com/mozilla/send/pull/758) refactored server (@dannycoates)
|
||||||
|
- [#757](https://github.com/mozilla/send/pull/757) Update to fluent 0.4.3 (@stasm)
|
||||||
|
|
||||||
|
### v2.3.0 (2018/02/01 23:27 +00:00)
|
||||||
|
- [#536](https://github.com/mozilla/send/pull/536) use redis expire event to delete stored data immediately (@ehuggett)
|
||||||
|
- [#744](https://github.com/mozilla/send/pull/744) Gradient experiment (@dannycoates)
|
||||||
|
- [#739](https://github.com/mozilla/send/pull/739) added /api/info/:id route (@dannycoates)
|
||||||
|
- [#737](https://github.com/mozilla/send/pull/737) big refactor (@dannycoates)
|
||||||
|
- [#722](https://github.com/mozilla/send/pull/722) Add localization note to 'Time' and 'Downloads' string (@flodolo)
|
||||||
|
- [#721](https://github.com/mozilla/send/pull/721) show download Limits on page; Fixes #661 (@shikhar-scs)
|
||||||
|
- [#694](https://github.com/mozilla/send/pull/694) Passwords can now be changed (#687) (@himanish-star)
|
||||||
|
- [#702](https://github.com/mozilla/send/pull/702) Restricted the banner from showing on unsupported browsers (@himanish-star)
|
||||||
|
- [#701](https://github.com/mozilla/send/pull/701) improved popup for mobile display; Fixes #699 (@shikhar-scs)
|
||||||
|
- [#683](https://github.com/mozilla/send/pull/683) API changes to accommodate 3rd party clients (@ehuggett)
|
||||||
|
- [#698](https://github.com/mozilla/send/pull/698) Popup for delete button attached (@himanish-star)
|
||||||
|
- [#695](https://github.com/mozilla/send/pull/695) Show Warning, Cancel and Redirect on size > 2GB ; fixes #578 (@shikhar-scs)
|
||||||
|
- [#684](https://github.com/mozilla/send/pull/684) delete btn popup attached (@himanish-star)
|
||||||
|
- [#686](https://github.com/mozilla/send/pull/686) Hide password while Typing and after Entering: Fixes #670 (@shikhar-scs)
|
||||||
|
- [#679](https://github.com/mozilla/send/pull/679) changed font to sans sherif: Solves #676 (@shikhar-scs)
|
||||||
|
- [#693](https://github.com/mozilla/send/pull/693) README: Fix query link for "good first bugs" (@jspam)
|
||||||
|
- [#685](https://github.com/mozilla/send/pull/685) checkbox now has a hover effect: fixes #635 (@himanish-star)
|
||||||
|
- [#668](https://github.com/mozilla/send/pull/668) Add possibility to bind to a specific IP address (@TwizzyDizzy)
|
||||||
|
- [#682](https://github.com/mozilla/send/pull/682) [Docs] - README.md - minor spelling fixes (@tmm2018)
|
||||||
|
- [#672](https://github.com/mozilla/send/pull/672) Use EXPIRE_SECONDS to calculate file ttl for static content (@derektamsen)
|
||||||
|
- [#680](https://github.com/mozilla/send/pull/680) adjusted line height of label : fixes #609 (@himanish-star)
|
||||||
|
|
||||||
|
### v2.2.2 (2017/12/19 18:06 +00:00)
|
||||||
|
- [#667](https://github.com/mozilla/send/pull/667) Make develop the default NODE_ENV (@claudijd)
|
||||||
|
|
||||||
|
### v2.2.1 (2017/12/08 18:00 +00:00)
|
||||||
|
- [#665](https://github.com/mozilla/send/pull/665) stop drag target from flickering when dragging over children (@ericawright)
|
||||||
|
|
||||||
|
### v2.2.0 (2017/12/06 23:57 +00:00)
|
||||||
|
- [#654](https://github.com/mozilla/send/pull/654) Multiple download UI (@dannycoates)
|
||||||
|
- [#650](https://github.com/mozilla/send/pull/650) #634: overwrite appearance of password submit input (@ovlb)
|
||||||
|
- [#649](https://github.com/mozilla/send/pull/649) #609 share interface: align text in input and button (@ovlb)
|
||||||
|
|
||||||
|
### v2.1.2 (2017/11/16 19:03 +00:00)
|
||||||
|
- [#645](https://github.com/mozilla/send/pull/645) Remove the leak of the password into the console (@laurentj)
|
||||||
|
|
||||||
|
### v2.1.0 (2017/11/15 03:07 +00:00)
|
||||||
|
- [#641](https://github.com/mozilla/send/pull/641) Added experiment for firefox download promo (@dannycoates)
|
||||||
|
- [#640](https://github.com/mozilla/send/pull/640) use fluent-langneg for subtag support (@dannycoates)
|
||||||
|
- [#639](https://github.com/mozilla/send/pull/639) wrap number localization in try/catch (@dannycoates)
|
||||||
|
|
||||||
|
### v2.0.0 (2017/11/08 05:31 +00:00)
|
||||||
|
- [#633](https://github.com/mozilla/send/pull/633) Keyboard navigation/visual feedback regression (@ehuggett)
|
||||||
|
- [#632](https://github.com/mozilla/send/pull/632) display the 'add password' button only when the input field isn't empty (@dannycoates)
|
||||||
|
- [#626](https://github.com/mozilla/send/pull/626) Partial fix for #623 (@ehuggett)
|
||||||
|
- [#624](https://github.com/mozilla/send/pull/624) set a default MIME type in file metadata (@ehuggett)
|
||||||
|
- [#612](https://github.com/mozilla/send/pull/612) Password UI nits (@dannycoates, @ericawright)
|
||||||
|
- [#617](https://github.com/mozilla/send/pull/617) allow drag and drop if navigating from shared page (@ericawright)
|
||||||
|
- [#608](https://github.com/mozilla/send/pull/608) disable copying link when password not completed (@ericawright)
|
||||||
|
- [#605](https://github.com/mozilla/send/pull/605) align the "Password" and "Copy to clipboard" fields. (@ericawright)
|
||||||
|
- [#582](https://github.com/mozilla/send/pull/582) Add optional password to the download url (@dannycoates)
|
||||||
|
|
||||||
|
### v1.2.4 (2017/10/10 17:34 +00:00)
|
||||||
|
- [#583](https://github.com/mozilla/send/pull/583) Promote the beefy UI to default (@dannycoates)
|
||||||
|
- [#581](https://github.com/mozilla/send/pull/581) introducing ToC to README.md (@tmm2018)
|
||||||
|
- [#579](https://github.com/mozilla/send/pull/579) Hide cancel button when upload reaches 100% (@ericawright)
|
||||||
|
- [#580](https://github.com/mozilla/send/pull/580) Change Favicon in to look better in a variety of cases (@ericawright)
|
||||||
|
- [#571](https://github.com/mozilla/send/pull/571) Centre logo (@ehuggett)
|
||||||
|
- [#574](https://github.com/mozilla/send/pull/574) Make upload button focusable (accessibility/tab navigation) (@ehuggett)
|
||||||
|
|
||||||
|
### v1.2.0 (2017/09/12 22:42 +00:00)
|
||||||
|
- [#559](https://github.com/mozilla/send/pull/559) added first A/B experiment (@dannycoates)
|
||||||
|
- [#542](https://github.com/mozilla/send/pull/542) fix docker link typo (@ehuggett)
|
||||||
|
- [#541](https://github.com/mozilla/send/pull/541) removed .title and .alt attributes from ftl (@dannycoates)
|
||||||
|
- [#537](https://github.com/mozilla/send/pull/537) a few changes to make A/B testing easier (@dannycoates)
|
||||||
|
- [#533](https://github.com/mozilla/send/pull/533) minor UI fixes (@youwenliang)
|
||||||
|
- [#531](https://github.com/mozilla/send/pull/531) Add CHANGELOG script (@pdehaan)
|
||||||
|
- [#535](https://github.com/mozilla/send/pull/535) Fixed minimum NodeJS version in README (@LuFlo)
|
||||||
|
- [#528](https://github.com/mozilla/send/pull/528) adding separators to README (@tmm2018)
|
||||||
|
|
||||||
### v1.1.1 (2017/08/17 01:29 +00:00)
|
### v1.1.1 (2017/08/17 01:29 +00:00)
|
||||||
- [#516](https://github.com/mozilla/send/pull/516) cache assets (@dannycoates)
|
- [#516](https://github.com/mozilla/send/pull/516) cache assets (@dannycoates)
|
||||||
- [#520](https://github.com/mozilla/send/pull/520) fix drag & drop (@dannycoates)
|
- [#520](https://github.com/mozilla/send/pull/520) fix drag & drop (@dannycoates)
|
||||||
|
|||||||
58
CONTRIBUTORS
@@ -1,74 +1,122 @@
|
|||||||
|
Abdalrahman Hwoij
|
||||||
Abhinav Adduri
|
Abhinav Adduri
|
||||||
|
Adnan Kičin
|
||||||
|
Alberto Castro
|
||||||
Alexander Slovesnik
|
Alexander Slovesnik
|
||||||
Amin Mahmudian
|
Amin Mahmudian
|
||||||
Andreas Pettersson
|
Andreas Pettersson
|
||||||
Arash Mousavi
|
Arash Mousavi
|
||||||
|
Artem Polivanchuk
|
||||||
|
Ashikur Rahman
|
||||||
Balázs Meskó
|
Balázs Meskó
|
||||||
Belayet Hossain
|
Belayet Hossain
|
||||||
|
Besnik Bleta
|
||||||
Bjørn I
|
Bjørn I
|
||||||
Boopesh Mahendran
|
Boopesh Mahendran
|
||||||
|
Breana Gonzales
|
||||||
Chuck Harmston
|
Chuck Harmston
|
||||||
Cláudio Esperança
|
Cláudio Esperança
|
||||||
|
Cristian Silaghi
|
||||||
Cynthia Pereira
|
Cynthia Pereira
|
||||||
Daniel Thorn
|
Daniel Thorn
|
||||||
Daniela Arcese
|
Daniela Arcese
|
||||||
Danny Coates
|
Danny Coates
|
||||||
|
Derek Tamsen
|
||||||
|
Edmund Huggett
|
||||||
|
Elisa X
|
||||||
Emin Mastizada
|
Emin Mastizada
|
||||||
Enol
|
Enol
|
||||||
Erica
|
Erica
|
||||||
Erica Wright
|
Erica Wright
|
||||||
|
Filip Hruška
|
||||||
Fjoerfoks
|
Fjoerfoks
|
||||||
Francesco Lodolo
|
Francesco Lodolo
|
||||||
Francesco Lodolo [:flod]
|
Francesco Lodolo [:flod]
|
||||||
|
Frederick Villaluna
|
||||||
Gautam krishna.R
|
Gautam krishna.R
|
||||||
|
Georgianizator
|
||||||
|
Hyeonseok Shin
|
||||||
Håvar Henriksen
|
Håvar Henriksen
|
||||||
Jae Hyeon Park
|
Jae Hyeon Park
|
||||||
Jakub Rychlý
|
Jakub Rychlý
|
||||||
Jamie
|
Jamie
|
||||||
Jim Spentzos
|
Jim Spentzos
|
||||||
|
Jobava
|
||||||
Johann-S
|
Johann-S
|
||||||
John Gruen
|
John Gruen
|
||||||
Jon Vadillo
|
Jon Vadillo
|
||||||
|
Jonathan Claudius
|
||||||
|
Jordi Cuevas
|
||||||
Jordi Serratosa
|
Jordi Serratosa
|
||||||
|
Juan Esteban Ajsivinac Sián
|
||||||
Juraj Cigáň
|
Juraj Cigáň
|
||||||
|
Kerim Kalamujić
|
||||||
|
Khaled Hosny
|
||||||
Kohei Yoshino
|
Kohei Yoshino
|
||||||
Lan Glad
|
Lan Glad
|
||||||
|
Laurent Jouanneau
|
||||||
|
Lobodzets
|
||||||
|
LuFlo
|
||||||
|
Luiz Carlos de Morais
|
||||||
Luna Jernberg
|
Luna Jernberg
|
||||||
Marcelo Poli
|
Marcelo Poli
|
||||||
Marco Aurélio
|
Marco Aurélio
|
||||||
|
Mark Heijl
|
||||||
Mark Liang
|
Mark Liang
|
||||||
|
Marko Andrejić
|
||||||
Matjaž Horvat
|
Matjaž Horvat
|
||||||
Maykon Chagas
|
Maykon Chagas
|
||||||
|
Melo46
|
||||||
|
Merike Sell
|
||||||
Michael Köhler
|
Michael Köhler
|
||||||
Michael Wolf
|
Michael Wolf
|
||||||
Michal Stanke
|
Michal Stanke
|
||||||
Michal Vašíček
|
Michal Vašíček
|
||||||
|
Mozilla Pontoon
|
||||||
Moḥend Belqasem
|
Moḥend Belqasem
|
||||||
|
Muḥend Belqasem
|
||||||
Nicholas Skinsacos
|
Nicholas Skinsacos
|
||||||
|
Nihad
|
||||||
|
Nihad Suljić
|
||||||
|
Oscar
|
||||||
Peter deHaan
|
Peter deHaan
|
||||||
Pierre Neter
|
Pierre Neter
|
||||||
Pin-guang Chen
|
Pin-guang Chen
|
||||||
|
Radu Popescu
|
||||||
Rhoslyn Prys
|
Rhoslyn Prys
|
||||||
|
RickieES
|
||||||
Rizky Ariestiyansyah
|
Rizky Ariestiyansyah
|
||||||
Roberto Alvarado
|
Roberto Alvarado
|
||||||
Rodrigo
|
Rodrigo
|
||||||
|
Rodrigo Guerra
|
||||||
Rok Žerdin
|
Rok Žerdin
|
||||||
Sahithi
|
Sahithi
|
||||||
Sairam Raavi
|
Sairam Raavi
|
||||||
|
Sander Lepik
|
||||||
Sandro
|
Sandro
|
||||||
|
Sara Todaro
|
||||||
|
Sav22999
|
||||||
Schieck :)
|
Schieck :)
|
||||||
Selim Şumlu
|
Selim Şumlu
|
||||||
Slimane Amiri
|
Slimane Amiri
|
||||||
|
Soumya Himanish Mohapatra
|
||||||
|
Staś Małolepszy
|
||||||
|
Tema
|
||||||
|
Thomas Dalichow
|
||||||
Théo Chevalier
|
Théo Chevalier
|
||||||
|
Tiago Morais Morgado
|
||||||
Tomáš Zelina
|
Tomáš Zelina
|
||||||
Ton
|
Ton
|
||||||
Tymur Faradzhev
|
Tymur Faradzhev
|
||||||
|
Uccen Marzuq
|
||||||
Varghese Thomas
|
Varghese Thomas
|
||||||
Victor Bychek
|
Victor Bychek
|
||||||
Weihang Lo
|
Weihang Lo
|
||||||
Wil Clouser
|
Wil Clouser
|
||||||
YFdyh000
|
YFdyh000
|
||||||
You-Wen Liang (Mark)
|
You-Wen Liang (Mark)
|
||||||
|
aefgh39622
|
||||||
|
albertdcastro
|
||||||
alex_mayorga
|
alex_mayorga
|
||||||
ariestiyansyah
|
ariestiyansyah
|
||||||
avelper
|
avelper
|
||||||
@@ -77,16 +125,26 @@ ehuggett
|
|||||||
eljuno
|
eljuno
|
||||||
erdem cobanoglu
|
erdem cobanoglu
|
||||||
gautamkrishnar
|
gautamkrishnar
|
||||||
|
gmontagu
|
||||||
goofy
|
goofy
|
||||||
|
hello
|
||||||
hi
|
hi
|
||||||
jesferman1993
|
jesferman1993
|
||||||
|
jlG
|
||||||
josotrix
|
josotrix
|
||||||
|
jspam
|
||||||
kenrick95
|
kenrick95
|
||||||
manxmensch
|
manxmensch
|
||||||
|
mirzet.omerovic.1992
|
||||||
ravmn
|
ravmn
|
||||||
reza.habibi2008
|
reza.habibi2008
|
||||||
|
savemore99.sm
|
||||||
|
shikhar-scs
|
||||||
siparon
|
siparon
|
||||||
skystar-p
|
skystar-p
|
||||||
|
tiagomoraismorgado
|
||||||
xcffl
|
xcffl
|
||||||
|
ybouhamam
|
||||||
Μιχάλης
|
Μιχάλης
|
||||||
Марко Костић (Marko Kostić)
|
Марко Костић (Marko Kostić)
|
||||||
|
صفا الفليج
|
||||||
|
|||||||
18
README.md
@@ -7,6 +7,20 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
* [What it does](#what-it-does)
|
||||||
|
* [Requirements](#requirements)
|
||||||
|
* [Development](#development)
|
||||||
|
* [Commands](#commands)
|
||||||
|
* [Configuration](#configuration)
|
||||||
|
* [Localization](#localization)
|
||||||
|
* [Contributing](#contributing)
|
||||||
|
* [Testing](#testing)
|
||||||
|
* [License](#license)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## What it does
|
## What it does
|
||||||
|
|
||||||
A file sharing experiment which allows you to send encrypted files to other users.
|
A file sharing experiment which allows you to send encrypted files to other users.
|
||||||
@@ -55,13 +69,13 @@ The server is configured with environment variables. See [server/config.js](serv
|
|||||||
|
|
||||||
## Localization
|
## Localization
|
||||||
|
|
||||||
Firefox Send localization is managed via [Pontoon](https://pontoon.mozilla.org/projects/test-pilot-firefox-send/), not direct pull requests to the repository. If you want to fix a typo, add a new language, or simply know more about localization, please get in touch with the [existing localization team](https://pontoon.mozilla.org/teams/) for your language, or Mozilla’s [l10n-drivers](https://wiki.mozilla.org/L10n:Mozilla_Team#Mozilla_Corporation) for guidance.
|
Firefox Send localization is managed via [Pontoon](https://pontoon.mozilla.org/projects/test-pilot-firefox-send/), not direct pull requests to the repository. If you want to fix a typo, add a new language, or simply know more about localization, please get in touch with the [existing localization team](https://pontoon.mozilla.org/teams/) for your language or Mozilla’s [l10n-drivers](https://wiki.mozilla.org/L10n:Mozilla_Team#Mozilla_Corporation) for guidance.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Pull requests are always welcome! Feel free to check out the list of ["good first bugs"](https://github.com/mozilla/send/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+bug%22).
|
Pull requests are always welcome! Feel free to check out the list of ["good first issues"](https://github.com/mozilla/send/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
199
app/api.js
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
import { arrayToB64, b64ToArray } from './utils';
|
||||||
|
|
||||||
|
function post(obj) {
|
||||||
|
return {
|
||||||
|
method: 'POST',
|
||||||
|
headers: new Headers({
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}),
|
||||||
|
body: JSON.stringify(obj)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseNonce(header) {
|
||||||
|
header = header || '';
|
||||||
|
return header.split(' ')[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchWithAuth(url, params, keychain) {
|
||||||
|
const result = {};
|
||||||
|
params = params || {};
|
||||||
|
const h = await keychain.authHeader();
|
||||||
|
params.headers = new Headers({ Authorization: h });
|
||||||
|
const response = await fetch(url, params);
|
||||||
|
result.response = response;
|
||||||
|
result.ok = response.ok;
|
||||||
|
const nonce = parseNonce(response.headers.get('WWW-Authenticate'));
|
||||||
|
result.shouldRetry = response.status === 401 && nonce !== keychain.nonce;
|
||||||
|
keychain.nonce = nonce;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchWithAuthAndRetry(url, params, keychain) {
|
||||||
|
const result = await fetchWithAuth(url, params, keychain);
|
||||||
|
if (result.shouldRetry) {
|
||||||
|
return fetchWithAuth(url, params, keychain);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function del(id, owner_token) {
|
||||||
|
const response = await fetch(`/api/delete/${id}`, post({ owner_token }));
|
||||||
|
return response.ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setParams(id, owner_token, params) {
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/params/${id}`,
|
||||||
|
post({
|
||||||
|
owner_token,
|
||||||
|
dlimit: params.dlimit
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return response.ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fileInfo(id, owner_token) {
|
||||||
|
const response = await fetch(`/api/info/${id}`, post({ owner_token }));
|
||||||
|
if (response.ok) {
|
||||||
|
const obj = await response.json();
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
throw new Error(response.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function metadata(id, keychain) {
|
||||||
|
const result = await fetchWithAuthAndRetry(
|
||||||
|
`/api/metadata/${id}`,
|
||||||
|
{ method: 'GET' },
|
||||||
|
keychain
|
||||||
|
);
|
||||||
|
if (result.ok) {
|
||||||
|
const data = await result.response.json();
|
||||||
|
const meta = await keychain.decryptMetadata(b64ToArray(data.metadata));
|
||||||
|
return {
|
||||||
|
size: data.size,
|
||||||
|
ttl: data.ttl,
|
||||||
|
iv: meta.iv,
|
||||||
|
name: meta.name,
|
||||||
|
type: meta.type
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error(result.response.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setPassword(id, owner_token, keychain) {
|
||||||
|
const auth = await keychain.authKeyB64();
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/password/${id}`,
|
||||||
|
post({ owner_token, auth })
|
||||||
|
);
|
||||||
|
return response.ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uploadFile(
|
||||||
|
encrypted,
|
||||||
|
metadata,
|
||||||
|
verifierB64,
|
||||||
|
keychain,
|
||||||
|
onprogress
|
||||||
|
) {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
const upload = {
|
||||||
|
cancel: function() {
|
||||||
|
xhr.abort();
|
||||||
|
},
|
||||||
|
result: new Promise(function(resolve, reject) {
|
||||||
|
xhr.addEventListener('loadend', function() {
|
||||||
|
const authHeader = xhr.getResponseHeader('WWW-Authenticate');
|
||||||
|
if (authHeader) {
|
||||||
|
keychain.nonce = parseNonce(authHeader);
|
||||||
|
}
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
const responseObj = JSON.parse(xhr.responseText);
|
||||||
|
return resolve({
|
||||||
|
url: responseObj.url,
|
||||||
|
id: responseObj.id,
|
||||||
|
ownerToken: responseObj.owner
|
||||||
|
});
|
||||||
|
}
|
||||||
|
reject(new Error(xhr.status));
|
||||||
|
});
|
||||||
|
})
|
||||||
|
};
|
||||||
|
const dataView = new DataView(encrypted);
|
||||||
|
const blob = new Blob([dataView], { type: 'application/octet-stream' });
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('data', blob);
|
||||||
|
xhr.upload.addEventListener('progress', function(event) {
|
||||||
|
if (event.lengthComputable) {
|
||||||
|
onprogress([event.loaded, event.total]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
xhr.open('post', '/api/upload', true);
|
||||||
|
xhr.setRequestHeader('X-File-Metadata', arrayToB64(new Uint8Array(metadata)));
|
||||||
|
xhr.setRequestHeader('Authorization', `send-v1 ${verifierB64}`);
|
||||||
|
xhr.send(fd);
|
||||||
|
return upload;
|
||||||
|
}
|
||||||
|
|
||||||
|
function download(id, keychain, onprogress, canceller) {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
canceller.oncancel = function() {
|
||||||
|
xhr.abort();
|
||||||
|
};
|
||||||
|
return new Promise(async function(resolve, reject) {
|
||||||
|
xhr.addEventListener('loadend', function() {
|
||||||
|
canceller.oncancel = function() {};
|
||||||
|
const authHeader = xhr.getResponseHeader('WWW-Authenticate');
|
||||||
|
if (authHeader) {
|
||||||
|
keychain.nonce = parseNonce(authHeader);
|
||||||
|
}
|
||||||
|
if (xhr.status !== 200) {
|
||||||
|
return reject(new Error(xhr.status));
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([xhr.response]);
|
||||||
|
const fileReader = new FileReader();
|
||||||
|
fileReader.readAsArrayBuffer(blob);
|
||||||
|
fileReader.onload = function() {
|
||||||
|
resolve(this.result);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
xhr.addEventListener('progress', function(event) {
|
||||||
|
if (event.lengthComputable && event.target.status === 200) {
|
||||||
|
onprogress([event.loaded, event.total]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const auth = await keychain.authHeader();
|
||||||
|
xhr.open('get', `/api/download/${id}`);
|
||||||
|
xhr.setRequestHeader('Authorization', auth);
|
||||||
|
xhr.responseType = 'blob';
|
||||||
|
xhr.send();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryDownload(id, keychain, onprogress, canceller, tries = 1) {
|
||||||
|
try {
|
||||||
|
const result = await download(id, keychain, onprogress, canceller);
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
if (e.message === '401' && --tries > 0) {
|
||||||
|
return tryDownload(id, keychain, onprogress, canceller, tries);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function downloadFile(id, keychain, onprogress) {
|
||||||
|
const canceller = {
|
||||||
|
oncancel: function() {} // download() sets this
|
||||||
|
};
|
||||||
|
function cancel() {
|
||||||
|
canceller.oncancel();
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
cancel,
|
||||||
|
result: tryDownload(id, keychain, onprogress, canceller, 2)
|
||||||
|
};
|
||||||
|
}
|
||||||
267
app/base.css
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
:root {
|
||||||
|
--pageBGColor: #fff;
|
||||||
|
--primaryControlBGColor: #0297f8;
|
||||||
|
--primaryControlFGColor: #fff;
|
||||||
|
--primaryControlHoverColor: #0287e8;
|
||||||
|
--inputTextColor: #737373;
|
||||||
|
--errorColor: #d70022;
|
||||||
|
--linkColor: #0094fb;
|
||||||
|
--textColor: #0c0c0d;
|
||||||
|
--lightTextColor: #737373;
|
||||||
|
--successControlBGColor: #05a700;
|
||||||
|
--successControlFGColor: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
background: url('../assets/send_bg.svg');
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'segoe ui',
|
||||||
|
'helvetica neue', helvetica, ubuntu, roboto, noto, arial, sans-serif;
|
||||||
|
font-weight: 200;
|
||||||
|
background-size: 110%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center top;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'segoe ui',
|
||||||
|
'helvetica neue', helvetica, ubuntu, roboto, noto, arial, sans-serif;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea,
|
||||||
|
button {
|
||||||
|
font-family: inherit;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
flex: auto;
|
||||||
|
max-width: 650px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 96%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noscript {
|
||||||
|
text-align: center;
|
||||||
|
border: 3px solid var(--errorColor);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--primaryControlFGColor);
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
background: var(--primaryControlBGColor);
|
||||||
|
border: 1px solid var(--primaryControlBGColor);
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background-color: var(--primaryControlHoverColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--cancel {
|
||||||
|
color: var(--errorColor);
|
||||||
|
background: var(--pageBGColor);
|
||||||
|
font-size: 15px;
|
||||||
|
border: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--cancel:disabled {
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--cancel:hover {
|
||||||
|
background-color: var(--pageBGColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
flex: 2 0 auto;
|
||||||
|
border: 1px solid var(--primaryControlBGColor);
|
||||||
|
border-radius: 6px 0 0 6px;
|
||||||
|
font-size: 20px;
|
||||||
|
color: var(--inputTextColor);
|
||||||
|
font-family: 'SF Pro Text', sans-serif;
|
||||||
|
letter-spacing: 0;
|
||||||
|
line-height: 23px;
|
||||||
|
font-weight: 300;
|
||||||
|
height: 46px;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input--error {
|
||||||
|
border-color: var(--errorColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input--noBtn {
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputBtn {
|
||||||
|
flex: auto;
|
||||||
|
background: var(--primaryControlBGColor);
|
||||||
|
border-radius: 0 6px 6px 0;
|
||||||
|
border: 1px solid var(--primaryControlBGColor);
|
||||||
|
color: var(--primaryControlFGColor);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
/* Force flat button look */
|
||||||
|
appearance: none;
|
||||||
|
font-size: 15px;
|
||||||
|
padding-bottom: 3px;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputBtn:disabled {
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputBtn:hover {
|
||||||
|
background-color: var(--primaryControlHoverColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputBtn--hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cursor--pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
color: var(--linkColor);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link:focus,
|
||||||
|
.link:active,
|
||||||
|
.link:hover {
|
||||||
|
color: var(--primaryControlHoverColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.link--action {
|
||||||
|
text-decoration: underline;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page {
|
||||||
|
margin: 0 auto 30px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressSection {
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressSection__text {
|
||||||
|
color: var(--lightTextColor);
|
||||||
|
letter-spacing: -0.4px;
|
||||||
|
margin-top: 24px;
|
||||||
|
margin-bottom: 74px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.effect--fadeOut {
|
||||||
|
opacity: 0;
|
||||||
|
animation: fadeout 200ms linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeout {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.effect--fadeIn {
|
||||||
|
opacity: 1;
|
||||||
|
animation: fadein 200ms linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadein {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: var(--errorColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 33px;
|
||||||
|
line-height: 40px;
|
||||||
|
margin: 20px auto;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 520px;
|
||||||
|
font-family: 'SF Pro Text', sans-serif;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 23px;
|
||||||
|
max-width: 630px;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 auto 60px;
|
||||||
|
color: var(--textColor);
|
||||||
|
width: 92%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-device-width: 768px), (max-width: 768px) {
|
||||||
|
.description {
|
||||||
|
margin: 0 auto 25px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-device-width: 520px), (max-width: 520px) {
|
||||||
|
.input {
|
||||||
|
font-size: 22px;
|
||||||
|
padding: 10px 10px;
|
||||||
|
border-radius: 6px 6px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputBtn {
|
||||||
|
border-radius: 0 0 6px 6px;
|
||||||
|
flex: 0 1 65px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input--noBtn {
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/* global MAXFILESIZE */
|
||||||
|
const { bytes } = require('./utils');
|
||||||
|
|
||||||
export default function(state, emitter) {
|
export default function(state, emitter) {
|
||||||
emitter.on('DOMContentLoaded', () => {
|
emitter.on('DOMContentLoaded', () => {
|
||||||
document.body.addEventListener('dragover', event => {
|
document.body.addEventListener('dragover', event => {
|
||||||
@@ -6,17 +9,28 @@ export default function(state, emitter) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
document.body.addEventListener('drop', event => {
|
document.body.addEventListener('drop', event => {
|
||||||
if (state.route === '/' && !state.transfer) {
|
if (state.route === '/' && !state.uploading) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
document.querySelector('.upload-window').classList.remove('ondrag');
|
document
|
||||||
|
.querySelector('.uploadArea')
|
||||||
|
.classList.remove('uploadArea--dragging');
|
||||||
const target = event.dataTransfer;
|
const target = event.dataTransfer;
|
||||||
if (target.files.length === 0) {
|
if (target.files.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (target.files.length > 1 || target.files[0].size === 0) {
|
if (target.files.length > 1) {
|
||||||
return alert(state.translate('uploadPageMultipleFilesAlert'));
|
return alert(state.translate('uploadPageMultipleFilesAlert'));
|
||||||
}
|
}
|
||||||
const file = target.files[0];
|
const file = target.files[0];
|
||||||
|
if (file.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (file.size > MAXFILESIZE) {
|
||||||
|
window.alert(
|
||||||
|
state.translate('fileTooBig', { size: bytes(MAXFILESIZE) })
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
emitter.emit('upload', { file, type: 'drop' });
|
emitter.emit('upload', { file, type: 'drop' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,28 +1,33 @@
|
|||||||
import hash from 'string-hash';
|
import hash from 'string-hash';
|
||||||
|
|
||||||
const experiments = {
|
const experiments = {
|
||||||
'5YHCzn2CQTmBwWwTmZupBA': {
|
S9wqVl2SQ4ab2yZtqDI3Dw: {
|
||||||
id: '5YHCzn2CQTmBwWwTmZupBA',
|
id: 'S9wqVl2SQ4ab2yZtqDI3Dw',
|
||||||
run: function(variant, state, emitter) {
|
run: function(variant, state, emitter) {
|
||||||
state.experiment = {
|
switch (variant) {
|
||||||
xid: this.id,
|
case 1:
|
||||||
xvar: variant
|
state.promo = 'blue';
|
||||||
};
|
break;
|
||||||
// Beefy UI
|
case 2:
|
||||||
if (variant === 1) {
|
state.promo = 'pink';
|
||||||
state.config.uploadWindowStyle = 'upload-window upload-window-b';
|
break;
|
||||||
state.config.uploadButtonStyle = 'btn browse browse-b';
|
default:
|
||||||
} else {
|
state.promo = 'grey';
|
||||||
state.config.uploadWindowStyle = 'upload-window';
|
|
||||||
state.config.uploadButtonStyle = 'btn browse';
|
|
||||||
}
|
}
|
||||||
emitter.emit('render');
|
emitter.emit('render');
|
||||||
},
|
},
|
||||||
eligible: function(state) {
|
eligible: function() {
|
||||||
return this.luckyNumber(state) >= 0.5;
|
return (
|
||||||
|
!/firefox|fxios/i.test(navigator.userAgent) &&
|
||||||
|
document.querySelector('html').lang === 'en-US'
|
||||||
|
);
|
||||||
},
|
},
|
||||||
variant: function(state) {
|
variant: function(state) {
|
||||||
return this.luckyNumber(state) < 0.5 ? 0 : 1;
|
const n = this.luckyNumber(state);
|
||||||
|
if (n < 0.33) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return n < 0.66 ? 1 : 2;
|
||||||
},
|
},
|
||||||
luckyNumber: function(state) {
|
luckyNumber: function(state) {
|
||||||
return luckyNumber(
|
return luckyNumber(
|
||||||
@@ -33,6 +38,7 @@ const experiments = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
//Returns a number between 0 and 1
|
//Returns a number between 0 and 1
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
function luckyNumber(str) {
|
function luckyNumber(str) {
|
||||||
return hash(str) / 0xffffffff;
|
return hash(str) / 0xffffffff;
|
||||||
}
|
}
|
||||||
@@ -51,7 +57,7 @@ export default function initialize(state, emitter) {
|
|||||||
emitter.on('DOMContentLoaded', () => {
|
emitter.on('DOMContentLoaded', () => {
|
||||||
const xp = experiments[state.query.x];
|
const xp = experiments[state.query.x];
|
||||||
if (xp) {
|
if (xp) {
|
||||||
xp.run(state.query.v, state, emitter);
|
xp.run(+state.query.v, state, emitter);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -61,12 +67,12 @@ export default function initialize(state, emitter) {
|
|||||||
checkExperiments(state, emitter);
|
checkExperiments(state, emitter);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const enrolled = state.storage.enrolled;
|
const enrolled = state.storage.enrolled.filter(([id, variant]) => {
|
||||||
enrolled.forEach(([id, variant]) => {
|
|
||||||
const xp = experiments[id];
|
const xp = experiments[id];
|
||||||
if (xp) {
|
if (xp) {
|
||||||
xp.run(variant, state, emitter);
|
xp.run(variant, state, emitter);
|
||||||
}
|
}
|
||||||
|
return !!xp;
|
||||||
});
|
});
|
||||||
// single experiment per session for now
|
// single experiment per session for now
|
||||||
if (enrolled.length === 0) {
|
if (enrolled.length === 0) {
|
||||||
|
|||||||
@@ -1,72 +1,34 @@
|
|||||||
/* global EXPIRE_SECONDS */
|
|
||||||
import FileSender from './fileSender';
|
import FileSender from './fileSender';
|
||||||
import FileReceiver from './fileReceiver';
|
import FileReceiver from './fileReceiver';
|
||||||
import { copyToClipboard, delay, fadeOut } from './utils';
|
import {
|
||||||
|
copyToClipboard,
|
||||||
|
delay,
|
||||||
|
fadeOut,
|
||||||
|
openLinksInNewTab,
|
||||||
|
percent
|
||||||
|
} from './utils';
|
||||||
import * as metrics from './metrics';
|
import * as metrics from './metrics';
|
||||||
|
|
||||||
function saveFile(file) {
|
|
||||||
const dataView = new DataView(file.plaintext);
|
|
||||||
const blob = new Blob([dataView], { type: file.type });
|
|
||||||
const downloadUrl = URL.createObjectURL(blob);
|
|
||||||
|
|
||||||
if (window.navigator.msSaveBlob) {
|
|
||||||
return window.navigator.msSaveBlob(blob, file.name);
|
|
||||||
}
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = downloadUrl;
|
|
||||||
a.download = file.name;
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
URL.revokeObjectURL(downloadUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
function openLinksInNewTab(links, should = true) {
|
|
||||||
links = links || Array.from(document.querySelectorAll('a:not([target])'));
|
|
||||||
if (should) {
|
|
||||||
links.forEach(l => {
|
|
||||||
l.setAttribute('target', '_blank');
|
|
||||||
l.setAttribute('rel', 'noopener noreferrer');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
links.forEach(l => {
|
|
||||||
l.removeAttribute('target');
|
|
||||||
l.removeAttribute('rel');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return links;
|
|
||||||
}
|
|
||||||
|
|
||||||
function exists(id) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const xhr = new XMLHttpRequest();
|
|
||||||
xhr.onreadystatechange = () => {
|
|
||||||
if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
|
|
||||||
resolve(xhr.status === 200);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
xhr.onerror = () => resolve(false);
|
|
||||||
xhr.ontimeout = () => resolve(false);
|
|
||||||
xhr.open('get', '/api/exists/' + id);
|
|
||||||
xhr.timeout = 2000;
|
|
||||||
xhr.send();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function(state, emitter) {
|
export default function(state, emitter) {
|
||||||
let lastRender = 0;
|
let lastRender = 0;
|
||||||
|
let updateTitle = false;
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
emitter.emit('render');
|
emitter.emit('render');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkFiles() {
|
async function checkFiles() {
|
||||||
const files = state.storage.files;
|
const files = state.storage.files.slice();
|
||||||
let rerender = false;
|
let rerender = false;
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const ok = await exists(file.id);
|
const oldLimit = file.dlimit;
|
||||||
if (!ok) {
|
const oldTotal = file.dtotal;
|
||||||
|
await file.updateDownloadCount();
|
||||||
|
if (file.dtotal === file.dlimit) {
|
||||||
state.storage.remove(file.id);
|
state.storage.remove(file.id);
|
||||||
rerender = true;
|
rerender = true;
|
||||||
|
} else if (oldLimit !== file.dlimit || oldTotal !== file.dtotal) {
|
||||||
|
rerender = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (rerender) {
|
if (rerender) {
|
||||||
@@ -74,7 +36,21 @@ export default function(state, emitter) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
emitter.on('DOMContentLoaded', checkFiles);
|
function updateProgress() {
|
||||||
|
if (updateTitle) {
|
||||||
|
emitter.emit('DOMTitleChange', percent(state.transfer.progressRatio));
|
||||||
|
}
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
emitter.on('DOMContentLoaded', () => {
|
||||||
|
document.addEventListener('blur', () => (updateTitle = true));
|
||||||
|
document.addEventListener('focus', () => {
|
||||||
|
updateTitle = false;
|
||||||
|
emitter.emit('DOMTitleChange', 'Firefox Send');
|
||||||
|
});
|
||||||
|
checkFiles();
|
||||||
|
});
|
||||||
|
|
||||||
emitter.on('navigate', checkFiles);
|
emitter.on('navigate', checkFiles);
|
||||||
|
|
||||||
@@ -82,6 +58,12 @@ export default function(state, emitter) {
|
|||||||
lastRender = Date.now();
|
lastRender = Date.now();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
emitter.on('changeLimit', async ({ file, value }) => {
|
||||||
|
await file.changeLimit(value);
|
||||||
|
state.storage.writeFile(file);
|
||||||
|
metrics.changedDownloadLimit(file);
|
||||||
|
});
|
||||||
|
|
||||||
emitter.on('delete', async ({ file, location }) => {
|
emitter.on('delete', async ({ file, location }) => {
|
||||||
try {
|
try {
|
||||||
metrics.deletedUpload({
|
metrics.deletedUpload({
|
||||||
@@ -93,11 +75,10 @@ export default function(state, emitter) {
|
|||||||
location
|
location
|
||||||
});
|
});
|
||||||
state.storage.remove(file.id);
|
state.storage.remove(file.id);
|
||||||
await FileSender.delete(file.id, file.deleteToken);
|
await file.del();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
state.raven.captureException(e);
|
state.raven.captureException(e);
|
||||||
}
|
}
|
||||||
state.fileInfo = null;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on('cancel', () => {
|
emitter.on('cancel', () => {
|
||||||
@@ -107,37 +88,31 @@ export default function(state, emitter) {
|
|||||||
emitter.on('upload', async ({ file, type }) => {
|
emitter.on('upload', async ({ file, type }) => {
|
||||||
const size = file.size;
|
const size = file.size;
|
||||||
const sender = new FileSender(file);
|
const sender = new FileSender(file);
|
||||||
sender.on('progress', render);
|
sender.on('progress', updateProgress);
|
||||||
sender.on('encrypting', render);
|
sender.on('encrypting', render);
|
||||||
state.transfer = sender;
|
state.transfer = sender;
|
||||||
|
state.uploading = true;
|
||||||
render();
|
render();
|
||||||
|
|
||||||
const links = openLinksInNewTab();
|
const links = openLinksInNewTab();
|
||||||
await delay(200);
|
await delay(200);
|
||||||
try {
|
try {
|
||||||
const start = Date.now();
|
|
||||||
metrics.startedUpload({ size, type });
|
metrics.startedUpload({ size, type });
|
||||||
const info = await sender.upload();
|
const ownedFile = await sender.upload();
|
||||||
const time = Date.now() - start;
|
ownedFile.type = type;
|
||||||
const speed = size / (time / 1000);
|
|
||||||
metrics.completedUpload({ size, time, speed, type });
|
|
||||||
await delay(1000);
|
|
||||||
await fadeOut('upload-progress');
|
|
||||||
info.name = file.name;
|
|
||||||
info.size = size;
|
|
||||||
info.type = type;
|
|
||||||
info.time = time;
|
|
||||||
info.speed = speed;
|
|
||||||
info.createdAt = Date.now();
|
|
||||||
info.url = `${info.url}#${info.secretKey}`;
|
|
||||||
info.expiresAt = Date.now() + EXPIRE_SECONDS * 1000;
|
|
||||||
state.fileInfo = info;
|
|
||||||
state.storage.addFile(state.fileInfo);
|
|
||||||
openLinksInNewTab(links, false);
|
|
||||||
state.transfer = null;
|
|
||||||
state.storage.totalUploads += 1;
|
state.storage.totalUploads += 1;
|
||||||
emitter.emit('pushState', `/share/${info.id}`);
|
metrics.completedUpload(ownedFile);
|
||||||
|
|
||||||
|
state.storage.addFile(ownedFile);
|
||||||
|
|
||||||
|
document.getElementById('cancel-upload').hidden = 'hidden';
|
||||||
|
await delay(1000);
|
||||||
|
await fadeOut('.page');
|
||||||
|
openLinksInNewTab(links, false);
|
||||||
|
emitter.emit('pushState', `/share/${ownedFile.id}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
state.transfer = null;
|
console.error(err);
|
||||||
|
|
||||||
if (err.message === '0') {
|
if (err.message === '0') {
|
||||||
//cancelled. do nothing
|
//cancelled. do nothing
|
||||||
metrics.cancelledUpload({ size, type });
|
metrics.cancelledUpload({ size, type });
|
||||||
@@ -145,41 +120,81 @@ export default function(state, emitter) {
|
|||||||
}
|
}
|
||||||
state.raven.captureException(err);
|
state.raven.captureException(err);
|
||||||
metrics.stoppedUpload({ size, type, err });
|
metrics.stoppedUpload({ size, type, err });
|
||||||
emitter.emit('replaceState', '/error');
|
emitter.emit('pushState', '/error');
|
||||||
|
} finally {
|
||||||
|
state.uploading = false;
|
||||||
|
state.transfer = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on('download', async file => {
|
emitter.on('password', async ({ password, file }) => {
|
||||||
const size = file.size;
|
try {
|
||||||
const url = `/api/download/${file.id}`;
|
state.settingPassword = true;
|
||||||
const receiver = new FileReceiver(url, file.key);
|
render();
|
||||||
receiver.on('progress', render);
|
await file.setPassword(password);
|
||||||
receiver.on('decrypting', render);
|
state.storage.writeFile(file);
|
||||||
state.transfer = receiver;
|
metrics.addedPassword({ size: file.size });
|
||||||
const links = openLinksInNewTab();
|
await delay(1000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
state.passwordSetError = err;
|
||||||
|
} finally {
|
||||||
|
state.settingPassword = false;
|
||||||
|
}
|
||||||
render();
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('getMetadata', async () => {
|
||||||
|
const file = state.fileInfo;
|
||||||
|
const receiver = new FileReceiver(file);
|
||||||
|
try {
|
||||||
|
await receiver.getMetadata();
|
||||||
|
state.transfer = receiver;
|
||||||
|
} catch (e) {
|
||||||
|
if (e.message === '401') {
|
||||||
|
file.password = null;
|
||||||
|
if (!file.requiresPassword) {
|
||||||
|
return emitter.emit('pushState', '/404');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('download', async file => {
|
||||||
|
state.transfer.on('progress', updateProgress);
|
||||||
|
state.transfer.on('decrypting', render);
|
||||||
|
const links = openLinksInNewTab();
|
||||||
|
const size = file.size;
|
||||||
try {
|
try {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
metrics.startedDownload({ size: file.size, ttl: file.ttl });
|
metrics.startedDownload({ size: file.size, ttl: file.ttl });
|
||||||
const f = await receiver.download();
|
const dl = state.transfer.download();
|
||||||
|
render();
|
||||||
|
await dl;
|
||||||
const time = Date.now() - start;
|
const time = Date.now() - start;
|
||||||
const speed = size / (time / 1000);
|
const speed = size / (time / 1000);
|
||||||
await delay(1000);
|
await delay(1000);
|
||||||
await fadeOut('download-progress');
|
await fadeOut('.page');
|
||||||
saveFile(f);
|
|
||||||
state.storage.totalDownloads += 1;
|
state.storage.totalDownloads += 1;
|
||||||
|
state.transfer.reset();
|
||||||
metrics.completedDownload({ size, time, speed });
|
metrics.completedDownload({ size, time, speed });
|
||||||
emitter.emit('pushState', '/completed');
|
emitter.emit('pushState', '/completed');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// TODO cancelled download
|
if (err.message === '0') {
|
||||||
const location = err.message === 'notfound' ? '/404' : '/error';
|
// download cancelled
|
||||||
|
state.transfer.reset();
|
||||||
|
return render();
|
||||||
|
}
|
||||||
|
console.error(err);
|
||||||
|
state.transfer = null;
|
||||||
|
const location = err.message === '404' ? '/404' : '/error';
|
||||||
if (location === '/error') {
|
if (location === '/error') {
|
||||||
state.raven.captureException(err);
|
state.raven.captureException(err);
|
||||||
metrics.stoppedDownload({ size, err });
|
metrics.stoppedDownload({ size, err });
|
||||||
}
|
}
|
||||||
emitter.emit('replaceState', location);
|
emitter.emit('pushState', location);
|
||||||
} finally {
|
} finally {
|
||||||
state.transfer = null;
|
|
||||||
openLinksInNewTab(links, false);
|
openLinksInNewTab(links, false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -189,6 +204,14 @@ export default function(state, emitter) {
|
|||||||
metrics.copiedLink({ location });
|
metrics.copiedLink({ location });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
// poll for updates of the download counts
|
||||||
|
// TODO something for the share page: || state.route === '/share/:id'
|
||||||
|
if (state.route === '/') {
|
||||||
|
checkFiles();
|
||||||
|
}
|
||||||
|
}, 2 * 60 * 1000);
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
// poll for rerendering the file list countdown timers
|
// poll for rerendering the file list countdown timers
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -1,32 +1,27 @@
|
|||||||
import Nanobus from 'nanobus';
|
import Nanobus from 'nanobus';
|
||||||
import { hexToArray, bytes } from './utils';
|
import Keychain from './keychain';
|
||||||
|
import { bytes } from './utils';
|
||||||
|
import { metadata, downloadFile } from './api';
|
||||||
|
|
||||||
export default class FileReceiver extends Nanobus {
|
export default class FileReceiver extends Nanobus {
|
||||||
constructor(url, k) {
|
constructor(fileInfo) {
|
||||||
super('FileReceiver');
|
super('FileReceiver');
|
||||||
this.key = window.crypto.subtle.importKey(
|
this.keychain = new Keychain(fileInfo.secretKey, fileInfo.nonce);
|
||||||
'jwk',
|
if (fileInfo.requiresPassword) {
|
||||||
{
|
this.keychain.setPassword(fileInfo.password, fileInfo.url);
|
||||||
k,
|
}
|
||||||
kty: 'oct',
|
this.fileInfo = fileInfo;
|
||||||
alg: 'A128GCM',
|
this.reset();
|
||||||
ext: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'AES-GCM'
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
['decrypt']
|
|
||||||
);
|
|
||||||
this.url = url;
|
|
||||||
this.msg = 'fileSizeProgress';
|
|
||||||
this.progress = [0, 1];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get progressRatio() {
|
get progressRatio() {
|
||||||
return this.progress[0] / this.progress[1];
|
return this.progress[0] / this.progress[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get progressIndefinite() {
|
||||||
|
return this.state !== 'downloading';
|
||||||
|
}
|
||||||
|
|
||||||
get sizes() {
|
get sizes() {
|
||||||
return {
|
return {
|
||||||
partialSize: bytes(this.progress[0]),
|
partialSize: bytes(this.progress[0]),
|
||||||
@@ -35,66 +30,95 @@ export default class FileReceiver extends Nanobus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cancel() {
|
cancel() {
|
||||||
// TODO
|
if (this.downloadRequest) {
|
||||||
|
this.downloadRequest.cancel();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadFile() {
|
reset() {
|
||||||
return new Promise((resolve, reject) => {
|
this.msg = 'fileSizeProgress';
|
||||||
const xhr = new XMLHttpRequest();
|
this.state = 'initialized';
|
||||||
|
this.progress = [0, 1];
|
||||||
xhr.onprogress = event => {
|
|
||||||
if (event.lengthComputable && event.target.status !== 404) {
|
|
||||||
this.progress = [event.loaded, event.total];
|
|
||||||
this.emit('progress', this.progress);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.onload = function(event) {
|
|
||||||
if (xhr.status === 404) {
|
|
||||||
reject(new Error('notfound'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const blob = new Blob([this.response]);
|
|
||||||
const meta = JSON.parse(xhr.getResponseHeader('X-File-Metadata'));
|
|
||||||
const fileReader = new FileReader();
|
|
||||||
fileReader.onload = function() {
|
|
||||||
resolve({
|
|
||||||
data: this.result,
|
|
||||||
name: meta.filename,
|
|
||||||
type: meta.mimeType,
|
|
||||||
iv: meta.id
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
fileReader.readAsArrayBuffer(blob);
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.open('get', this.url);
|
|
||||||
xhr.responseType = 'blob';
|
|
||||||
xhr.send();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async download() {
|
async getMetadata() {
|
||||||
const key = await this.key;
|
const meta = await metadata(this.fileInfo.id, this.keychain);
|
||||||
const file = await this.downloadFile();
|
this.keychain.setIV(meta.iv);
|
||||||
this.msg = 'decryptingFile';
|
this.fileInfo.name = meta.name;
|
||||||
this.emit('decrypting');
|
this.fileInfo.type = meta.type;
|
||||||
const plaintext = await window.crypto.subtle.decrypt(
|
this.fileInfo.iv = meta.iv;
|
||||||
{
|
this.fileInfo.size = meta.size;
|
||||||
name: 'AES-GCM',
|
this.state = 'ready';
|
||||||
iv: hexToArray(file.iv),
|
}
|
||||||
tagLength: 128
|
|
||||||
},
|
async download(noSave = false) {
|
||||||
key,
|
this.state = 'downloading';
|
||||||
file.data
|
this.downloadRequest = await downloadFile(
|
||||||
|
this.fileInfo.id,
|
||||||
|
this.keychain,
|
||||||
|
p => {
|
||||||
|
this.progress = p;
|
||||||
|
this.emit('progress');
|
||||||
|
}
|
||||||
);
|
);
|
||||||
this.msg = 'downloadFinish';
|
try {
|
||||||
return {
|
const ciphertext = await this.downloadRequest.result;
|
||||||
plaintext,
|
this.downloadRequest = null;
|
||||||
name: decodeURIComponent(file.name),
|
this.msg = 'decryptingFile';
|
||||||
type: file.type
|
this.state = 'decrypting';
|
||||||
};
|
this.emit('decrypting');
|
||||||
|
const plaintext = await this.keychain.decryptFile(ciphertext);
|
||||||
|
if (!noSave) {
|
||||||
|
await saveFile({
|
||||||
|
plaintext,
|
||||||
|
name: decodeURIComponent(this.fileInfo.name),
|
||||||
|
type: this.fileInfo.type
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.msg = 'downloadFinish';
|
||||||
|
this.state = 'complete';
|
||||||
|
} catch (e) {
|
||||||
|
this.downloadRequest = null;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveFile(file) {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
const dataView = new DataView(file.plaintext);
|
||||||
|
const blob = new Blob([dataView], { type: file.type });
|
||||||
|
|
||||||
|
if (navigator.msSaveBlob) {
|
||||||
|
navigator.msSaveBlob(blob, file.name);
|
||||||
|
return resolve();
|
||||||
|
} else if (/iPhone|fxios/i.test(navigator.userAgent)) {
|
||||||
|
// This method is much slower but createObjectURL
|
||||||
|
// is buggy on iOS
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.addEventListener('loadend', function() {
|
||||||
|
if (reader.error) {
|
||||||
|
return reject(reader.error);
|
||||||
|
}
|
||||||
|
if (reader.result) {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = reader.result;
|
||||||
|
a.download = file.name;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
reader.readAsDataURL(blob);
|
||||||
|
} else {
|
||||||
|
const downloadUrl = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = downloadUrl;
|
||||||
|
a.download = file.name;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(downloadUrl);
|
||||||
|
setTimeout(resolve, 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,48 +1,26 @@
|
|||||||
|
/* global EXPIRE_SECONDS */
|
||||||
import Nanobus from 'nanobus';
|
import Nanobus from 'nanobus';
|
||||||
import { arrayToHex, bytes } from './utils';
|
import OwnedFile from './ownedFile';
|
||||||
|
import Keychain from './keychain';
|
||||||
|
import { arrayToB64, bytes } from './utils';
|
||||||
|
import { uploadFile } from './api';
|
||||||
|
|
||||||
export default class FileSender extends Nanobus {
|
export default class FileSender extends Nanobus {
|
||||||
constructor(file) {
|
constructor(file) {
|
||||||
super('FileSender');
|
super('FileSender');
|
||||||
this.file = file;
|
this.file = file;
|
||||||
this.msg = 'importingFile';
|
this.keychain = new Keychain();
|
||||||
this.progress = [0, 1];
|
this.reset();
|
||||||
this.cancelled = false;
|
|
||||||
this.iv = window.crypto.getRandomValues(new Uint8Array(12));
|
|
||||||
this.uploadXHR = new XMLHttpRequest();
|
|
||||||
this.key = window.crypto.subtle.generateKey(
|
|
||||||
{
|
|
||||||
name: 'AES-GCM',
|
|
||||||
length: 128
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
['encrypt']
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static delete(id, token) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (!id || !token) {
|
|
||||||
return reject();
|
|
||||||
}
|
|
||||||
const xhr = new XMLHttpRequest();
|
|
||||||
xhr.open('POST', `/api/delete/${id}`);
|
|
||||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
|
||||||
|
|
||||||
xhr.onreadystatechange = () => {
|
|
||||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.send(JSON.stringify({ delete_token: token }));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get progressRatio() {
|
get progressRatio() {
|
||||||
return this.progress[0] / this.progress[1];
|
return this.progress[0] / this.progress[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get progressIndefinite() {
|
||||||
|
return ['fileSizeProgress', 'notifyUploadDone'].indexOf(this.msg) === -1;
|
||||||
|
}
|
||||||
|
|
||||||
get sizes() {
|
get sizes() {
|
||||||
return {
|
return {
|
||||||
partialSize: bytes(this.progress[0]),
|
partialSize: bytes(this.progress[0]),
|
||||||
@@ -50,10 +28,17 @@ export default class FileSender extends Nanobus {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.uploadRequest = null;
|
||||||
|
this.msg = 'importingFile';
|
||||||
|
this.progress = [0, 1];
|
||||||
|
this.cancelled = false;
|
||||||
|
}
|
||||||
|
|
||||||
cancel() {
|
cancel() {
|
||||||
this.cancelled = true;
|
this.cancelled = true;
|
||||||
if (this.msg === 'fileSizeProgress') {
|
if (this.uploadRequest) {
|
||||||
this.uploadXHR.abort();
|
this.uploadRequest.cancel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,6 +46,7 @@ export default class FileSender extends Nanobus {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.readAsArrayBuffer(this.file);
|
reader.readAsArrayBuffer(this.file);
|
||||||
|
// TODO: progress?
|
||||||
reader.onload = function(event) {
|
reader.onload = function(event) {
|
||||||
const plaintext = new Uint8Array(this.result);
|
const plaintext = new Uint8Array(this.result);
|
||||||
resolve(plaintext);
|
resolve(plaintext);
|
||||||
@@ -71,76 +57,56 @@ export default class FileSender extends Nanobus {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadFile(encrypted, keydata) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const file = this.file;
|
|
||||||
const id = arrayToHex(this.iv);
|
|
||||||
const dataView = new DataView(encrypted);
|
|
||||||
const blob = new Blob([dataView], { type: file.type });
|
|
||||||
const fd = new FormData();
|
|
||||||
fd.append('data', blob, file.name);
|
|
||||||
|
|
||||||
const xhr = this.uploadXHR;
|
|
||||||
|
|
||||||
xhr.upload.addEventListener('progress', e => {
|
|
||||||
if (e.lengthComputable) {
|
|
||||||
this.progress = [e.loaded, e.total];
|
|
||||||
this.emit('progress', this.progress);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
xhr.onreadystatechange = () => {
|
|
||||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
|
||||||
if (xhr.status === 200) {
|
|
||||||
this.progress = [1, 1];
|
|
||||||
this.msg = 'notifyUploadDone';
|
|
||||||
const responseObj = JSON.parse(xhr.responseText);
|
|
||||||
return resolve({
|
|
||||||
url: responseObj.url,
|
|
||||||
id: responseObj.id,
|
|
||||||
secretKey: keydata.k,
|
|
||||||
deleteToken: responseObj.delete
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.msg = 'errorPageHeader';
|
|
||||||
reject(new Error(xhr.status));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.open('post', '/api/upload', true);
|
|
||||||
xhr.setRequestHeader(
|
|
||||||
'X-File-Metadata',
|
|
||||||
JSON.stringify({
|
|
||||||
id: id,
|
|
||||||
filename: encodeURIComponent(file.name)
|
|
||||||
})
|
|
||||||
);
|
|
||||||
xhr.send(fd);
|
|
||||||
this.msg = 'fileSizeProgress';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async upload() {
|
async upload() {
|
||||||
const key = await this.key;
|
const start = Date.now();
|
||||||
const plaintext = await this.readFile();
|
const plaintext = await this.readFile();
|
||||||
if (this.cancelled) {
|
if (this.cancelled) {
|
||||||
throw new Error(0);
|
throw new Error(0);
|
||||||
}
|
}
|
||||||
this.msg = 'encryptingFile';
|
this.msg = 'encryptingFile';
|
||||||
this.emit('encrypting');
|
this.emit('encrypting');
|
||||||
const encrypted = await window.crypto.subtle.encrypt(
|
const encrypted = await this.keychain.encryptFile(plaintext);
|
||||||
{
|
const metadata = await this.keychain.encryptMetadata(this.file);
|
||||||
name: 'AES-GCM',
|
const authKeyB64 = await this.keychain.authKeyB64();
|
||||||
iv: this.iv,
|
|
||||||
tagLength: 128
|
|
||||||
},
|
|
||||||
key,
|
|
||||||
plaintext
|
|
||||||
);
|
|
||||||
if (this.cancelled) {
|
if (this.cancelled) {
|
||||||
throw new Error(0);
|
throw new Error(0);
|
||||||
}
|
}
|
||||||
const keydata = await window.crypto.subtle.exportKey('jwk', key);
|
this.uploadRequest = uploadFile(
|
||||||
return this.uploadFile(encrypted, keydata);
|
encrypted,
|
||||||
|
metadata,
|
||||||
|
authKeyB64,
|
||||||
|
this.keychain,
|
||||||
|
p => {
|
||||||
|
this.progress = p;
|
||||||
|
this.emit('progress', p);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.msg = 'fileSizeProgress';
|
||||||
|
try {
|
||||||
|
const result = await this.uploadRequest.result;
|
||||||
|
const time = Date.now() - start;
|
||||||
|
this.msg = 'notifyUploadDone';
|
||||||
|
this.uploadRequest = null;
|
||||||
|
this.progress = [1, 1];
|
||||||
|
const secretKey = arrayToB64(this.keychain.rawSecret);
|
||||||
|
const ownedFile = new OwnedFile({
|
||||||
|
id: result.id,
|
||||||
|
url: `${result.url}#${secretKey}`,
|
||||||
|
name: this.file.name,
|
||||||
|
size: this.file.size,
|
||||||
|
time: time,
|
||||||
|
speed: this.file.size / (time / 1000),
|
||||||
|
createdAt: Date.now(),
|
||||||
|
expiresAt: Date.now() + EXPIRE_SECONDS * 1000,
|
||||||
|
secretKey: secretKey,
|
||||||
|
nonce: this.keychain.nonce,
|
||||||
|
ownerToken: result.ownerToken
|
||||||
|
});
|
||||||
|
return ownedFile;
|
||||||
|
} catch (e) {
|
||||||
|
this.msg = 'errorPageHeader';
|
||||||
|
this.uploadRequest = null;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
209
app/keychain.js
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
import { arrayToB64, b64ToArray } from './utils';
|
||||||
|
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
|
||||||
|
export default class Keychain {
|
||||||
|
constructor(secretKeyB64, nonce, ivB64) {
|
||||||
|
this._nonce = nonce || 'yRCdyQ1EMSA3mo4rqSkuNQ==';
|
||||||
|
if (ivB64) {
|
||||||
|
this.iv = b64ToArray(ivB64);
|
||||||
|
} else {
|
||||||
|
this.iv = window.crypto.getRandomValues(new Uint8Array(12));
|
||||||
|
}
|
||||||
|
if (secretKeyB64) {
|
||||||
|
this.rawSecret = b64ToArray(secretKeyB64);
|
||||||
|
} else {
|
||||||
|
this.rawSecret = window.crypto.getRandomValues(new Uint8Array(16));
|
||||||
|
}
|
||||||
|
this.secretKeyPromise = window.crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
this.rawSecret,
|
||||||
|
'HKDF',
|
||||||
|
false,
|
||||||
|
['deriveKey']
|
||||||
|
);
|
||||||
|
this.encryptKeyPromise = this.secretKeyPromise.then(function(secretKey) {
|
||||||
|
return window.crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: 'HKDF',
|
||||||
|
salt: new Uint8Array(),
|
||||||
|
info: encoder.encode('encryption'),
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
secretKey,
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
length: 128
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
['encrypt', 'decrypt']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
this.metaKeyPromise = this.secretKeyPromise.then(function(secretKey) {
|
||||||
|
return window.crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: 'HKDF',
|
||||||
|
salt: new Uint8Array(),
|
||||||
|
info: encoder.encode('metadata'),
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
secretKey,
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
length: 128
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
['encrypt', 'decrypt']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
this.authKeyPromise = this.secretKeyPromise.then(function(secretKey) {
|
||||||
|
return window.crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: 'HKDF',
|
||||||
|
salt: new Uint8Array(),
|
||||||
|
info: encoder.encode('authentication'),
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
secretKey,
|
||||||
|
{
|
||||||
|
name: 'HMAC',
|
||||||
|
hash: { name: 'SHA-256' }
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
['sign']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get nonce() {
|
||||||
|
return this._nonce;
|
||||||
|
}
|
||||||
|
|
||||||
|
set nonce(n) {
|
||||||
|
if (n && n !== this._nonce) {
|
||||||
|
this._nonce = n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setIV(ivB64) {
|
||||||
|
this.iv = b64ToArray(ivB64);
|
||||||
|
}
|
||||||
|
|
||||||
|
setPassword(password, shareUrl) {
|
||||||
|
this.authKeyPromise = window.crypto.subtle
|
||||||
|
.importKey('raw', encoder.encode(password), { name: 'PBKDF2' }, false, [
|
||||||
|
'deriveKey'
|
||||||
|
])
|
||||||
|
.then(passwordKey =>
|
||||||
|
window.crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: 'PBKDF2',
|
||||||
|
salt: encoder.encode(shareUrl),
|
||||||
|
iterations: 100,
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
passwordKey,
|
||||||
|
{
|
||||||
|
name: 'HMAC',
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
['sign']
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setAuthKey(authKeyB64) {
|
||||||
|
this.authKeyPromise = window.crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
b64ToArray(authKeyB64),
|
||||||
|
{
|
||||||
|
name: 'HMAC',
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
['sign']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async authKeyB64() {
|
||||||
|
const authKey = await this.authKeyPromise;
|
||||||
|
const rawAuth = await window.crypto.subtle.exportKey('raw', authKey);
|
||||||
|
return arrayToB64(new Uint8Array(rawAuth));
|
||||||
|
}
|
||||||
|
|
||||||
|
async authHeader() {
|
||||||
|
const authKey = await this.authKeyPromise;
|
||||||
|
const sig = await window.crypto.subtle.sign(
|
||||||
|
{
|
||||||
|
name: 'HMAC'
|
||||||
|
},
|
||||||
|
authKey,
|
||||||
|
b64ToArray(this.nonce)
|
||||||
|
);
|
||||||
|
return `send-v1 ${arrayToB64(new Uint8Array(sig))}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async encryptFile(plaintext) {
|
||||||
|
const encryptKey = await this.encryptKeyPromise;
|
||||||
|
const ciphertext = await window.crypto.subtle.encrypt(
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
iv: this.iv,
|
||||||
|
tagLength: 128
|
||||||
|
},
|
||||||
|
encryptKey,
|
||||||
|
plaintext
|
||||||
|
);
|
||||||
|
return ciphertext;
|
||||||
|
}
|
||||||
|
|
||||||
|
async encryptMetadata(metadata) {
|
||||||
|
const metaKey = await this.metaKeyPromise;
|
||||||
|
const ciphertext = await window.crypto.subtle.encrypt(
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
iv: new Uint8Array(12),
|
||||||
|
tagLength: 128
|
||||||
|
},
|
||||||
|
metaKey,
|
||||||
|
encoder.encode(
|
||||||
|
JSON.stringify({
|
||||||
|
iv: arrayToB64(this.iv),
|
||||||
|
name: metadata.name,
|
||||||
|
type: metadata.type || 'application/octet-stream'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return ciphertext;
|
||||||
|
}
|
||||||
|
|
||||||
|
async decryptFile(ciphertext) {
|
||||||
|
const encryptKey = await this.encryptKeyPromise;
|
||||||
|
const plaintext = await window.crypto.subtle.decrypt(
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
iv: this.iv,
|
||||||
|
tagLength: 128
|
||||||
|
},
|
||||||
|
encryptKey,
|
||||||
|
ciphertext
|
||||||
|
);
|
||||||
|
return plaintext;
|
||||||
|
}
|
||||||
|
|
||||||
|
async decryptMetadata(ciphertext) {
|
||||||
|
const metaKey = await this.metaKeyPromise;
|
||||||
|
const plaintext = await window.crypto.subtle.decrypt(
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
iv: new Uint8Array(12),
|
||||||
|
tagLength: 128
|
||||||
|
},
|
||||||
|
metaKey,
|
||||||
|
ciphertext
|
||||||
|
);
|
||||||
|
return JSON.parse(decoder.decode(plaintext));
|
||||||
|
}
|
||||||
|
}
|
||||||
16
app/main.css
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
@import './base.css';
|
||||||
|
@import './templates/header/header.css';
|
||||||
|
@import './templates/downloadButton/downloadButton.css';
|
||||||
|
@import './templates/progress/progress.css';
|
||||||
|
@import './templates/passwordInput/passwordInput.css';
|
||||||
|
@import './templates/downloadPassword/downloadPassword.css';
|
||||||
|
@import './templates/setPasswordSection/setPasswordSection.css';
|
||||||
|
@import './templates/footer/footer.css';
|
||||||
|
@import './templates/fxPromo/fxPromo.css';
|
||||||
|
@import './templates/selectbox/selectbox.css';
|
||||||
|
@import './templates/fileList/fileList.css';
|
||||||
|
@import './templates/file/file.css';
|
||||||
|
@import './templates/popup/popup.css';
|
||||||
|
@import './pages/welcome/welcome.css';
|
||||||
|
@import './pages/share/share.css';
|
||||||
|
@import './pages/unsupported/unsupported.css';
|
||||||
35
app/main.js
@@ -1,5 +1,5 @@
|
|||||||
|
import 'fluent-intl-polyfill';
|
||||||
import app from './routes';
|
import app from './routes';
|
||||||
import log from 'choo-log';
|
|
||||||
import locale from '../common/locales';
|
import locale from '../common/locales';
|
||||||
import fileManager from './fileManager';
|
import fileManager from './fileManager';
|
||||||
import dragManager from './dragManager';
|
import dragManager from './dragManager';
|
||||||
@@ -14,24 +14,35 @@ if (navigator.doNotTrack !== '1' && window.RAVEN_CONFIG) {
|
|||||||
Raven.config(window.SENTRY_ID, window.RAVEN_CONFIG).install();
|
Raven.config(window.SENTRY_ID, window.RAVEN_CONFIG).install();
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use(log());
|
|
||||||
|
|
||||||
app.use((state, emitter) => {
|
app.use((state, emitter) => {
|
||||||
// init state
|
|
||||||
state.transfer = null;
|
state.transfer = null;
|
||||||
state.fileInfo = null;
|
state.fileInfo = null;
|
||||||
state.translate = locale.getTranslator();
|
state.translate = locale.getTranslator();
|
||||||
state.storage = storage;
|
state.storage = storage;
|
||||||
state.raven = Raven;
|
state.raven = Raven;
|
||||||
state.config = {
|
window.appState = state;
|
||||||
uploadWindowStyle: 'upload-window',
|
emitter.on('DOMContentLoaded', async function checkSupport() {
|
||||||
uploadButtonStyle: 'browse btn'
|
let unsupportedReason = null;
|
||||||
};
|
if (
|
||||||
emitter.on('DOMContentLoaded', async () => {
|
// Firefox < 50
|
||||||
|
/firefox/i.test(navigator.userAgent) &&
|
||||||
|
parseInt(navigator.userAgent.match(/firefox\/*([^\n\r]*)\./i)[1], 10) < 50
|
||||||
|
) {
|
||||||
|
unsupportedReason = 'outdated';
|
||||||
|
}
|
||||||
|
if (/edge\/\d+/i.test(navigator.userAgent)) {
|
||||||
|
unsupportedReason = 'edge';
|
||||||
|
}
|
||||||
const ok = await canHasSend(assets.get('cryptofill.js'));
|
const ok = await canHasSend(assets.get('cryptofill.js'));
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
const reason = /firefox/i.test(navigator.userAgent) ? 'outdated' : 'gcm';
|
unsupportedReason = /firefox/i.test(navigator.userAgent)
|
||||||
emitter.emit('replaceState', `/unsupported/${reason}`);
|
? 'outdated'
|
||||||
|
: 'gcm';
|
||||||
|
}
|
||||||
|
if (unsupportedReason) {
|
||||||
|
setTimeout(() =>
|
||||||
|
emitter.emit('replaceState', `/unsupported/${unsupportedReason}`)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -41,4 +52,4 @@ app.use(fileManager);
|
|||||||
app.use(dragManager);
|
app.use(dragManager);
|
||||||
app.use(experiments);
|
app.use(experiments);
|
||||||
|
|
||||||
app.mount('#page-one');
|
app.mount('body');
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ export default function initialize(state, emitter) {
|
|||||||
});
|
});
|
||||||
//TODO restart handlers... somewhere
|
//TODO restart handlers... somewhere
|
||||||
});
|
});
|
||||||
|
emitter.on('exit', exitEvent);
|
||||||
|
emitter.on('experiment', experimentEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
function category() {
|
function category() {
|
||||||
@@ -81,6 +83,8 @@ function urlToMetric(url) {
|
|||||||
case 'https://testpilot.firefox.com/':
|
case 'https://testpilot.firefox.com/':
|
||||||
case 'https://testpilot.firefox.com/experiments/send':
|
case 'https://testpilot.firefox.com/experiments/send':
|
||||||
return 'testpilot';
|
return 'testpilot';
|
||||||
|
case 'https://www.mozilla.org/firefox/new/?utm_campaign=send-acquisition&utm_medium=referral&utm_source=send.firefox.com':
|
||||||
|
return 'promo';
|
||||||
default:
|
default:
|
||||||
return 'other';
|
return 'other';
|
||||||
}
|
}
|
||||||
@@ -147,6 +151,15 @@ function completedUpload(params) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addedPassword(params) {
|
||||||
|
return sendEvent('sender', 'password-added', {
|
||||||
|
cm1: params.size,
|
||||||
|
cm5: storage.totalUploads,
|
||||||
|
cm6: storage.files.length,
|
||||||
|
cm7: storage.totalDownloads
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function startedDownload(params) {
|
function startedDownload(params) {
|
||||||
return sendEvent('recipient', 'download-started', {
|
return sendEvent('recipient', 'download-started', {
|
||||||
cm1: params.size,
|
cm1: params.size,
|
||||||
@@ -191,6 +204,16 @@ function stoppedUpload(params) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function changedDownloadLimit(params) {
|
||||||
|
return sendEvent('sender', 'download-limit-changed', {
|
||||||
|
cm1: params.size,
|
||||||
|
cm5: storage.totalUploads,
|
||||||
|
cm6: storage.files.length,
|
||||||
|
cm7: storage.totalDownloads,
|
||||||
|
cm8: params.dlimit
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function completedDownload(params) {
|
function completedDownload(params) {
|
||||||
return sendEvent('recipient', 'download-stopped', {
|
return sendEvent('recipient', 'download-stopped', {
|
||||||
cm1: params.size,
|
cm1: params.size,
|
||||||
@@ -235,6 +258,11 @@ function exitEvent(target) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function experimentEvent(params) {
|
||||||
|
return sendEvent(category(), 'experiment', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
function addExitHandlers() {
|
function addExitHandlers() {
|
||||||
const links = Array.from(document.querySelectorAll('a'));
|
const links = Array.from(document.querySelectorAll('a'));
|
||||||
links.forEach(l => {
|
links.forEach(l => {
|
||||||
@@ -257,11 +285,13 @@ export {
|
|||||||
cancelledUpload,
|
cancelledUpload,
|
||||||
stoppedUpload,
|
stoppedUpload,
|
||||||
completedUpload,
|
completedUpload,
|
||||||
|
changedDownloadLimit,
|
||||||
deletedUpload,
|
deletedUpload,
|
||||||
startedDownload,
|
startedDownload,
|
||||||
cancelledDownload,
|
cancelledDownload,
|
||||||
stoppedDownload,
|
stoppedDownload,
|
||||||
completedDownload,
|
completedDownload,
|
||||||
|
addedPassword,
|
||||||
restart,
|
restart,
|
||||||
unsupported
|
unsupported
|
||||||
};
|
};
|
||||||
|
|||||||
84
app/ownedFile.js
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import Keychain from './keychain';
|
||||||
|
import { arrayToB64 } from './utils';
|
||||||
|
import { del, fileInfo, setParams, setPassword } from './api';
|
||||||
|
|
||||||
|
export default class OwnedFile {
|
||||||
|
constructor(obj) {
|
||||||
|
this.id = obj.id;
|
||||||
|
this.url = obj.url;
|
||||||
|
this.name = obj.name;
|
||||||
|
this.size = obj.size;
|
||||||
|
this.type = obj.type;
|
||||||
|
this.time = obj.time;
|
||||||
|
this.speed = obj.speed;
|
||||||
|
this.createdAt = obj.createdAt;
|
||||||
|
this.expiresAt = obj.expiresAt;
|
||||||
|
this.ownerToken = obj.ownerToken;
|
||||||
|
this.dlimit = obj.dlimit || 1;
|
||||||
|
this.dtotal = obj.dtotal || 0;
|
||||||
|
this.keychain = new Keychain(obj.secretKey, obj.nonce);
|
||||||
|
this._hasPassword = !!obj.hasPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setPassword(password) {
|
||||||
|
try {
|
||||||
|
this.password = password;
|
||||||
|
this._hasPassword = true;
|
||||||
|
this.keychain.setPassword(password, this.url);
|
||||||
|
const result = await setPassword(this.id, this.ownerToken, this.keychain);
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
this.password = null;
|
||||||
|
this._hasPassword = false;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
del() {
|
||||||
|
return del(this.id, this.ownerToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
changeLimit(dlimit) {
|
||||||
|
if (this.dlimit !== dlimit) {
|
||||||
|
this.dlimit = dlimit;
|
||||||
|
return setParams(this.id, this.ownerToken, { dlimit });
|
||||||
|
}
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasPassword() {
|
||||||
|
return !!this._hasPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateDownloadCount() {
|
||||||
|
try {
|
||||||
|
const result = await fileInfo(this.id, this.ownerToken);
|
||||||
|
this.dtotal = result.dtotal;
|
||||||
|
this.dlimit = result.dlimit;
|
||||||
|
} catch (e) {
|
||||||
|
if (e.message === '404') {
|
||||||
|
this.dtotal = this.dlimit;
|
||||||
|
}
|
||||||
|
// ignore other errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
url: this.url,
|
||||||
|
name: this.name,
|
||||||
|
size: this.size,
|
||||||
|
type: this.type,
|
||||||
|
time: this.time,
|
||||||
|
speed: this.speed,
|
||||||
|
createdAt: this.createdAt,
|
||||||
|
expiresAt: this.expiresAt,
|
||||||
|
secretKey: arrayToB64(this.keychain.rawSecret),
|
||||||
|
ownerToken: this.ownerToken,
|
||||||
|
dlimit: this.dlimit,
|
||||||
|
dtotal: this.dtotal,
|
||||||
|
hasPassword: this.hasPassword
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
5
app/pages/blank.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
|
||||||
|
module.exports = function() {
|
||||||
|
return html`<div></div>`;
|
||||||
|
};
|
||||||
26
app/pages/completed/index.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const progress = require('../../templates/progress');
|
||||||
|
const { fadeOut } = require('../../utils');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
return html`
|
||||||
|
<div class="page effect--fadeIn">
|
||||||
|
<div class="title">
|
||||||
|
${state.translate('downloadFinish')}
|
||||||
|
</div>
|
||||||
|
<div class="description"></div>
|
||||||
|
${progress(1)}
|
||||||
|
<div class="progressSection">
|
||||||
|
<div class="progressSection__text"></div>
|
||||||
|
</div>
|
||||||
|
<a class="link link--action"
|
||||||
|
href="/"
|
||||||
|
onclick=${sendNew}>${state.translate('sendYourFilesLink')}</a>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
async function sendNew(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
await fadeOut('.page');
|
||||||
|
emit('pushState', '/');
|
||||||
|
}
|
||||||
|
};
|
||||||
42
app/pages/download/index.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const progress = require('../../templates/progress');
|
||||||
|
const { bytes } = require('../../utils');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
const transfer = state.transfer;
|
||||||
|
const cancelBtn = html`
|
||||||
|
<button
|
||||||
|
id="cancel"
|
||||||
|
class="btn btn--cancel"
|
||||||
|
title="${state.translate('deletePopupCancel')}"
|
||||||
|
onclick=${cancel}>
|
||||||
|
${state.translate('deletePopupCancel')}
|
||||||
|
</button>`;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="page effect--fadeIn">
|
||||||
|
<div class="title">
|
||||||
|
${state.translate('downloadingPageProgress', {
|
||||||
|
filename: state.fileInfo.name,
|
||||||
|
size: bytes(state.fileInfo.size)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div class="description">
|
||||||
|
${state.translate('downloadingPageMessage')}
|
||||||
|
</div>
|
||||||
|
${progress(transfer.progressRatio, transfer.progressIndefinite)}
|
||||||
|
<div class="progressSection">
|
||||||
|
<div class="progressSection__text">
|
||||||
|
${state.translate(transfer.msg, transfer.sizes)}
|
||||||
|
</div>
|
||||||
|
${transfer.state === 'downloading' ? cancelBtn : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
const btn = document.getElementById('cancel');
|
||||||
|
btn.remove();
|
||||||
|
emit('cancel');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
const html = require('choo/html');
|
const html = require('choo/html');
|
||||||
const assets = require('../../common/assets');
|
const assets = require('../../../common/assets');
|
||||||
|
|
||||||
module.exports = function(state) {
|
module.exports = function(state) {
|
||||||
return html`
|
return html`
|
||||||
<div id="upload-error">
|
<div class="page">
|
||||||
<div class="title">${state.translate('errorPageHeader')}</div>
|
<div class="title">${state.translate('errorPageHeader')}</div>
|
||||||
<img id="upload-error-img" src="${assets.get('illustration_error.svg')}"/>
|
<img src="${assets.get('illustration_error.svg')}"/>
|
||||||
</div>`;
|
</div>`;
|
||||||
};
|
};
|
||||||
32
app/pages/legal.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const raw = require('choo/html/raw');
|
||||||
|
|
||||||
|
module.exports = function(state) {
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
<div class="title">${state.translate('legalHeader')}</div>
|
||||||
|
${raw(
|
||||||
|
replaceLinks(state.translate('legalNoticeTestPilot'), [
|
||||||
|
'https://testpilot.firefox.com/terms',
|
||||||
|
'https://testpilot.firefox.com/privacy',
|
||||||
|
'https://testpilot.firefox.com/experiments/send'
|
||||||
|
])
|
||||||
|
)}
|
||||||
|
${raw(
|
||||||
|
replaceLinks(state.translate('legalNoticeMozilla'), [
|
||||||
|
'https://www.mozilla.org/privacy/websites/',
|
||||||
|
'https://www.mozilla.org/about/legal/terms/mozilla/'
|
||||||
|
])
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
function replaceLinks(str, urls) {
|
||||||
|
let i = 0;
|
||||||
|
const s = str.replace(
|
||||||
|
/<a>([^<]+)<\/a>/g,
|
||||||
|
(m, v) => `<a href="${urls[i++]}">${v}</a>`
|
||||||
|
);
|
||||||
|
return `<div class="description">${s}</div>`;
|
||||||
|
}
|
||||||
16
app/pages/notFound/index.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const assets = require('../../../common/assets');
|
||||||
|
|
||||||
|
module.exports = function(state) {
|
||||||
|
return html`
|
||||||
|
<div class="page">
|
||||||
|
<div class="title">${state.translate('expiredPageHeader')}</div>
|
||||||
|
<img src="${assets.get('illustration_expired.svg')}" id="expired-img">
|
||||||
|
<div class="description">
|
||||||
|
${state.translate('uploadPageExplainer')}
|
||||||
|
</div>
|
||||||
|
<a class="link link--action" href="/">
|
||||||
|
${state.translate('sendYourFilesLink')}
|
||||||
|
</a>
|
||||||
|
</div>`;
|
||||||
|
};
|
||||||
40
app/pages/preview/index.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const assets = require('../../../common/assets');
|
||||||
|
const { bytes } = require('../../utils');
|
||||||
|
|
||||||
|
module.exports = function(state, pageAction) {
|
||||||
|
const fileInfo = state.fileInfo;
|
||||||
|
|
||||||
|
const size = fileInfo.size
|
||||||
|
? state.translate('downloadFileSize', { size: bytes(fileInfo.size) })
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const title = fileInfo.name
|
||||||
|
? state.translate('downloadFileName', { filename: fileInfo.name })
|
||||||
|
: state.translate('downloadFileTitle');
|
||||||
|
|
||||||
|
const info = html`
|
||||||
|
<div id="dl-file"
|
||||||
|
data-nonce="${fileInfo.nonce}"
|
||||||
|
data-requires-password="${fileInfo.requiresPassword}"></div>`;
|
||||||
|
if (!pageAction) {
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<div class="page">
|
||||||
|
<div class="title">
|
||||||
|
<span>${title}</span>
|
||||||
|
<span>${' ' + size}</span>
|
||||||
|
</div>
|
||||||
|
<div class="description">${state.translate('downloadMessage')}</div>
|
||||||
|
<img
|
||||||
|
src="${assets.get('illustration_download.svg')}"
|
||||||
|
title="${state.translate('downloadAltText')}"/>
|
||||||
|
${pageAction}
|
||||||
|
<a class="link link--action" href="/">
|
||||||
|
${state.translate('sendYourFilesLink')}
|
||||||
|
</a>
|
||||||
|
${info}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
115
app/pages/share/index.js
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
/* global EXPIRE_SECONDS */
|
||||||
|
const html = require('choo/html');
|
||||||
|
const raw = require('choo/html/raw');
|
||||||
|
const assets = require('../../../common/assets');
|
||||||
|
const notFound = require('../notFound');
|
||||||
|
const setPasswordSection = require('../../templates/setPasswordSection');
|
||||||
|
const selectbox = require('../../templates/selectbox');
|
||||||
|
const deletePopup = require('../../templates/popup');
|
||||||
|
const { allowedCopy, delay, fadeOut } = require('../../utils');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
const file = state.storage.getFileById(state.params.id);
|
||||||
|
if (!file) {
|
||||||
|
return notFound(state, emit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div id="shareWrapper" class="effect--fadeIn">
|
||||||
|
<div class="title">${expireInfo(file, state.translate, emit)}</div>
|
||||||
|
<div class="sharePage">
|
||||||
|
<div class="sharePage__copyText">
|
||||||
|
${state.translate('copyUrlFormLabelWithName', { filename: file.name })}
|
||||||
|
</div>
|
||||||
|
<div class="copySection">
|
||||||
|
<input
|
||||||
|
id="fileUrl"
|
||||||
|
class="copySection__url"
|
||||||
|
type="url"
|
||||||
|
value="${file.url}"
|
||||||
|
readonly="true"/>
|
||||||
|
<button id="copyBtn"
|
||||||
|
class="inputBtn inputBtn--copy"
|
||||||
|
title="${state.translate('copyUrlFormButton')}"
|
||||||
|
onclick=${copyLink}>${state.translate('copyUrlFormButton')}</button>
|
||||||
|
</div>
|
||||||
|
${setPasswordSection(state, emit)}
|
||||||
|
<button
|
||||||
|
class="btn btn--delete"
|
||||||
|
title="${state.translate('deleteFileButton')}"
|
||||||
|
onclick=${showPopup}>${state.translate('deleteFileButton')}
|
||||||
|
</button>
|
||||||
|
<div class="sharePage__deletePopup">
|
||||||
|
${deletePopup(
|
||||||
|
state.translate('deletePopupText'),
|
||||||
|
state.translate('deletePopupYes'),
|
||||||
|
state.translate('deletePopupCancel'),
|
||||||
|
deleteFile
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<a class="link link--action"
|
||||||
|
href="/"
|
||||||
|
onclick=${sendNew}>${state.translate('sendAnotherFileLink')}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
function showPopup() {
|
||||||
|
const popup = document.querySelector('.popup');
|
||||||
|
popup.classList.add('popup--show');
|
||||||
|
popup.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendNew(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
await fadeOut('#shareWrapper');
|
||||||
|
emit('pushState', '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyLink() {
|
||||||
|
if (allowedCopy()) {
|
||||||
|
emit('copy', { url: file.url, location: 'success-screen' });
|
||||||
|
const input = document.getElementById('fileUrl');
|
||||||
|
input.disabled = true;
|
||||||
|
input.classList.add('input--copied');
|
||||||
|
const copyBtn = document.getElementById('copyBtn');
|
||||||
|
copyBtn.disabled = true;
|
||||||
|
copyBtn.classList.add('inputBtn--copied');
|
||||||
|
copyBtn.replaceChild(
|
||||||
|
html`<img src="${assets.get('check-16.svg')}" class="cursor--pointer">`,
|
||||||
|
copyBtn.firstChild
|
||||||
|
);
|
||||||
|
await delay(2000);
|
||||||
|
input.disabled = false;
|
||||||
|
input.classList.remove('input--copied');
|
||||||
|
copyBtn.disabled = false;
|
||||||
|
copyBtn.classList.remove('inputBtn--copied');
|
||||||
|
copyBtn.textContent = state.translate('copyUrlFormButton');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteFile() {
|
||||||
|
emit('delete', { file, location: 'success-screen' });
|
||||||
|
await fadeOut('#shareWrapper');
|
||||||
|
emit('pushState', '/');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function expireInfo(file, translate, emit) {
|
||||||
|
const hours = Math.floor(EXPIRE_SECONDS / 60 / 60);
|
||||||
|
const el = html`<div>${raw(
|
||||||
|
translate('expireInfo', {
|
||||||
|
downloadCount: '<select></select>',
|
||||||
|
timespan: translate('timespanHours', { num: hours })
|
||||||
|
})
|
||||||
|
)}</div>`;
|
||||||
|
const select = el.querySelector('select');
|
||||||
|
const options = [1, 2, 3, 4, 5, 20].filter(i => i > (file.dtotal || 0));
|
||||||
|
const t = num => translate('downloadCount', { num });
|
||||||
|
const changed = value => emit('changeLimit', { file, value });
|
||||||
|
select.parentNode.replaceChild(
|
||||||
|
selectbox(file.dlimit || 1, options, t, changed),
|
||||||
|
select
|
||||||
|
);
|
||||||
|
return el;
|
||||||
|
}
|
||||||
112
app/pages/share/share.css
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
.sharePage {
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 640px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sharePage__copyText {
|
||||||
|
align-self: flex-start;
|
||||||
|
margin-top: 60px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: var(--textColor);
|
||||||
|
max-width: 614px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sharePage__deletePopup {
|
||||||
|
position: relative;
|
||||||
|
align-self: center;
|
||||||
|
bottom: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copySection {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copySection__url {
|
||||||
|
flex: 1;
|
||||||
|
height: 56px;
|
||||||
|
border: 1px solid var(--primaryControlBGColor);
|
||||||
|
border-radius: 6px 0 0 6px;
|
||||||
|
font-size: 20px;
|
||||||
|
color: var(--inputTextColor);
|
||||||
|
font-family: 'SF Pro Text', sans-serif;
|
||||||
|
letter-spacing: 0;
|
||||||
|
line-height: 23px;
|
||||||
|
font-weight: 300;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copySection__url:disabled {
|
||||||
|
border: 1px solid var(--successControlBGColor);
|
||||||
|
background: var(--successControlFGColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputBtn--copy {
|
||||||
|
flex: 0 1 165px;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input--copied {
|
||||||
|
border-color: var(--successControlBGColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputBtn--copied,
|
||||||
|
.inputBtn--copied:hover {
|
||||||
|
background: var(--successControlBGColor);
|
||||||
|
border: 1px solid var(--successControlBGColor);
|
||||||
|
color: var(--successControlFGColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--delete {
|
||||||
|
align-self: center;
|
||||||
|
width: 176px;
|
||||||
|
height: 44px;
|
||||||
|
background: #fff;
|
||||||
|
border-color: rgba(12, 12, 13, 0.3);
|
||||||
|
margin-top: 50px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: #313131;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--delete:hover {
|
||||||
|
background: #efeff1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-device-width: 768px), (max-width: 768px) {
|
||||||
|
.copySection {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copySection__url {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-device-width: 520px), (max-width: 520px) {
|
||||||
|
.copySection {
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copySection__url {
|
||||||
|
font-size: 22px;
|
||||||
|
padding: 15px 10px;
|
||||||
|
border-radius: 6px 6px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sharePage__copyText {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputBtn--copy {
|
||||||
|
border-radius: 0 0 6px 6px;
|
||||||
|
flex: 0 1 65px;
|
||||||
|
}
|
||||||
|
}
|
||||||
67
app/pages/unsupported/index.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const assets = require('../../../common/assets');
|
||||||
|
|
||||||
|
module.exports = function(state) {
|
||||||
|
let strings = {};
|
||||||
|
let why = '';
|
||||||
|
let url = '';
|
||||||
|
let buttonAction = '';
|
||||||
|
if (state.params.reason !== 'outdated') {
|
||||||
|
strings = unsupportedStrings(state);
|
||||||
|
why = html`
|
||||||
|
<div class="description">
|
||||||
|
<a href="https://github.com/mozilla/send/blob/master/docs/faq.md#why-is-my-browser-not-supported">
|
||||||
|
${state.translate('notSupportedLink')}
|
||||||
|
</a>
|
||||||
|
</div>`;
|
||||||
|
url =
|
||||||
|
'https://www.mozilla.org/firefox/new/?utm_campaign=send-acquisition&utm_medium=referral&utm_source=send.firefox.com';
|
||||||
|
buttonAction = html`
|
||||||
|
<div class="firefoxDownload__action">
|
||||||
|
Firefox<br><span class="firefoxDownload__text">${strings.button}</span>
|
||||||
|
</div>`;
|
||||||
|
} else {
|
||||||
|
strings = outdatedStrings(state);
|
||||||
|
url = 'https://support.mozilla.org/kb/update-firefox-latest-version';
|
||||||
|
buttonAction = html`
|
||||||
|
<div class="firefoxDownload__action">
|
||||||
|
${strings.button}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<div class="unsupportedPage">
|
||||||
|
<div class="title">${strings.title}</div>
|
||||||
|
<div class="description">
|
||||||
|
${strings.description}
|
||||||
|
</div>
|
||||||
|
${why}
|
||||||
|
<a href="${url}" class="firefoxDownload">
|
||||||
|
<img
|
||||||
|
src="${assets.get('firefox_logo-only.svg')}"
|
||||||
|
class="firefoxDownload__logo"
|
||||||
|
alt="Firefox"/>
|
||||||
|
${buttonAction}
|
||||||
|
</a>
|
||||||
|
<div class="unsupportedPage__info">
|
||||||
|
${strings.explainer}
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
function outdatedStrings(state) {
|
||||||
|
return {
|
||||||
|
title: state.translate('notSupportedHeader'),
|
||||||
|
description: state.translate('notSupportedOutdatedDetail'),
|
||||||
|
button: state.translate('updateFirefox'),
|
||||||
|
explainer: state.translate('uploadPageExplainer')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function unsupportedStrings(state) {
|
||||||
|
return {
|
||||||
|
title: state.translate('notSupportedHeader'),
|
||||||
|
description: state.translate('notSupportedDetail'),
|
||||||
|
button: state.translate('downloadFirefoxButtonSub'),
|
||||||
|
explainer: state.translate('uploadPageExplainer')
|
||||||
|
};
|
||||||
|
}
|
||||||
49
app/pages/unsupported/unsupported.css
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
.unsupportedPage {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unsupportedPage__info {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 23px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--lightTextColor);
|
||||||
|
margin: 0 auto 23px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.firefoxDownload {
|
||||||
|
margin-bottom: 181px;
|
||||||
|
height: 80px;
|
||||||
|
background: #98e02b;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 0;
|
||||||
|
box-shadow: 0 5px 3px rgb(234, 234, 234);
|
||||||
|
font-family: 'Fira Sans', 'segoe ui', sans-serif;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--primaryControlFGColor);
|
||||||
|
font-size: 26px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.firefoxDownload__logo {
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.firefoxDownload__action {
|
||||||
|
text-align: left;
|
||||||
|
margin-left: 20.4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.firefoxDownload__text {
|
||||||
|
font-family: 'Fira Sans', 'segoe ui', sans-serif;
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 18px;
|
||||||
|
letter-spacing: -0.69px;
|
||||||
|
}
|
||||||
39
app/pages/upload/index.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const progress = require('../../templates/progress');
|
||||||
|
const { bytes } = require('../../utils');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
const transfer = state.transfer;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="page effect--fadeIn">
|
||||||
|
<div class="title">
|
||||||
|
${state.translate('uploadingPageProgress', {
|
||||||
|
filename: transfer.file.name,
|
||||||
|
size: bytes(transfer.file.size)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div class="description"></div>
|
||||||
|
${progress(transfer.progressRatio, transfer.progressIndefinite)}
|
||||||
|
<div class="progressSection">
|
||||||
|
<div class="progressSection__text">
|
||||||
|
${state.translate(transfer.msg, transfer.sizes)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
id="cancel-upload"
|
||||||
|
class="btn btn--cancel"
|
||||||
|
title="${state.translate('uploadingPageCancel')}"
|
||||||
|
onclick=${cancel}>
|
||||||
|
${state.translate('uploadingPageCancel')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
const btn = document.getElementById('cancel-upload');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = state.translate('uploadCancelNotification');
|
||||||
|
emit('cancel');
|
||||||
|
}
|
||||||
|
};
|
||||||
83
app/pages/welcome/index.js
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
/* global MAXFILESIZE */
|
||||||
|
const html = require('choo/html');
|
||||||
|
const assets = require('../../../common/assets');
|
||||||
|
const fileList = require('../../templates/fileList');
|
||||||
|
const { bytes, fadeOut } = require('../../utils');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
// the page flickers if both the server and browser set 'effect--fadeIn'
|
||||||
|
const fade = state.layout ? '' : 'effect--fadeIn';
|
||||||
|
return html`
|
||||||
|
<div id="page-one" class="${fade}">
|
||||||
|
<div class="title">${state.translate('uploadPageHeader')}</div>
|
||||||
|
<div class="description">
|
||||||
|
<div>${state.translate('uploadPageExplainer')}</div>
|
||||||
|
<a
|
||||||
|
href="https://testpilot.firefox.com/experiments/send"
|
||||||
|
class="link">
|
||||||
|
${state.translate('uploadPageLearnMore')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="uploadArea"
|
||||||
|
ondragover=${dragover}
|
||||||
|
ondragleave=${dragleave}>
|
||||||
|
<img
|
||||||
|
src="${assets.get('upload.svg')}"
|
||||||
|
title="${state.translate('uploadSvgAlt')}"/>
|
||||||
|
<div class="uploadArea__msg">
|
||||||
|
${state.translate('uploadPageDropMessage')}
|
||||||
|
</div>
|
||||||
|
<span class="uploadArea__sizeMsg">
|
||||||
|
${state.translate('uploadPageSizeMessage')}
|
||||||
|
</span>
|
||||||
|
<input id="file-upload"
|
||||||
|
class="inputFile"
|
||||||
|
type="file"
|
||||||
|
name="fileUploaded"
|
||||||
|
onfocus=${onfocus}
|
||||||
|
onblur=${onblur}
|
||||||
|
onchange=${upload} />
|
||||||
|
<label for="file-upload"
|
||||||
|
class="btn btn--file"
|
||||||
|
title="${state.translate('uploadPageBrowseButton1')}">
|
||||||
|
${state.translate('uploadPageBrowseButton1')}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
${fileList(state, emit)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
function dragover(event) {
|
||||||
|
const div = document.querySelector('.uploadArea');
|
||||||
|
div.classList.add('uploadArea--dragging');
|
||||||
|
}
|
||||||
|
|
||||||
|
function dragleave(event) {
|
||||||
|
const div = document.querySelector('.uploadArea');
|
||||||
|
div.classList.remove('uploadArea--dragging');
|
||||||
|
}
|
||||||
|
|
||||||
|
function onfocus(event) {
|
||||||
|
event.target.classList.add('inputFile--focused');
|
||||||
|
}
|
||||||
|
|
||||||
|
function onblur(event) {
|
||||||
|
event.target.classList.remove('inputFile--focused');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function upload(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const target = event.target;
|
||||||
|
const file = target.files[0];
|
||||||
|
if (file.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (file.size > MAXFILESIZE) {
|
||||||
|
window.alert(state.translate('fileTooBig', { size: bytes(MAXFILESIZE) }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await fadeOut('#page-one');
|
||||||
|
emit('upload', { file, type: 'click' });
|
||||||
|
}
|
||||||
|
};
|
||||||
65
app/pages/welcome/welcome.css
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
.uploadArea {
|
||||||
|
border: 3px dashed rgba(0, 148, 251, 0.5);
|
||||||
|
margin: 0 auto 10px;
|
||||||
|
height: 255px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
transition: transform 150ms;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadArea__msg {
|
||||||
|
font-size: 22px;
|
||||||
|
color: var(--lightTextColor);
|
||||||
|
margin: 20px 0 10px;
|
||||||
|
font-family: 'SF Pro Text', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadArea__sizeMsg {
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 16px;
|
||||||
|
color: var(--lightTextColor);
|
||||||
|
margin-bottom: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadArea--dragging {
|
||||||
|
border: 5px dashed rgba(0, 148, 251, 0.5);
|
||||||
|
height: 251px;
|
||||||
|
transform: scale(1.04);
|
||||||
|
border-radius: 4.2px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadArea--dragging * {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--file {
|
||||||
|
font-size: 20px;
|
||||||
|
min-width: 240px;
|
||||||
|
height: 60px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputFile {
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputFile--focused + .btn--file {
|
||||||
|
background-color: var(--primaryControlHoverColor);
|
||||||
|
outline: 1px dotted #000;
|
||||||
|
outline: -webkit-focus-ring-color auto 5px;
|
||||||
|
}
|
||||||
@@ -1,9 +1,60 @@
|
|||||||
const preview = require('../templates/preview');
|
const preview = require('../pages/preview');
|
||||||
const download = require('../templates/download');
|
const download = require('../pages/download');
|
||||||
|
const notFound = require('../pages/notFound');
|
||||||
|
const downloadPassword = require('../templates/downloadPassword');
|
||||||
|
const downloadButton = require('../templates/downloadButton');
|
||||||
|
|
||||||
|
function hasFileInfo() {
|
||||||
|
return !!document.getElementById('dl-file');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileInfoFromDOM() {
|
||||||
|
const el = document.getElementById('dl-file');
|
||||||
|
if (!el) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
nonce: el.getAttribute('data-nonce'),
|
||||||
|
requiresPassword: !!+el.getAttribute('data-requires-password')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFileInfo(state) {
|
||||||
|
const metadata = getFileInfoFromDOM();
|
||||||
|
return {
|
||||||
|
id: state.params.id,
|
||||||
|
secretKey: state.params.key,
|
||||||
|
nonce: metadata.nonce,
|
||||||
|
requiresPassword: metadata.requiresPassword
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = function(state, emit) {
|
module.exports = function(state, emit) {
|
||||||
if (state.transfer) {
|
if (!state.fileInfo) {
|
||||||
return download(state, emit);
|
// This is a fresh page load
|
||||||
|
// We need to parse the file info from the server's html
|
||||||
|
if (!hasFileInfo()) {
|
||||||
|
return notFound(state, emit);
|
||||||
|
}
|
||||||
|
state.fileInfo = createFileInfo(state);
|
||||||
|
|
||||||
|
if (!state.fileInfo.requiresPassword) {
|
||||||
|
emit('getMetadata');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return preview(state, emit);
|
|
||||||
|
let pageAction = null; //default state: we don't have file metadata
|
||||||
|
if (state.transfer) {
|
||||||
|
const s = state.transfer.state;
|
||||||
|
if (['downloading', 'decrypting', 'complete'].indexOf(s) > -1) {
|
||||||
|
// Downloading is in progress
|
||||||
|
return download(state, emit);
|
||||||
|
}
|
||||||
|
// we have file metadata
|
||||||
|
pageAction = downloadButton(state, emit);
|
||||||
|
} else if (state.fileInfo.requiresPassword && !state.fileInfo.password) {
|
||||||
|
// we're waiting on the user for a valid password
|
||||||
|
pageAction = downloadPassword(state, emit);
|
||||||
|
}
|
||||||
|
return preview(state, pageAction);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
const welcome = require('../templates/welcome');
|
const welcome = require('../pages/welcome');
|
||||||
const upload = require('../templates/upload');
|
const upload = require('../pages/upload');
|
||||||
|
|
||||||
module.exports = function(state, emit) {
|
module.exports = function(state, emit) {
|
||||||
if (state.transfer) {
|
if (state.uploading) {
|
||||||
return upload(state, emit);
|
return upload(state, emit);
|
||||||
}
|
}
|
||||||
return welcome(state, emit);
|
return welcome(state, emit);
|
||||||
|
|||||||
@@ -1,17 +1,58 @@
|
|||||||
const choo = require('choo');
|
const choo = require('choo');
|
||||||
|
const html = require('choo/html');
|
||||||
|
const nanotiming = require('nanotiming');
|
||||||
const download = require('./download');
|
const download = require('./download');
|
||||||
|
const header = require('../templates/header');
|
||||||
|
const footer = require('../templates/footer');
|
||||||
|
const fxPromo = require('../templates/fxPromo');
|
||||||
|
|
||||||
|
nanotiming.disabled = true;
|
||||||
const app = choo();
|
const app = choo();
|
||||||
|
|
||||||
app.route('/', require('./home'));
|
function banner(state, emit) {
|
||||||
app.route('/share/:id', require('../templates/share'));
|
if (state.promo && !state.route.startsWith('/unsupported/')) {
|
||||||
app.route('/download/:id', download);
|
return fxPromo(state, emit);
|
||||||
app.route('/download/:id/:key', download);
|
}
|
||||||
app.route('/completed', require('../templates/completed'));
|
}
|
||||||
app.route('/unsupported/:reason', require('../templates/unsupported'));
|
|
||||||
app.route('/legal', require('../templates/legal'));
|
function body(template) {
|
||||||
app.route('/error', require('../templates/error'));
|
return function(state, emit) {
|
||||||
app.route('/blank', require('../templates/blank'));
|
const b = html`<body>
|
||||||
app.route('*', require('../templates/notFound'));
|
${banner(state, emit)}
|
||||||
|
${header(state)}
|
||||||
|
<main class="main">
|
||||||
|
<noscript>
|
||||||
|
<div class="noscript">
|
||||||
|
<h2>${state.translate('javascriptRequired')}</h2>
|
||||||
|
<p>
|
||||||
|
<a class="link" href="https://github.com/mozilla/send/blob/master/docs/faq.md#why-does-firefox-send-require-javascript">
|
||||||
|
${state.translate('whyJavascript')}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p>${state.translate('enableJavascript')}</p>
|
||||||
|
</div>
|
||||||
|
</noscript>
|
||||||
|
${template(state, emit)}
|
||||||
|
</main>
|
||||||
|
${footer(state)}
|
||||||
|
</body>`;
|
||||||
|
if (state.layout) {
|
||||||
|
// server side only
|
||||||
|
return state.layout(state, b);
|
||||||
|
}
|
||||||
|
return b;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
app.route('/', body(require('./home')));
|
||||||
|
app.route('/share/:id', body(require('../pages/share')));
|
||||||
|
app.route('/download/:id', body(download));
|
||||||
|
app.route('/download/:id/:key', body(download));
|
||||||
|
app.route('/completed', body(require('../pages/completed')));
|
||||||
|
app.route('/unsupported/:reason', body(require('../pages/unsupported')));
|
||||||
|
app.route('/legal', body(require('../pages/legal')));
|
||||||
|
app.route('/error', body(require('../pages/error')));
|
||||||
|
app.route('/blank', body(require('../pages/blank')));
|
||||||
|
app.route('*', body(require('../pages/notFound')));
|
||||||
|
|
||||||
module.exports = app;
|
module.exports = app;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { isFile } from './utils';
|
import { isFile } from './utils';
|
||||||
|
import OwnedFile from './ownedFile';
|
||||||
|
|
||||||
class Mem {
|
class Mem {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -42,7 +43,7 @@ class Storage {
|
|||||||
const k = this.engine.key(i);
|
const k = this.engine.key(i);
|
||||||
if (isFile(k)) {
|
if (isFile(k)) {
|
||||||
try {
|
try {
|
||||||
const f = JSON.parse(this.engine.getItem(k));
|
const f = new OwnedFile(JSON.parse(this.engine.getItem(k)));
|
||||||
if (!f.id) {
|
if (!f.id) {
|
||||||
f.id = f.fileId;
|
f.id = f.fileId;
|
||||||
}
|
}
|
||||||
@@ -92,11 +93,7 @@ class Storage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getFileById(id) {
|
getFileById(id) {
|
||||||
try {
|
return this._files.find(f => f.id === id);
|
||||||
return JSON.parse(this.engine.getItem(id));
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get(id) {
|
get(id) {
|
||||||
@@ -112,8 +109,16 @@ class Storage {
|
|||||||
|
|
||||||
addFile(file) {
|
addFile(file) {
|
||||||
this._files.push(file);
|
this._files.push(file);
|
||||||
|
this.writeFile(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFile(file) {
|
||||||
this.engine.setItem(file.id, JSON.stringify(file));
|
this.engine.setItem(file.id, JSON.stringify(file));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
writeFiles() {
|
||||||
|
this._files.forEach(f => this.writeFile(f));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new Storage();
|
export default new Storage();
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
|
|
||||||
module.exports = function(state) {
|
|
||||||
const div = html`<div id="page-one"></div>`;
|
|
||||||
if (state.layout) {
|
|
||||||
return state.layout(state, div);
|
|
||||||
}
|
|
||||||
return div;
|
|
||||||
};
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
const progress = require('./progress');
|
|
||||||
const { fadeOut } = require('../utils');
|
|
||||||
|
|
||||||
module.exports = function(state, emit) {
|
|
||||||
const div = html`
|
|
||||||
<div id="download" class="fadeIn">
|
|
||||||
<div id="download-progress">
|
|
||||||
<div id="dl-title" class="title">${state.translate(
|
|
||||||
'downloadFinish'
|
|
||||||
)}</div>
|
|
||||||
<div class="description"></div>
|
|
||||||
${progress(1)}
|
|
||||||
<div class="upload">
|
|
||||||
<div class="progress-text"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a class="send-new" data-state="completed" href="/" onclick=${sendNew}>${state.translate(
|
|
||||||
'sendYourFilesLink'
|
|
||||||
)}</a>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
async function sendNew(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
await fadeOut('download');
|
|
||||||
emit('pushState', '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
return div;
|
|
||||||
};
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
const progress = require('./progress');
|
|
||||||
const { bytes } = require('../utils');
|
|
||||||
|
|
||||||
module.exports = function(state) {
|
|
||||||
const transfer = state.transfer;
|
|
||||||
const div = html`
|
|
||||||
<div id="download-progress" class="fadeIn">
|
|
||||||
<div id="dl-title" class="title">${state.translate(
|
|
||||||
'downloadingPageProgress',
|
|
||||||
{
|
|
||||||
filename: state.fileInfo.name,
|
|
||||||
size: bytes(state.fileInfo.size)
|
|
||||||
}
|
|
||||||
)}</div>
|
|
||||||
<div class="description">${state.translate('downloadingPageMessage')}</div>
|
|
||||||
${progress(transfer.progressRatio)}
|
|
||||||
<div class="upload">
|
|
||||||
<div class="progress-text">${state.translate(
|
|
||||||
transfer.msg,
|
|
||||||
transfer.sizes
|
|
||||||
)}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
return div;
|
|
||||||
};
|
|
||||||
6
app/templates/downloadButton/downloadButton.css
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
.btn--download {
|
||||||
|
width: 180px;
|
||||||
|
height: 44px;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
13
app/templates/downloadButton/index.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
return html`
|
||||||
|
<button class="btn btn--download"
|
||||||
|
onclick=${download}>${state.translate('downloadButtonLabel')}
|
||||||
|
</button>`;
|
||||||
|
|
||||||
|
function download(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
emit('download', state.fileInfo);
|
||||||
|
}
|
||||||
|
};
|
||||||
22
app/templates/downloadPassword/downloadPassword.css
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
.passwordSection {
|
||||||
|
text-align: left;
|
||||||
|
padding: 40px 0;
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.passwordForm {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-device-width: 520px), (max-width: 520px) {
|
||||||
|
.passwordSection {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.passwordForm {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
66
app/templates/downloadPassword/index.js
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
const fileInfo = state.fileInfo;
|
||||||
|
const invalid = fileInfo.password === null;
|
||||||
|
const label = invalid
|
||||||
|
? html`
|
||||||
|
<label class="error" for="password-input">
|
||||||
|
${state.translate('passwordTryAgain')}
|
||||||
|
</label>`
|
||||||
|
: html`
|
||||||
|
<label for="password-input">
|
||||||
|
${state.translate('unlockInputLabel')}
|
||||||
|
</label>`;
|
||||||
|
const inputClass = invalid
|
||||||
|
? 'input input--noBtn input--error'
|
||||||
|
: 'input input--noBtn';
|
||||||
|
const div = html`
|
||||||
|
<div class="passwordSection">
|
||||||
|
${label}
|
||||||
|
<form class="passwordForm" onsubmit=${checkPassword} data-no-csrf>
|
||||||
|
<input id="password-input"
|
||||||
|
class="${inputClass}"
|
||||||
|
maxlength="64"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="${state.translate('unlockInputPlaceholder')}"
|
||||||
|
oninput=${inputChanged}
|
||||||
|
type="password" />
|
||||||
|
<input type="submit"
|
||||||
|
id="password-btn"
|
||||||
|
class="inputBtn inputBtn--hidden"
|
||||||
|
value="${state.translate('unlockButtonLabel')}"/>
|
||||||
|
</form>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
if (!(div instanceof String)) {
|
||||||
|
setTimeout(() => document.getElementById('password-input').focus());
|
||||||
|
}
|
||||||
|
|
||||||
|
function inputChanged() {
|
||||||
|
const input = document.getElementById('password-input');
|
||||||
|
const btn = document.getElementById('password-btn');
|
||||||
|
input.classList.remove('input--error');
|
||||||
|
if (input.value.length > 0) {
|
||||||
|
btn.classList.remove('inputBtn--hidden');
|
||||||
|
input.classList.remove('input--noBtn');
|
||||||
|
} else {
|
||||||
|
btn.classList.add('inputBtn--hidden');
|
||||||
|
input.classList.add('input--noBtn');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkPassword(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const password = document.getElementById('password-input').value;
|
||||||
|
if (password.length > 0) {
|
||||||
|
document.getElementById('password-btn').disabled = true;
|
||||||
|
state.fileInfo.url = window.location.href;
|
||||||
|
state.fileInfo.password = password;
|
||||||
|
emit('getMetadata');
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return div;
|
||||||
|
};
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
const assets = require('../../common/assets');
|
|
||||||
|
|
||||||
function timeLeft(milliseconds) {
|
|
||||||
const minutes = Math.floor(milliseconds / 1000 / 60);
|
|
||||||
const hours = Math.floor(minutes / 60);
|
|
||||||
const seconds = Math.floor(milliseconds / 1000 % 60);
|
|
||||||
if (hours >= 1) {
|
|
||||||
return `${hours}h ${minutes % 60}m`;
|
|
||||||
} else if (hours === 0) {
|
|
||||||
return `${minutes}m ${seconds}s`;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = function(file, state, emit) {
|
|
||||||
const ttl = file.expiresAt - Date.now();
|
|
||||||
const remaining = timeLeft(ttl) || state.translate('linkExpiredAlt');
|
|
||||||
const row = html`
|
|
||||||
<tr id="${file.id}">
|
|
||||||
<td class="overflow-col" title="${file.name}">${file.name}</td>
|
|
||||||
<td class="center-col">
|
|
||||||
<img onclick=${copyClick} src="${assets.get(
|
|
||||||
'copy-16.svg'
|
|
||||||
)}" class="icon-copy" title="${state.translate('copyUrlHover')}">
|
|
||||||
<span class="text-copied" hidden="true">${state.translate(
|
|
||||||
'copiedUrl'
|
|
||||||
)}</span>
|
|
||||||
</td>
|
|
||||||
<td>${remaining}</td>
|
|
||||||
<td class="center-col">
|
|
||||||
<img onclick=${showPopup} src="${assets.get(
|
|
||||||
'close-16.svg'
|
|
||||||
)}" class="icon-delete" title="${state.translate('deleteButtonHover')}">
|
|
||||||
<div class="popup">
|
|
||||||
<div class="popuptext" onblur=${cancel} tabindex="-1">
|
|
||||||
<div class="popup-message">${state.translate('deletePopupText')}</div>
|
|
||||||
<div class="popup-action">
|
|
||||||
<span class="popup-no" onclick=${cancel}>${state.translate(
|
|
||||||
'deletePopupCancel'
|
|
||||||
)}</span>
|
|
||||||
<span class="popup-yes" onclick=${deleteFile}>${state.translate(
|
|
||||||
'deletePopupYes'
|
|
||||||
)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
`;
|
|
||||||
|
|
||||||
function copyClick(e) {
|
|
||||||
emit('copy', { url: file.url, location: 'upload-list' });
|
|
||||||
const icon = e.target;
|
|
||||||
const text = e.target.nextSibling;
|
|
||||||
icon.hidden = true;
|
|
||||||
text.hidden = false;
|
|
||||||
setTimeout(() => {
|
|
||||||
icon.hidden = false;
|
|
||||||
text.hidden = true;
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showPopup() {
|
|
||||||
const tr = document.getElementById(file.id);
|
|
||||||
const popup = tr.querySelector('.popuptext');
|
|
||||||
popup.classList.add('show');
|
|
||||||
popup.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancel(e) {
|
|
||||||
e.stopPropagation();
|
|
||||||
const tr = document.getElementById(file.id);
|
|
||||||
const popup = tr.querySelector('.popuptext');
|
|
||||||
popup.classList.remove('show');
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteFile() {
|
|
||||||
emit('delete', { file, location: 'upload-list' });
|
|
||||||
emit('render');
|
|
||||||
}
|
|
||||||
|
|
||||||
return row;
|
|
||||||
};
|
|
||||||
26
app/templates/file/file.css
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
.fileData {
|
||||||
|
font-size: 15px;
|
||||||
|
vertical-align: top;
|
||||||
|
color: var(--lightTextColor);
|
||||||
|
padding: 17px 19px 0;
|
||||||
|
line-height: 23px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileData--overflow {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileData--center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-device-width: 520px), (max-width: 520px) {
|
||||||
|
.fileData {
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 17px 5px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
87
app/templates/file/index.js
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const assets = require('../../../common/assets');
|
||||||
|
const number = require('../../utils').number;
|
||||||
|
const deletePopup = require('../popup');
|
||||||
|
|
||||||
|
module.exports = function(file, state, emit) {
|
||||||
|
const ttl = file.expiresAt - Date.now();
|
||||||
|
const remainingTime =
|
||||||
|
timeLeft(ttl, state) || state.translate('linkExpiredAlt');
|
||||||
|
const downloadLimit = file.dlimit || 1;
|
||||||
|
const totalDownloads = file.dtotal || 0;
|
||||||
|
return html`
|
||||||
|
<tr id="${file.id}">
|
||||||
|
<td class="fileData fileData--overflow" title="${file.name}">
|
||||||
|
<a class="link" href="/share/${file.id}">${file.name}</a>
|
||||||
|
</td>
|
||||||
|
<td class="fileData fileData--center">
|
||||||
|
<img
|
||||||
|
onclick=${copyClick}
|
||||||
|
src="${assets.get('copy-16.svg')}"
|
||||||
|
class="cursor--pointer"
|
||||||
|
title="${state.translate('copyUrlHover')}">
|
||||||
|
<span hidden="true">
|
||||||
|
${state.translate('copiedUrl')}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="fileData fileData--overflow">${remainingTime}</td>
|
||||||
|
<td class="fileData fileData--center">${number(totalDownloads)} / ${number(
|
||||||
|
downloadLimit
|
||||||
|
)}</td>
|
||||||
|
<td class="fileData fileData--center">
|
||||||
|
<img
|
||||||
|
onclick=${showPopup}
|
||||||
|
src="${assets.get('close-16.svg')}"
|
||||||
|
class="cursor--pointer"
|
||||||
|
title="${state.translate('deleteButtonHover')}">
|
||||||
|
${deletePopup(
|
||||||
|
state.translate('deletePopupText'),
|
||||||
|
state.translate('deletePopupYes'),
|
||||||
|
state.translate('deletePopupCancel'),
|
||||||
|
deleteFile
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
|
||||||
|
function copyClick(e) {
|
||||||
|
emit('copy', { url: file.url, location: 'upload-list' });
|
||||||
|
const icon = e.target;
|
||||||
|
const text = e.target.nextSibling;
|
||||||
|
icon.hidden = true;
|
||||||
|
text.hidden = false;
|
||||||
|
setTimeout(() => {
|
||||||
|
icon.hidden = false;
|
||||||
|
text.hidden = true;
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPopup() {
|
||||||
|
const tr = document.getElementById(file.id);
|
||||||
|
const popup = tr.querySelector('.popup');
|
||||||
|
popup.classList.add('popup--show');
|
||||||
|
popup.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteFile() {
|
||||||
|
emit('delete', { file, location: 'upload-list' });
|
||||||
|
emit('render');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function timeLeft(milliseconds, state) {
|
||||||
|
const minutes = Math.floor(milliseconds / 1000 / 60);
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
if (hours >= 1) {
|
||||||
|
return state.translate('expiresHoursMinutes', {
|
||||||
|
hours,
|
||||||
|
minutes: minutes % 60
|
||||||
|
});
|
||||||
|
} else if (hours === 0) {
|
||||||
|
if (minutes === 0) {
|
||||||
|
return state.translate('expiresMinutes', { minutes: '< 1' });
|
||||||
|
}
|
||||||
|
return state.translate('expiresMinutes', { minutes });
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
const file = require('./file');
|
|
||||||
|
|
||||||
module.exports = function(state, emit) {
|
|
||||||
let table = '';
|
|
||||||
if (state.storage.files.length) {
|
|
||||||
table = html`
|
|
||||||
<table id="uploaded-files">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th id="uploaded-file">${state.translate('uploadedFile')}</th>
|
|
||||||
<th id="copy-file-list" class="center-col">${state.translate(
|
|
||||||
'copyFileList'
|
|
||||||
)}</th>
|
|
||||||
<th id="expiry-file-list">${state.translate('expiryFileList')}</th>
|
|
||||||
<th id="delete-file-list" class="center-col">${state.translate(
|
|
||||||
'deleteFileList'
|
|
||||||
)}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
${state.storage.files.map(f => file(f, state, emit))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
return html`
|
|
||||||
<div id="file-list">
|
|
||||||
${table}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
};
|
|
||||||
52
app/templates/fileList/fileList.css
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
.fileList {
|
||||||
|
margin: 45.3px auto;
|
||||||
|
table-layout: fixed;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-family: 'Segoe UI', 'SF Pro Text', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileList__header {
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--lightTextColor);
|
||||||
|
font-weight: lighter;
|
||||||
|
text-align: left;
|
||||||
|
background: rgba(0, 148, 251, 0.05);
|
||||||
|
height: 40px;
|
||||||
|
border-top: 1px solid rgba(0, 148, 251, 0.1);
|
||||||
|
padding: 0 19px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileList__body {
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileList__nameCol {
|
||||||
|
width: 35%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileList__copyCol {
|
||||||
|
text-align: center;
|
||||||
|
width: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileList__expireCol {
|
||||||
|
width: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileList__dlCol {
|
||||||
|
width: 8%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileList__delCol {
|
||||||
|
text-align: center;
|
||||||
|
width: 7%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-device-width: 520px), (max-width: 520px) {
|
||||||
|
.fileList__header {
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
33
app/templates/fileList/index.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const file = require('../file');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
if (state.storage.files.length) {
|
||||||
|
return html`
|
||||||
|
<table class="fileList">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="fileList__header fileList__nameCol">
|
||||||
|
${state.translate('uploadedFile')}
|
||||||
|
</th>
|
||||||
|
<th class="fileList__header fileList__copyCol">
|
||||||
|
${state.translate('copyFileList')}
|
||||||
|
</th>
|
||||||
|
<th class="fileList__header fileList__expireCol" >
|
||||||
|
${state.translate('timeFileList')}
|
||||||
|
</th>
|
||||||
|
<th class="fileList__header fileList__dlCol" >
|
||||||
|
${state.translate('downloadsFileList')}
|
||||||
|
</th>
|
||||||
|
<th class="fileList__header fileList__delCol">
|
||||||
|
${state.translate('deleteFileList')}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="fileList__body">
|
||||||
|
${state.storage.files.map(f => file(f, state, emit))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
};
|
||||||
93
app/templates/footer/footer.css
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
.footer {
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 50px 31px 41px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legalSection {
|
||||||
|
max-width: 81vw;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legalSection__link {
|
||||||
|
color: var(--lightTextColor);
|
||||||
|
opacity: 0.9;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-right: 2vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legalSection__link:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legalSection__link:visited {
|
||||||
|
color: var(--lightTextColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.legalSection__mozLogo {
|
||||||
|
width: 112px;
|
||||||
|
height: 32px;
|
||||||
|
margin-bottom: -5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.socialSection {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 94px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.socialSection__link {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.socialSection__link:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.socialSection__icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
margin-bottom: -5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-device-width: 768px), (max-width: 768px) {
|
||||||
|
.footer {
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: flex-start;
|
||||||
|
max-width: 630px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legalSection__mozLogo {
|
||||||
|
margin-left: -7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legalSection {
|
||||||
|
flex-direction: column;
|
||||||
|
margin: auto;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legalSection__link {
|
||||||
|
display: block;
|
||||||
|
padding: 10px 0;
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.socialSection {
|
||||||
|
margin-top: 20px;
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
64
app/templates/footer/index.js
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const assets = require('../../../common/assets');
|
||||||
|
|
||||||
|
module.exports = function(state) {
|
||||||
|
return html`<footer class="footer">
|
||||||
|
<div class="legalSection">
|
||||||
|
<a
|
||||||
|
href="https://www.mozilla.org"
|
||||||
|
class="legalSection__link"
|
||||||
|
role="presentation">
|
||||||
|
<img
|
||||||
|
class="legalSection__mozLogo"
|
||||||
|
src="${assets.get('mozilla-logo.svg')}"
|
||||||
|
alt="mozilla"/>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://www.mozilla.org/about/legal"
|
||||||
|
class="legalSection__link">
|
||||||
|
${state.translate('footerLinkLegal')}
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://testpilot.firefox.com/about"
|
||||||
|
class="legalSection__link">
|
||||||
|
${state.translate('footerLinkAbout')}
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/legal"
|
||||||
|
class="legalSection__link">${state.translate('footerLinkPrivacy')}</a>
|
||||||
|
<a
|
||||||
|
href="/legal"
|
||||||
|
class="legalSection__link">${state.translate('footerLinkTerms')}</a>
|
||||||
|
<a
|
||||||
|
href="https://www.mozilla.org/privacy/websites/#cookies"
|
||||||
|
class="legalSection__link">
|
||||||
|
${state.translate('footerLinkCookies')}
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://www.mozilla.org/about/legal/report-infringement/"
|
||||||
|
class="legalSection__link">
|
||||||
|
${state.translate('reportIPInfringement')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="socialSection">
|
||||||
|
<a
|
||||||
|
href="https://github.com/mozilla/send"
|
||||||
|
class="socialSection__link"
|
||||||
|
role="presentation">
|
||||||
|
<img
|
||||||
|
class="socialSection__icon"
|
||||||
|
src="${assets.get('github-icon.svg')}"
|
||||||
|
alt="github"/>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://twitter.com/FxTestPilot"
|
||||||
|
class="socialSection__link"
|
||||||
|
role="presentation">
|
||||||
|
<img
|
||||||
|
class="socialSection__icon"
|
||||||
|
src="${assets.get('twitter-icon.svg')}"
|
||||||
|
alt="twitter"/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</footer>`;
|
||||||
|
};
|
||||||
56
app/templates/fxPromo/fxPromo.css
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
.fxPromo {
|
||||||
|
padding: 0 15px;
|
||||||
|
height: 48px;
|
||||||
|
background-color: #efeff1;
|
||||||
|
color: #4a4a4f;
|
||||||
|
font-size: 13px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-content: center;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fxPromo > div {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fxPromo > div > span {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fxPromo__logo {
|
||||||
|
width: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fxPromo--blue {
|
||||||
|
background: linear-gradient(-180deg, #45a1ff 0%, #00feff 94%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fxPromo--pink {
|
||||||
|
background: linear-gradient(-180deg, #ff9400 0%, #ff1ad9 94%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fxPromo--blue a {
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fxPromo--pink a {
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fxPromo--blue a:hover {
|
||||||
|
color: #eee;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fxPromo--pink a:hover {
|
||||||
|
color: #eee;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
34
app/templates/fxPromo/index.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const assets = require('../../../common/assets');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
let classes = 'fxPromo';
|
||||||
|
switch (state.promo) {
|
||||||
|
case 'blue':
|
||||||
|
classes = 'fxPromo fxPromo--blue';
|
||||||
|
break;
|
||||||
|
case 'pink':
|
||||||
|
classes = 'fxPromo fxPromo--pink';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="${classes}">
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
src="${assets.get('firefox_logo-only.svg')}"
|
||||||
|
class="fxPromo__logo"
|
||||||
|
alt="Firefox"/>
|
||||||
|
<span>Send is brought to you by the all-new Firefox.
|
||||||
|
<a
|
||||||
|
class="link"
|
||||||
|
href="https://www.mozilla.org/firefox/new/?utm_campaign=send-acquisition&utm_medium=referral&utm_source=send.firefox.com"
|
||||||
|
onclick=${clicked}
|
||||||
|
>Download Firefox now ≫</a></span>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
function clicked() {
|
||||||
|
emit('experiment', { cd3: 'promo' });
|
||||||
|
}
|
||||||
|
};
|
||||||
104
app/templates/header/header.css
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
.header {
|
||||||
|
align-items: flex-start;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 31px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo__link {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo__title {
|
||||||
|
color: #3e3d40;
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin: 0;
|
||||||
|
position: relative;
|
||||||
|
top: -1px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
margin-left: 8px;
|
||||||
|
transition: color 50ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo__title:hover {
|
||||||
|
color: var(--primaryControlBGColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo__subtitle {
|
||||||
|
color: #3e3d40;
|
||||||
|
font-size: 12px;
|
||||||
|
margin: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo__subtitle-link {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #3e3d40;
|
||||||
|
transition: color 50ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo__subtitle-link:hover {
|
||||||
|
color: var(--primaryControlBGColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback {
|
||||||
|
background-color: var(--primaryControlBGColor);
|
||||||
|
background-image: url('../assets/feedback.svg');
|
||||||
|
background-position: 2px 4px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: 18px;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid var(--primaryControlBGColor);
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||||
|
color: var(--primaryControlFGColor);
|
||||||
|
cursor: pointer;
|
||||||
|
display: block;
|
||||||
|
float: right;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 12px;
|
||||||
|
opacity: 0.9;
|
||||||
|
padding: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
min-width: 12px;
|
||||||
|
max-width: 12px;
|
||||||
|
text-indent: 17px;
|
||||||
|
transition: all 250ms ease-in-out;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback:hover,
|
||||||
|
.feedback:focus {
|
||||||
|
min-width: 30px;
|
||||||
|
max-width: 300px;
|
||||||
|
text-indent: 2px;
|
||||||
|
padding: 5px 5px 5px 20px;
|
||||||
|
background-color: var(--primaryControlHoverColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback:active {
|
||||||
|
background-color: var(--primaryControlHoverColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-device-width: 520px), (max-width: 520px) {
|
||||||
|
.header {
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback {
|
||||||
|
margin-top: 10px;
|
||||||
|
min-width: 30px;
|
||||||
|
max-width: 300px;
|
||||||
|
text-indent: 2px;
|
||||||
|
padding: 5px 5px 5px 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
59
app/templates/header/index.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const assets = require('../../../common/assets');
|
||||||
|
/*
|
||||||
|
The current weback config uses package.json to generate
|
||||||
|
version.json for /__version__ meaning `require` returns the
|
||||||
|
string 'version.json' in the frontend context but the json
|
||||||
|
on the server.
|
||||||
|
|
||||||
|
We want `version` to be constant at build time so this file
|
||||||
|
has a custom loader (/build/version_loader.js) just to replace
|
||||||
|
string with the value from package.json. 🤢
|
||||||
|
*/
|
||||||
|
const version = require('../../../package.json').version || 'VERSION';
|
||||||
|
const browser = browserName();
|
||||||
|
|
||||||
|
module.exports = function(state) {
|
||||||
|
const feedbackUrl = `https://qsurvey.mozilla.com/s3/txp-firefox-send?ver=${version}&browser=${browser}`;
|
||||||
|
return html`<header class="header">
|
||||||
|
<div class="logo">
|
||||||
|
<a class="logo__link" href="/">
|
||||||
|
<img
|
||||||
|
src="${assets.get('send_logo.svg')}"
|
||||||
|
alt="Send"/>
|
||||||
|
<h1 class="logo__title">Send</h1>
|
||||||
|
</a>
|
||||||
|
<div class="logo__subtitle">
|
||||||
|
<a class="logo__subtitle-link" href="https://testpilot.firefox.com">Firefox Test Pilot</a>
|
||||||
|
<div>${state.translate('siteSubtitle')}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a href="${feedbackUrl}"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
class="feedback"
|
||||||
|
target="_blank">${state.translate('siteFeedback')}</a>
|
||||||
|
</header>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
function browserName() {
|
||||||
|
try {
|
||||||
|
if (/firefox/i.test(navigator.userAgent)) {
|
||||||
|
return 'firefox';
|
||||||
|
}
|
||||||
|
if (/edge/i.test(navigator.userAgent)) {
|
||||||
|
return 'edge';
|
||||||
|
}
|
||||||
|
if (/trident/i.test(navigator.userAgent)) {
|
||||||
|
return 'ie';
|
||||||
|
}
|
||||||
|
if (/chrome/i.test(navigator.userAgent)) {
|
||||||
|
return 'chrome';
|
||||||
|
}
|
||||||
|
if (/safari/i.test(navigator.userAgent)) {
|
||||||
|
return 'safari';
|
||||||
|
}
|
||||||
|
return 'other';
|
||||||
|
} catch (e) {
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
|
|
||||||
function replaceLinks(str, urls) {
|
|
||||||
let i = -1;
|
|
||||||
const s = str.replace(/<a>([^<]+)<\/a>/g, (m, v) => {
|
|
||||||
i++;
|
|
||||||
return `<a href="${urls[i]}">${v}</a>`;
|
|
||||||
});
|
|
||||||
return [`<div class="description">${s}</div>`];
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = function(state) {
|
|
||||||
const div = html`
|
|
||||||
<div id="page-one">
|
|
||||||
<div id="legal">
|
|
||||||
<div class="title">${state.translate('legalHeader')}</div>
|
|
||||||
${html(
|
|
||||||
replaceLinks(state.translate('legalNoticeTestPilot'), [
|
|
||||||
'https://testpilot.firefox.com/terms',
|
|
||||||
'https://testpilot.firefox.com/privacy',
|
|
||||||
'https://testpilot.firefox.com/experiments/send'
|
|
||||||
])
|
|
||||||
)}
|
|
||||||
${html(
|
|
||||||
replaceLinks(state.translate('legalNoticeMozilla'), [
|
|
||||||
'https://www.mozilla.org/privacy/websites/',
|
|
||||||
'https://www.mozilla.org/about/legal/terms/mozilla/'
|
|
||||||
])
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
if (state.layout) {
|
|
||||||
return state.layout(state, div);
|
|
||||||
}
|
|
||||||
return div;
|
|
||||||
};
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
const assets = require('../../common/assets');
|
|
||||||
|
|
||||||
module.exports = function(state) {
|
|
||||||
const div = html`
|
|
||||||
<div id="page-one">
|
|
||||||
<div id="download">
|
|
||||||
<div class="title">${state.translate('expiredPageHeader')}</div>
|
|
||||||
<div class="share-window">
|
|
||||||
<img src="${assets.get('illustration_expired.svg')}" id="expired-img">
|
|
||||||
</div>
|
|
||||||
<div class="expired-description">${state.translate(
|
|
||||||
'uploadPageExplainer'
|
|
||||||
)}</div>
|
|
||||||
<a class="send-new" href="/" data-state="notfound">${state.translate(
|
|
||||||
'sendYourFilesLink'
|
|
||||||
)}</a>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
if (state.layout) {
|
|
||||||
return state.layout(state, div);
|
|
||||||
}
|
|
||||||
return div;
|
|
||||||
};
|
|
||||||
109
app/templates/passwordInput/index.js
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const MAX_LENGTH = 32;
|
||||||
|
|
||||||
|
module.exports = function(file, state, emit) {
|
||||||
|
const loading = state.settingPassword;
|
||||||
|
const pwd = file.hasPassword;
|
||||||
|
const sectionClass =
|
||||||
|
pwd || state.passwordSetError
|
||||||
|
? 'passwordInput'
|
||||||
|
: 'passwordInput passwordInput--hidden';
|
||||||
|
const inputClass = loading || pwd ? 'input' : 'input input--noBtn';
|
||||||
|
let btnClass = 'inputBtn inputBtn--password inputBtn--hidden';
|
||||||
|
if (loading) {
|
||||||
|
btnClass = 'inputBtn inputBtn--password inputBtn--loading';
|
||||||
|
} else if (pwd) {
|
||||||
|
btnClass = 'inputBtn inputBtn--password';
|
||||||
|
}
|
||||||
|
const action = pwd
|
||||||
|
? state.translate('changePasswordButton')
|
||||||
|
: state.translate('addPasswordButton');
|
||||||
|
return html`
|
||||||
|
<div class="${sectionClass}">
|
||||||
|
<form
|
||||||
|
class="passwordInput__form"
|
||||||
|
onsubmit=${setPassword}
|
||||||
|
data-no-csrf>
|
||||||
|
<input id="password-input"
|
||||||
|
${loading ? 'disabled' : ''}
|
||||||
|
class="${inputClass}"
|
||||||
|
maxlength="${MAX_LENGTH}"
|
||||||
|
autocomplete="off"
|
||||||
|
type="password"
|
||||||
|
oninput=${inputChanged}
|
||||||
|
onfocus=${focused}
|
||||||
|
placeholder="${
|
||||||
|
pwd && !state.passwordSetError
|
||||||
|
? passwordPlaceholder(file.password)
|
||||||
|
: state.translate('unlockInputPlaceholder')
|
||||||
|
}">
|
||||||
|
<input type="submit"
|
||||||
|
id="password-btn"
|
||||||
|
${loading ? 'disabled' : ''}
|
||||||
|
class="${btnClass}"
|
||||||
|
value="${loading ? '' : action}">
|
||||||
|
</form>
|
||||||
|
<label
|
||||||
|
class="passwordInput__msg ${
|
||||||
|
state.passwordSetError ? 'passwordInput__msg--error' : ''
|
||||||
|
}"
|
||||||
|
for="password-input">${message(state, pwd)}</label>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
function inputChanged() {
|
||||||
|
state.passwordSetError = null;
|
||||||
|
const resetInput = document.getElementById('password-input');
|
||||||
|
const resetBtn = document.getElementById('password-btn');
|
||||||
|
const pwdmsg = document.querySelector('.passwordInput__msg');
|
||||||
|
const length = resetInput.value.length;
|
||||||
|
|
||||||
|
if (length === MAX_LENGTH) {
|
||||||
|
pwdmsg.textContent = state.translate('maxPasswordLength', {
|
||||||
|
length: MAX_LENGTH
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
pwdmsg.textContent = '';
|
||||||
|
}
|
||||||
|
if (length > 0) {
|
||||||
|
resetBtn.classList.remove('inputBtn--hidden');
|
||||||
|
resetInput.classList.remove('input--noBtn');
|
||||||
|
} else {
|
||||||
|
resetBtn.classList.add('inputBtn--hidden');
|
||||||
|
resetInput.classList.add('input--noBtn');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function focused(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const el = document.getElementById('password-input');
|
||||||
|
if (el.placeholder !== state.translate('unlockInputPlaceholder')) {
|
||||||
|
el.placeholder = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPassword(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const el = document.getElementById('password-input');
|
||||||
|
const password = el.value;
|
||||||
|
if (password.length > 0) {
|
||||||
|
emit('password', { password, file });
|
||||||
|
} else {
|
||||||
|
el.focus();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function passwordPlaceholder(password) {
|
||||||
|
return password ? password.replace(/./g, '●') : '●●●●●●●●●●●●';
|
||||||
|
}
|
||||||
|
|
||||||
|
function message(state, pwd) {
|
||||||
|
if (state.passwordSetError) {
|
||||||
|
return state.translate('passwordSetError');
|
||||||
|
}
|
||||||
|
if (state.settingPassword || !pwd) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return state.translate('passwordIsSet');
|
||||||
|
}
|
||||||
42
app/templates/passwordInput/passwordInput.css
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
.passwordInput {
|
||||||
|
width: 90%;
|
||||||
|
height: 100px;
|
||||||
|
padding: 10px 5px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.passwordInput--hidden {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.passwordInput__form {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.passwordInput__msg {
|
||||||
|
font-size: 15px;
|
||||||
|
color: var(--lightTextColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.passwordInput__msg--error {
|
||||||
|
color: var(--errorColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputBtn--loading {
|
||||||
|
background-image: url('../assets/spinner.svg');
|
||||||
|
background-position: center;
|
||||||
|
background-size: 30px 30px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputBtn--password {
|
||||||
|
flex: 0 0 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-device-width: 520px), (max-width: 520px) {
|
||||||
|
.passwordInput {
|
||||||
|
flex-direction: column;
|
||||||
|
width: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
26
app/templates/popup/index.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
|
||||||
|
module.exports = function(msg, confirmText, cancelText, confirmCallback) {
|
||||||
|
return html`
|
||||||
|
<div class="popup__wrapper">
|
||||||
|
<div class="popup" onblur=${hide} tabindex="-1">
|
||||||
|
<div class="popup__message">${msg}</div>
|
||||||
|
<div class="popup__action">
|
||||||
|
<span class="popup__no" onclick=${hide}>
|
||||||
|
${cancelText}
|
||||||
|
</span>
|
||||||
|
<span class="popup__yes" onclick=${confirmCallback}>
|
||||||
|
${confirmText}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
function hide(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
const popup = document.querySelector('.popup.popup--show');
|
||||||
|
if (popup) {
|
||||||
|
popup.classList.remove('popup--show');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
122
app/templates/popup/popup.css
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
.popup {
|
||||||
|
visibility: hidden;
|
||||||
|
min-width: 204px;
|
||||||
|
min-height: 105px;
|
||||||
|
background-color: var(--pageBGColor);
|
||||||
|
color: var(--textColor);
|
||||||
|
border: 1px solid #d7d7db;
|
||||||
|
padding: 15px 24px;
|
||||||
|
box-sizing: content-box;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 5px;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
bottom: 20px;
|
||||||
|
left: -40px;
|
||||||
|
transition: opacity 0.5s;
|
||||||
|
opacity: 0;
|
||||||
|
outline: 0;
|
||||||
|
box-shadow: 3px 3px 7px rgba(136, 136, 136, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -11px;
|
||||||
|
left: 20px;
|
||||||
|
background-color: #fff;
|
||||||
|
display: block;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
border-radius: 0 0 5px;
|
||||||
|
border-right: 1px solid #d7d7db;
|
||||||
|
border-bottom: 1px solid #d7d7db;
|
||||||
|
border-left: 1px solid #fff;
|
||||||
|
border-top: 1px solid #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup__wrapper {
|
||||||
|
position: absolute;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup__message {
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px #ebebeb solid;
|
||||||
|
color: var(--textColor);
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: normal;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: calc(100% + 48px);
|
||||||
|
margin-left: -24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup__action {
|
||||||
|
margin-top: 15px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup__no {
|
||||||
|
color: #4a4a4a;
|
||||||
|
background-color: #fbfbfb;
|
||||||
|
border: 1px #c1c1c1 solid;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 5px 25px;
|
||||||
|
font-weight: normal;
|
||||||
|
min-width: 94px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup__no:hover {
|
||||||
|
background-color: #efeff1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup__yes {
|
||||||
|
color: var(--primaryControlFGColor);
|
||||||
|
background-color: var(--primaryControlBGColor);
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 5px 25px;
|
||||||
|
font-weight: normal;
|
||||||
|
cursor: pointer;
|
||||||
|
min-width: 94px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup__yes:hover {
|
||||||
|
background-color: var(--primaryControlHoverColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup--show {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-device-width: 992px), (max-width: 992px) {
|
||||||
|
.popup {
|
||||||
|
left: auto;
|
||||||
|
right: -40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup::after {
|
||||||
|
left: auto;
|
||||||
|
right: 36px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-device-width: 520px), (max-width: 520px) {
|
||||||
|
.popup::after {
|
||||||
|
left: 125px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
const assets = require('../../common/assets');
|
|
||||||
const notFound = require('./notFound');
|
|
||||||
const { bytes } = require('../utils');
|
|
||||||
|
|
||||||
function getFileFromDOM() {
|
|
||||||
const el = document.getElementById('dl-file');
|
|
||||||
if (!el) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const data = el.dataset;
|
|
||||||
return {
|
|
||||||
name: data.name,
|
|
||||||
size: parseInt(data.size, 10),
|
|
||||||
ttl: parseInt(data.ttl, 10)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = function(state, emit) {
|
|
||||||
state.fileInfo = state.fileInfo || getFileFromDOM();
|
|
||||||
if (!state.fileInfo) {
|
|
||||||
return notFound(state, emit);
|
|
||||||
}
|
|
||||||
state.fileInfo.id = state.params.id;
|
|
||||||
state.fileInfo.key = state.params.key;
|
|
||||||
const fileInfo = state.fileInfo;
|
|
||||||
const size = bytes(fileInfo.size);
|
|
||||||
const div = html`
|
|
||||||
<div id="page-one">
|
|
||||||
<div id="download">
|
|
||||||
<div id="download-page-one">
|
|
||||||
<div class="title">
|
|
||||||
<span id="dl-file"
|
|
||||||
data-name="${fileInfo.name}"
|
|
||||||
data-size="${fileInfo.size}"
|
|
||||||
data-ttl="${fileInfo.ttl}">${state.translate('downloadFileName', {
|
|
||||||
filename: fileInfo.name
|
|
||||||
})}</span>
|
|
||||||
<span id="dl-filesize">${' ' +
|
|
||||||
state.translate('downloadFileSize', { size })}</span>
|
|
||||||
</div>
|
|
||||||
<div class="description">${state.translate('downloadMessage')}</div>
|
|
||||||
<img src="${assets.get(
|
|
||||||
'illustration_download.svg'
|
|
||||||
)}" id="download-img" alt="${state.translate('downloadAltText')}"/>
|
|
||||||
<div>
|
|
||||||
<button id="download-btn" class="btn" onclick=${download}>${state.translate(
|
|
||||||
'downloadButtonLabel'
|
|
||||||
)}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a class="send-new" href="/">${state.translate('sendYourFilesLink')}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
function download(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
emit('download', fileInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.layout) {
|
|
||||||
return state.layout(state, div);
|
|
||||||
}
|
|
||||||
return div;
|
|
||||||
};
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
|
|
||||||
const radius = 73;
|
|
||||||
const oRadius = radius + 10;
|
|
||||||
const oDiameter = oRadius * 2;
|
|
||||||
const circumference = 2 * Math.PI * radius;
|
|
||||||
|
|
||||||
module.exports = function(progressRatio) {
|
|
||||||
const dashOffset = (1 - progressRatio) * circumference;
|
|
||||||
const percent = Math.floor(progressRatio * 100);
|
|
||||||
const div = html`
|
|
||||||
<div class="progress-bar">
|
|
||||||
<svg id="progress" width="${oDiameter}" height="${oDiameter}" viewPort="0 0 ${oDiameter} ${oDiameter}" version="1.1">
|
|
||||||
<circle r="${radius}" cx="${oRadius}" cy="${oRadius}" fill="transparent"/>
|
|
||||||
<circle id="bar" r="${radius}" cx="${oRadius}" cy="${oRadius}" fill="transparent" transform="rotate(-90 ${oRadius} ${oRadius})" stroke-dasharray="${circumference}" stroke-dashoffset="${dashOffset}"/>
|
|
||||||
<text class="percentage" text-anchor="middle" x="50%" y="98"><tspan class="percent-number">${percent}</tspan><tspan class="percent-sign">%</tspan></text>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
return div;
|
|
||||||
};
|
|
||||||
43
app/templates/progress/index.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const percent = require('../../utils').percent;
|
||||||
|
|
||||||
|
const radius = 73;
|
||||||
|
const oRadius = radius + 10;
|
||||||
|
const oDiameter = oRadius * 2;
|
||||||
|
const circumference = 2 * Math.PI * radius;
|
||||||
|
|
||||||
|
module.exports = function(progressRatio, indefinite = false) {
|
||||||
|
const p = indefinite ? 0.2 : progressRatio;
|
||||||
|
const dashOffset = (1 - p) * circumference;
|
||||||
|
const progressPercent = html`
|
||||||
|
<text class="progress__percent" text-anchor="middle" x="50%" y="98">
|
||||||
|
${percent(progressRatio)}
|
||||||
|
</text>`;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="progress">
|
||||||
|
<svg
|
||||||
|
width="${oDiameter}"
|
||||||
|
height="${oDiameter}"
|
||||||
|
viewPort="0 0 ${oDiameter} ${oDiameter}"
|
||||||
|
version="1.1">
|
||||||
|
<circle
|
||||||
|
class="progress__bg"
|
||||||
|
r="${radius}"
|
||||||
|
cx="${oRadius}"
|
||||||
|
cy="${oRadius}"
|
||||||
|
fill="transparent"/>
|
||||||
|
<circle
|
||||||
|
class="${indefinite ? 'progress__indefinite' : 'progress__bar'}"
|
||||||
|
r="${radius}"
|
||||||
|
cx="${oRadius}"
|
||||||
|
cy="${oRadius}"
|
||||||
|
fill="transparent"
|
||||||
|
transform="rotate(-90 ${oRadius} ${oRadius})"
|
||||||
|
stroke-dasharray="${circumference}"
|
||||||
|
stroke-dashoffset="${dashOffset}"/>
|
||||||
|
${indefinite ? '' : progressPercent}
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
39
app/templates/progress/progress.css
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
.progress {
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress__bg {
|
||||||
|
stroke: #eee;
|
||||||
|
stroke-width: 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress__bar {
|
||||||
|
stroke: #3b9dff;
|
||||||
|
stroke-width: 0.75em;
|
||||||
|
transition: stroke-dashoffset 300ms linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress__indefinite {
|
||||||
|
stroke: #3b9dff;
|
||||||
|
stroke-width: 0.75em;
|
||||||
|
animation: 1s linear infinite spin;
|
||||||
|
transform-origin: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress__percent {
|
||||||
|
font-family: 'Segoe UI', 'SF Pro Text', sans-serif;
|
||||||
|
font-size: 43.2px;
|
||||||
|
letter-spacing: -0.78px;
|
||||||
|
line-height: 58px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
59
app/templates/selectbox/index.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const number = require('../../utils').number;
|
||||||
|
|
||||||
|
module.exports = function(selected, options, translate, changed) {
|
||||||
|
const id = `select-${Math.random()}`;
|
||||||
|
let x = selected;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="selectbox">
|
||||||
|
<div onclick=${toggle}>
|
||||||
|
<span class="link">${translate(selected)}</span>
|
||||||
|
<svg width="32" height="32">
|
||||||
|
<polygon points="8 18 17 28 26 18" fill="#0094fb"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<ul id="${id}" class="selectbox__options">
|
||||||
|
${options.map(
|
||||||
|
i => html`
|
||||||
|
<li
|
||||||
|
class="selectbox__option"
|
||||||
|
onclick=${choose}
|
||||||
|
data-value="${i}">${number(i)}</li>`
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
const ul = document.getElementById(id);
|
||||||
|
const body = document.querySelector('body');
|
||||||
|
ul.classList.remove('selectbox__options--active');
|
||||||
|
body.removeEventListener('click', close);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
const ul = document.getElementById(id);
|
||||||
|
if (ul.classList.contains('selectbox__options--active')) {
|
||||||
|
close();
|
||||||
|
} else {
|
||||||
|
ul.classList.add('selectbox__options--active');
|
||||||
|
const body = document.querySelector('body');
|
||||||
|
body.addEventListener('click', close);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function choose(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
const target = event.target;
|
||||||
|
const value = +target.dataset.value;
|
||||||
|
target.parentNode.previousSibling.firstElementChild.textContent = translate(
|
||||||
|
value
|
||||||
|
);
|
||||||
|
if (x !== value) {
|
||||||
|
x = value;
|
||||||
|
changed(value);
|
||||||
|
}
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
};
|
||||||
36
app/templates/selectbox/selectbox.css
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
.selectbox {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectbox__options {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectbox__options--active {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
padding: 0;
|
||||||
|
margin: 40px 0;
|
||||||
|
background-color: var(--pageBGColor);
|
||||||
|
border: 1px solid rgba(12, 12, 13, 0.3);
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 1px 2px 4px rgba(12, 12, 13, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectbox__option {
|
||||||
|
color: var(--lightTextColor);
|
||||||
|
font-size: 12pt;
|
||||||
|
list-style: none;
|
||||||
|
user-select: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 0 60px;
|
||||||
|
border-bottom: 1px solid rgba(12, 12, 13, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectbox__option:hover {
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
}
|
||||||
37
app/templates/setPasswordSection/index.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const passwordInput = require('../passwordInput');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
const file = state.storage.getFileById(state.params.id);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="setPasswordSection">
|
||||||
|
<div class="checkbox">
|
||||||
|
<input
|
||||||
|
${file.hasPassword ? 'disabled' : ''}
|
||||||
|
${file.hasPassword || state.passwordSetError ? 'checked' : ''}
|
||||||
|
class="checkbox__input"
|
||||||
|
id="add-password"
|
||||||
|
type="checkbox"
|
||||||
|
autocomplete="off"
|
||||||
|
onchange=${togglePasswordInput}/>
|
||||||
|
<label class="checkbox__label" for="add-password">
|
||||||
|
${state.translate('requirePasswordCheckbox')}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
${passwordInput(file, state, emit)}
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
function togglePasswordInput(e) {
|
||||||
|
const unlockInput = document.getElementById('password-input');
|
||||||
|
const boxChecked = e.target.checked;
|
||||||
|
document
|
||||||
|
.querySelector('.passwordInput')
|
||||||
|
.classList.toggle('passwordInput--hidden', !boxChecked);
|
||||||
|
if (boxChecked) {
|
||||||
|
unlockInput.focus();
|
||||||
|
} else {
|
||||||
|
unlockInput.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
68
app/templates/setPasswordSection/setPasswordSection.css
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
.setPasswordSection {
|
||||||
|
padding: 10px 0;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
min-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox__input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox__label {
|
||||||
|
line-height: 23px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--lightTextColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox__label::before {
|
||||||
|
content: '';
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-left: 5px;
|
||||||
|
float: left;
|
||||||
|
border: 1px solid rgba(12, 12, 13, 0.3);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox__input:focus + .checkbox__label::before,
|
||||||
|
.checkbox:hover .checkbox__label::before {
|
||||||
|
border: 1px solid var(--primaryControlBGColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox__input:checked + .checkbox__label {
|
||||||
|
color: var(--textColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox__input:checked + .checkbox__label::before {
|
||||||
|
background-image: url('../assets/check-16-blue.svg');
|
||||||
|
background-position: 2px 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox__input:disabled + .checkbox__label {
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox__input:disabled + .checkbox__label::before {
|
||||||
|
background-image: url('../assets/check-16-blue.svg');
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: 26px 26px;
|
||||||
|
border: none;
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-device-width: 520px), (max-width: 520px) {
|
||||||
|
.setPasswordSection {
|
||||||
|
align-self: center;
|
||||||
|
min-width: 95%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox__label::before {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
const assets = require('../../common/assets');
|
|
||||||
const notFound = require('./notFound');
|
|
||||||
const { allowedCopy, delay, fadeOut } = require('../utils');
|
|
||||||
|
|
||||||
module.exports = function(state, emit) {
|
|
||||||
const file = state.storage.getFileById(state.params.id);
|
|
||||||
if (!file) {
|
|
||||||
return notFound(state, emit);
|
|
||||||
}
|
|
||||||
const div = html`
|
|
||||||
<div id="share-link" class="fadeIn">
|
|
||||||
<div class="title">${state.translate('uploadSuccessTimingHeader')}</div>
|
|
||||||
<div id="share-window">
|
|
||||||
<div id="copy-text">${state.translate('copyUrlFormLabelWithName', {
|
|
||||||
filename: file.name
|
|
||||||
})}</div>
|
|
||||||
<div id="copy">
|
|
||||||
<input id="link" type="url" value="${file.url}" readonly="true"/>
|
|
||||||
<button id="copy-btn" class="btn" onclick=${copyLink}>${state.translate(
|
|
||||||
'copyUrlFormButton'
|
|
||||||
)}</button>
|
|
||||||
</div>
|
|
||||||
<button id="delete-file" class="btn" onclick=${deleteFile}>${state.translate(
|
|
||||||
'deleteFileButton'
|
|
||||||
)}</button>
|
|
||||||
<a class="send-new" data-state="completed" href="/" onclick=${sendNew}>${state.translate(
|
|
||||||
'sendAnotherFileLink'
|
|
||||||
)}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
async function sendNew(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
await fadeOut('share-link');
|
|
||||||
emit('pushState', '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function copyLink() {
|
|
||||||
if (allowedCopy()) {
|
|
||||||
emit('copy', { url: file.url, location: 'success-screen' });
|
|
||||||
const input = document.getElementById('link');
|
|
||||||
input.disabled = true;
|
|
||||||
const copyBtn = document.getElementById('copy-btn');
|
|
||||||
copyBtn.disabled = true;
|
|
||||||
copyBtn.replaceChild(
|
|
||||||
html`<img src="${assets.get('check-16.svg')}" class="icon-check">`,
|
|
||||||
copyBtn.firstChild
|
|
||||||
);
|
|
||||||
await delay(2000);
|
|
||||||
input.disabled = false;
|
|
||||||
copyBtn.disabled = false;
|
|
||||||
copyBtn.textContent = state.translate('copyUrlFormButton');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteFile() {
|
|
||||||
emit('delete', { file, location: 'success-screen' });
|
|
||||||
await fadeOut('share-link');
|
|
||||||
emit('pushState', '/');
|
|
||||||
}
|
|
||||||
return div;
|
|
||||||
};
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
const assets = require('../../common/assets');
|
|
||||||
|
|
||||||
module.exports = function(state) {
|
|
||||||
const msg =
|
|
||||||
state.params.reason === 'outdated'
|
|
||||||
? html`
|
|
||||||
<div id="unsupported-browser">
|
|
||||||
<div class="title">${state.translate('notSupportedHeader')}</div>
|
|
||||||
<div class="description">${state.translate(
|
|
||||||
'notSupportedOutdatedDetail'
|
|
||||||
)}</div>
|
|
||||||
<a id="update-firefox" href="https://support.mozilla.org/kb/update-firefox-latest-version">
|
|
||||||
<img src="${assets.get(
|
|
||||||
'firefox_logo-only.svg'
|
|
||||||
)}" class="firefox-logo" alt="Firefox"/>
|
|
||||||
<div class="unsupported-button-text">${state.translate(
|
|
||||||
'updateFirefox'
|
|
||||||
)}</div>
|
|
||||||
</a>
|
|
||||||
<div class="unsupported-description">${state.translate(
|
|
||||||
'uploadPageExplainer'
|
|
||||||
)}</div>
|
|
||||||
</div>`
|
|
||||||
: html`
|
|
||||||
<div id="unsupported-browser">
|
|
||||||
<div class="title">${state.translate('notSupportedHeader')}</div>
|
|
||||||
<div class="description">${state.translate('notSupportedDetail')}</div>
|
|
||||||
<div class="description"><a href="https://github.com/mozilla/send/blob/master/docs/faq.md#why-is-my-browser-not-supported">${state.translate(
|
|
||||||
'notSupportedLink'
|
|
||||||
)}</a></div>
|
|
||||||
<a id="dl-firefox" href="https://www.mozilla.org/firefox/new/?scene=2">
|
|
||||||
<img src="${assets.get(
|
|
||||||
'firefox_logo-only.svg'
|
|
||||||
)}" class="firefox-logo" alt="Firefox"/>
|
|
||||||
<div class="unsupported-button-text">Firefox<br>
|
|
||||||
<span>${state.translate('downloadFirefoxButtonSub')}</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<div class="unsupported-description">${state.translate(
|
|
||||||
'uploadPageExplainer'
|
|
||||||
)}</div>
|
|
||||||
</div>`;
|
|
||||||
const div = html`<div id="page-one">${msg}</div>`;
|
|
||||||
|
|
||||||
if (state.layout) {
|
|
||||||
return state.layout(state, div);
|
|
||||||
}
|
|
||||||
return div;
|
|
||||||
};
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
const progress = require('./progress');
|
|
||||||
const { bytes } = require('../utils');
|
|
||||||
|
|
||||||
module.exports = function(state, emit) {
|
|
||||||
const transfer = state.transfer;
|
|
||||||
|
|
||||||
const div = html`
|
|
||||||
<div id="upload-progress" class="fadeIn">
|
|
||||||
<div class="title" id="upload-filename">${state.translate(
|
|
||||||
'uploadingPageProgress',
|
|
||||||
{
|
|
||||||
filename: transfer.file.name,
|
|
||||||
size: bytes(transfer.file.size)
|
|
||||||
}
|
|
||||||
)}</div>
|
|
||||||
<div class="description"></div>
|
|
||||||
${progress(transfer.progressRatio)}
|
|
||||||
<div class="upload">
|
|
||||||
<div class="progress-text">${state.translate(
|
|
||||||
transfer.msg,
|
|
||||||
transfer.sizes
|
|
||||||
)}</div>
|
|
||||||
<button id="cancel-upload" onclick=${cancel}>${state.translate(
|
|
||||||
'uploadingPageCancel'
|
|
||||||
)}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
function cancel() {
|
|
||||||
const btn = document.getElementById('cancel-upload');
|
|
||||||
btn.disabled = true;
|
|
||||||
btn.textContent = state.translate('uploadCancelNotification');
|
|
||||||
emit('cancel');
|
|
||||||
}
|
|
||||||
return div;
|
|
||||||
};
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
const assets = require('../../common/assets');
|
|
||||||
const fileList = require('./fileList');
|
|
||||||
const { fadeOut } = require('../utils');
|
|
||||||
|
|
||||||
module.exports = function(state, emit) {
|
|
||||||
const div = html`
|
|
||||||
<div id="page-one" class="fadeIn">
|
|
||||||
<div class="title">${state.translate('uploadPageHeader')}</div>
|
|
||||||
<div class="description">
|
|
||||||
<div>${state.translate('uploadPageExplainer')}</div>
|
|
||||||
<a href="https://testpilot.firefox.com/experiments/send" class="link">${state.translate(
|
|
||||||
'uploadPageLearnMore'
|
|
||||||
)}</a>
|
|
||||||
</div>
|
|
||||||
<div class="${state.config
|
|
||||||
.uploadWindowStyle}" ondragover=${dragover} ondragleave=${dragleave}>
|
|
||||||
<div id="upload-img"><img src="${assets.get(
|
|
||||||
'upload.svg'
|
|
||||||
)}" title="${state.translate('uploadSvgAlt')}"/></div>
|
|
||||||
<div id="upload-text">${state.translate('uploadPageDropMessage')}</div>
|
|
||||||
<span id="file-size-msg"><em>${state.translate(
|
|
||||||
'uploadPageSizeMessage'
|
|
||||||
)}</em></span>
|
|
||||||
<form method="post" action="upload" enctype="multipart/form-data">
|
|
||||||
<label for="file-upload" id="browse" class="${state.config
|
|
||||||
.uploadButtonStyle}">${state.translate(
|
|
||||||
'uploadPageBrowseButton1'
|
|
||||||
)}</label>
|
|
||||||
<input id="file-upload" type="file" name="fileUploaded" onchange=${upload} />
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
${fileList(state, emit)}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
function dragover(event) {
|
|
||||||
const div = document.querySelector('.upload-window');
|
|
||||||
div.classList.add('ondrag');
|
|
||||||
}
|
|
||||||
|
|
||||||
function dragleave(event) {
|
|
||||||
const div = document.querySelector('.upload-window');
|
|
||||||
div.classList.remove('ondrag');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function upload(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const target = event.target;
|
|
||||||
const file = target.files[0];
|
|
||||||
if (file.size === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await fadeOut('page-one');
|
|
||||||
emit('upload', { file, type: 'click' });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.layout) {
|
|
||||||
return state.layout(state, div);
|
|
||||||
}
|
|
||||||
return div;
|
|
||||||
};
|
|
||||||
115
app/utils.js
@@ -1,38 +1,18 @@
|
|||||||
function arrayToHex(iv) {
|
const b64 = require('base64-js');
|
||||||
let hexStr = '';
|
|
||||||
// eslint-disable-next-line prefer-const
|
function arrayToB64(array) {
|
||||||
for (let i in iv) {
|
return b64
|
||||||
if (iv[i] < 16) {
|
.fromByteArray(array)
|
||||||
hexStr += '0' + iv[i].toString(16);
|
.replace(/\+/g, '-')
|
||||||
} else {
|
.replace(/\//g, '_')
|
||||||
hexStr += iv[i].toString(16);
|
.replace(/=/g, '');
|
||||||
}
|
|
||||||
}
|
|
||||||
return hexStr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hexToArray(str) {
|
function b64ToArray(str) {
|
||||||
const iv = new Uint8Array(str.length / 2);
|
str = (str + '==='.slice((str.length + 3) % 4))
|
||||||
for (let i = 0; i < str.length; i += 2) {
|
.replace(/-/g, '+')
|
||||||
iv[i / 2] = parseInt(str.charAt(i) + str.charAt(i + 1), 16);
|
.replace(/_/g, '/');
|
||||||
}
|
return b64.toByteArray(str);
|
||||||
|
|
||||||
return iv;
|
|
||||||
}
|
|
||||||
|
|
||||||
function notify(str) {
|
|
||||||
return str;
|
|
||||||
/* TODO: enable once we have an opt-in ui element
|
|
||||||
if (!('Notification' in window)) {
|
|
||||||
return;
|
|
||||||
} else if (Notification.permission === 'granted') {
|
|
||||||
new Notification(str);
|
|
||||||
} else if (Notification.permission !== 'denied') {
|
|
||||||
Notification.requestPermission(function(permission) {
|
|
||||||
if (permission === 'granted') new Notification(str);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadShim(polyfill) {
|
function loadShim(polyfill) {
|
||||||
@@ -105,21 +85,44 @@ const LOCALIZE_NUMBERS = !!(
|
|||||||
|
|
||||||
const UNITS = ['B', 'kB', 'MB', 'GB'];
|
const UNITS = ['B', 'kB', 'MB', 'GB'];
|
||||||
function bytes(num) {
|
function bytes(num) {
|
||||||
|
if (num < 1) {
|
||||||
|
return '0B';
|
||||||
|
}
|
||||||
const exponent = Math.min(Math.floor(Math.log10(num) / 3), UNITS.length - 1);
|
const exponent = Math.min(Math.floor(Math.log10(num) / 3), UNITS.length - 1);
|
||||||
const n = Number(num / Math.pow(1000, exponent));
|
const n = Number(num / Math.pow(1000, exponent));
|
||||||
const nStr = LOCALIZE_NUMBERS
|
let nStr = n.toFixed(1);
|
||||||
? n.toLocaleString(navigator.languages, {
|
if (LOCALIZE_NUMBERS) {
|
||||||
|
try {
|
||||||
|
const locale = document.querySelector('html').lang;
|
||||||
|
nStr = n.toLocaleString(locale, {
|
||||||
minimumFractionDigits: 1,
|
minimumFractionDigits: 1,
|
||||||
maximumFractionDigits: 1
|
maximumFractionDigits: 1
|
||||||
})
|
});
|
||||||
: n.toFixed(1);
|
} catch (e) {
|
||||||
|
// fall through
|
||||||
|
}
|
||||||
|
}
|
||||||
return `${nStr}${UNITS[exponent]}`;
|
return `${nStr}${UNITS[exponent]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function percent(ratio) {
|
function percent(ratio) {
|
||||||
return LOCALIZE_NUMBERS
|
if (LOCALIZE_NUMBERS) {
|
||||||
? ratio.toLocaleString(navigator.languages, { style: 'percent' })
|
try {
|
||||||
: `${Math.floor(ratio * 100)}%`;
|
const locale = document.querySelector('html').lang;
|
||||||
|
return ratio.toLocaleString(locale, { style: 'percent' });
|
||||||
|
} catch (e) {
|
||||||
|
// fall through
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `${Math.floor(ratio * 100)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function number(n) {
|
||||||
|
if (LOCALIZE_NUMBERS) {
|
||||||
|
const locale = document.querySelector('html').lang;
|
||||||
|
return n.toLocaleString(locale);
|
||||||
|
}
|
||||||
|
return n.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
function allowedCopy() {
|
function allowedCopy() {
|
||||||
@@ -131,14 +134,28 @@ function delay(delay = 100) {
|
|||||||
return new Promise(resolve => setTimeout(resolve, delay));
|
return new Promise(resolve => setTimeout(resolve, delay));
|
||||||
}
|
}
|
||||||
|
|
||||||
function fadeOut(id) {
|
function fadeOut(selector) {
|
||||||
const classes = document.getElementById(id).classList;
|
const classes = document.querySelector(selector).classList;
|
||||||
classes.remove('fadeIn');
|
classes.remove('effect--fadeIn');
|
||||||
classes.add('fadeOut');
|
classes.add('effect--fadeOut');
|
||||||
return delay(300);
|
return delay(300);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ONE_DAY_IN_MS = 86400000;
|
function openLinksInNewTab(links, should = true) {
|
||||||
|
links = links || Array.from(document.querySelectorAll('a:not([target])'));
|
||||||
|
if (should) {
|
||||||
|
links.forEach(l => {
|
||||||
|
l.setAttribute('target', '_blank');
|
||||||
|
l.setAttribute('rel', 'noopener noreferrer');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
links.forEach(l => {
|
||||||
|
l.removeAttribute('target');
|
||||||
|
l.removeAttribute('rel');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return links;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
fadeOut,
|
fadeOut,
|
||||||
@@ -146,11 +163,11 @@ module.exports = {
|
|||||||
allowedCopy,
|
allowedCopy,
|
||||||
bytes,
|
bytes,
|
||||||
percent,
|
percent,
|
||||||
|
number,
|
||||||
copyToClipboard,
|
copyToClipboard,
|
||||||
arrayToHex,
|
arrayToB64,
|
||||||
hexToArray,
|
b64ToArray,
|
||||||
notify,
|
|
||||||
canHasSend,
|
canHasSend,
|
||||||
isFile,
|
isFile,
|
||||||
ONE_DAY_IN_MS
|
openLinksInNewTab
|
||||||
};
|
};
|
||||||
|
|||||||
1
assets/check-16-blue.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path fill="#0A84FF " d="M6 14a1 1 0 0 1-.707-.293l-3-3a1 1 0 0 1 1.414-1.414l2.157 2.157 6.316-9.023a1 1 0 0 1 1.639 1.146l-7 10a1 1 0 0 1-.732.427A.863.863 0 0 1 6 14z"/></svg>
|
||||||
|
After Width: | Height: | Size: 238 B |
@@ -1 +1 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path class="icon-copy" fill="#0A8DFF" d="M14.707 8.293l-3-3A1 1 0 0 0 11 5h-1V4a1 1 0 0 0-.293-.707l-3-3A1 1 0 0 0 6 0H3a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h3v3a2 2 0 0 0 2 2h5a2 2 0 0 0 2-2V9a1 1 0 0 0-.293-.707zM12.586 9H11V7.414zm-5-5H6V2.414zM6 7v2H3V2h2v2.5a.5.5 0 0 0 .5.5H8a2 2 0 0 0-2 2zm2 7V7h2v2.5a.5.5 0 0 0 .5.5H13v4z"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="#0A8DFF" d="M14.707 8.293l-3-3A1 1 0 0 0 11 5h-1V4a1 1 0 0 0-.293-.707l-3-3A1 1 0 0 0 6 0H3a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h3v3a2 2 0 0 0 2 2h5a2 2 0 0 0 2-2V9a1 1 0 0 0-.293-.707zM12.586 9H11V7.414zm-5-5H6V2.414zM6 7v2H3V2h2v2.5a.5.5 0 0 0 .5.5H8a2 2 0 0 0-2 2zm2 7V7h2v2.5a.5.5 0 0 0 .5.5H13v4z"/></svg>
|
||||||
|
Before Width: | Height: | Size: 416 B After Width: | Height: | Size: 398 B |
BIN
assets/favicon-120.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
assets/favicon-128.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
assets/favicon-144.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
assets/favicon-152.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
assets/favicon-167.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
assets/favicon-180.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
assets/favicon-195.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
assets/favicon-196.png
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
assets/favicon-228.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
assets/favicon-32.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
BIN
assets/favicon-96.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 239 KiB After Width: | Height: | Size: 16 KiB |
931
assets/main.css
@@ -1,931 +0,0 @@
|
|||||||
/*** index.html ***/
|
|
||||||
html {
|
|
||||||
background: url('./send_bg.svg');
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'segoe ui',
|
|
||||||
'helvetica neue', helvetica, ubuntu, roboto, noto, arial, sans-serif;
|
|
||||||
font-weight: 200;
|
|
||||||
background-size: 110%;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: center top;
|
|
||||||
height: 100%;
|
|
||||||
max-width: 1440px;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'segoe ui',
|
|
||||||
'helvetica neue', helvetica, ubuntu, roboto, noto, arial, sans-serif;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin: 0;
|
|
||||||
min-height: 100vh;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
#progress circle {
|
|
||||||
stroke: #eee;
|
|
||||||
stroke-width: 0.75em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#progress #bar {
|
|
||||||
transition: stroke-dashoffset 300ms linear;
|
|
||||||
stroke: #3b9dff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
align-items: flex-start;
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 31px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.send-logo {
|
|
||||||
display: flex;
|
|
||||||
position: relative;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.send-logo h1 {
|
|
||||||
transition: color 50ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.send-logo h1:hover {
|
|
||||||
color: #0297f8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.send-logo > a {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-title {
|
|
||||||
color: #3e3d40;
|
|
||||||
font-size: 32px;
|
|
||||||
font-weight: 500;
|
|
||||||
margin: 0;
|
|
||||||
position: relative;
|
|
||||||
top: -1px;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-subtitle {
|
|
||||||
color: #3e3d40;
|
|
||||||
font-size: 12px;
|
|
||||||
margin: 0 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-subtitle a {
|
|
||||||
font-weight: bold;
|
|
||||||
color: #3e3d40;
|
|
||||||
transition: color 50ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-subtitle a:hover {
|
|
||||||
color: #0297f8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feedback {
|
|
||||||
background-color: #0297f8;
|
|
||||||
background-image: url('./feedback.svg');
|
|
||||||
background-position: 2px 4px;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: 18px;
|
|
||||||
border-radius: 3px;
|
|
||||||
border: 1px solid #0297f8;
|
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
||||||
color: #fff;
|
|
||||||
cursor: pointer;
|
|
||||||
display: block;
|
|
||||||
float: right;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 12px;
|
|
||||||
opacity: 0.9;
|
|
||||||
padding: 5px;
|
|
||||||
overflow: hidden;
|
|
||||||
min-width: 12px;
|
|
||||||
max-width: 12px;
|
|
||||||
text-indent: 17px;
|
|
||||||
transition: all 250ms ease-in-out;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feedback:hover,
|
|
||||||
.feedback:focus {
|
|
||||||
min-width: 30px;
|
|
||||||
max-width: 300px;
|
|
||||||
text-indent: 2px;
|
|
||||||
padding: 5px 5px 5px 20px;
|
|
||||||
background-color: #0287e8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feedback:active {
|
|
||||||
background-color: #0277d8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.all {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
max-width: 630px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 20px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 96%;
|
|
||||||
}
|
|
||||||
|
|
||||||
input,
|
|
||||||
select,
|
|
||||||
textarea,
|
|
||||||
button {
|
|
||||||
font-family: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** page-one **/
|
|
||||||
|
|
||||||
.fadeOut {
|
|
||||||
opacity: 0;
|
|
||||||
animation: fadeout 200ms linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeout {
|
|
||||||
0% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.fadeIn {
|
|
||||||
opacity: 1;
|
|
||||||
animation: fadein 200ms linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadein {
|
|
||||||
0% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 33px;
|
|
||||||
line-height: 40px;
|
|
||||||
margin: 20px auto;
|
|
||||||
text-align: center;
|
|
||||||
max-width: 520px;
|
|
||||||
font-family: 'SF Pro Text', sans-serif;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
font-size: 15px;
|
|
||||||
line-height: 23px;
|
|
||||||
max-width: 630px;
|
|
||||||
text-align: center;
|
|
||||||
margin: 0 auto 60px;
|
|
||||||
color: #0c0c0d;
|
|
||||||
width: 92%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-window {
|
|
||||||
border: 1px dashed rgba(0, 148, 251, 0.5);
|
|
||||||
margin: 0 auto;
|
|
||||||
height: 255px;
|
|
||||||
border-radius: 4px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
text-align: center;
|
|
||||||
transition: transform 150ms;
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-window.ondrag {
|
|
||||||
border: 3px dashed rgba(0, 148, 251, 0.5);
|
|
||||||
margin: 0 auto;
|
|
||||||
height: 251px;
|
|
||||||
transform: scale(1.04);
|
|
||||||
border-radius: 4.2px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-window-b {
|
|
||||||
border: 3px dashed rgba(0, 148, 251, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-window-b.ondrag {
|
|
||||||
border: 5px dashed rgba(0, 148, 251, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.link {
|
|
||||||
color: #0094fb;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link:hover {
|
|
||||||
color: #0287e8;
|
|
||||||
}
|
|
||||||
|
|
||||||
#upload-text {
|
|
||||||
font-size: 22px;
|
|
||||||
color: #737373;
|
|
||||||
margin: 20px 0 10px;
|
|
||||||
font-family: 'SF Pro Text', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.browse {
|
|
||||||
background: #0297f8;
|
|
||||||
border-radius: 5px;
|
|
||||||
font-size: 15px;
|
|
||||||
color: #fff;
|
|
||||||
min-width: 240px;
|
|
||||||
height: 44px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.browse:hover {
|
|
||||||
background-color: #0287e8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.browse-b {
|
|
||||||
height: 60px;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="file"] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#file-size-msg {
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 16px;
|
|
||||||
color: #737373;
|
|
||||||
margin-bottom: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** file-list **/
|
|
||||||
th {
|
|
||||||
font-size: 16px;
|
|
||||||
color: #858585;
|
|
||||||
font-weight: lighter;
|
|
||||||
text-align: left;
|
|
||||||
background: rgba(0, 148, 251, 0.05);
|
|
||||||
height: 40px;
|
|
||||||
border-top: 1px solid rgba(0, 148, 251, 0.1);
|
|
||||||
padding: 0 19px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
td {
|
|
||||||
font-size: 15px;
|
|
||||||
vertical-align: top;
|
|
||||||
color: #4a4a4a;
|
|
||||||
padding: 17px 19px 0;
|
|
||||||
line-height: 23px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
font-family: 'Segoe UI', 'SF Pro Text', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody {
|
|
||||||
word-wrap: break-word;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
#uploaded-files {
|
|
||||||
margin: 45.3px auto;
|
|
||||||
table-layout: fixed;
|
|
||||||
}
|
|
||||||
|
|
||||||
#uploaded-file {
|
|
||||||
width: 35%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#copy-file-list {
|
|
||||||
width: 25%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#expiry-file-list {
|
|
||||||
width: 21%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#delete-file-list {
|
|
||||||
width: 12%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.overflow-col {
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
max-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.center-col {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-delete,
|
|
||||||
.icon-copy,
|
|
||||||
.icon-check {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-copy[disabled="disabled"] {
|
|
||||||
pointer-events: none;
|
|
||||||
opacity: 0.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-copied {
|
|
||||||
color: #0a8dff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Popup container */
|
|
||||||
.popup {
|
|
||||||
position: absolute;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* The actual popup (appears on top) */
|
|
||||||
.popup .popuptext {
|
|
||||||
visibility: hidden;
|
|
||||||
min-width: 204px;
|
|
||||||
min-height: 105px;
|
|
||||||
background-color: #fff;
|
|
||||||
color: #000;
|
|
||||||
border: 1px solid #d7d7db;
|
|
||||||
padding: 15px 24px;
|
|
||||||
box-sizing: content-box;
|
|
||||||
text-align: center;
|
|
||||||
border-radius: 5px;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1;
|
|
||||||
bottom: 20px;
|
|
||||||
left: -40px;
|
|
||||||
transition: opacity 0.5s;
|
|
||||||
opacity: 0;
|
|
||||||
outline: 0;
|
|
||||||
box-shadow: 3px 3px 7px rgba(136, 136, 136, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Popup arrow */
|
|
||||||
.popup .popuptext::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
bottom: -11px;
|
|
||||||
left: 20px;
|
|
||||||
background-color: #fff;
|
|
||||||
display: block;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
transform: rotate(45deg);
|
|
||||||
border-radius: 0 0 5px;
|
|
||||||
border-right: 1px solid #d7d7db;
|
|
||||||
border-bottom: 1px solid #d7d7db;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup .show {
|
|
||||||
visibility: visible;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-message {
|
|
||||||
height: 40px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
border-bottom: 1px #ebebeb solid;
|
|
||||||
color: #0c0c0d;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: normal;
|
|
||||||
padding-bottom: 15px;
|
|
||||||
white-space: nowrap;
|
|
||||||
width: calc(100% + 48px);
|
|
||||||
margin-left: -24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-action {
|
|
||||||
margin-top: 15px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-yes {
|
|
||||||
color: #fff;
|
|
||||||
background-color: #0297f8;
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 5px 25px;
|
|
||||||
font-weight: normal;
|
|
||||||
cursor: pointer;
|
|
||||||
min-width: 94px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
white-space: nowrap;
|
|
||||||
margin-left: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-yes:hover {
|
|
||||||
background-color: #0287e8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-no {
|
|
||||||
color: #4a4a4a;
|
|
||||||
background-color: #fbfbfb;
|
|
||||||
border: 1px #c1c1c1 solid;
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 5px 25px;
|
|
||||||
font-weight: normal;
|
|
||||||
min-width: 94px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
cursor: pointer;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-no:hover {
|
|
||||||
background-color: #efeff1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** upload-progress **/
|
|
||||||
.progress-bar {
|
|
||||||
margin-top: 3px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
text-align: center;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.percentage {
|
|
||||||
letter-spacing: -0.78px;
|
|
||||||
font-family: 'Segoe UI', 'SF Pro Text', sans-serif;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.percent-number {
|
|
||||||
font-size: 43.2px;
|
|
||||||
line-height: 58px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.percent-sign {
|
|
||||||
font-size: 28.8px;
|
|
||||||
stroke: none;
|
|
||||||
fill: #686868;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload {
|
|
||||||
margin: 0 auto;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-text {
|
|
||||||
color: rgba(0, 0, 0, 0.5);
|
|
||||||
letter-spacing: -0.4px;
|
|
||||||
margin-top: 24px;
|
|
||||||
margin-bottom: 74px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cancel-upload {
|
|
||||||
color: #d70022;
|
|
||||||
background: #fff;
|
|
||||||
font-size: 15px;
|
|
||||||
border: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cancel-upload:disabled {
|
|
||||||
text-decoration: none;
|
|
||||||
cursor: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** share-link **/
|
|
||||||
#share-window {
|
|
||||||
margin: 0 auto;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 640px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#share-window-r > div {
|
|
||||||
font-size: 12px;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#copy {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#copy-text {
|
|
||||||
align-self: flex-start;
|
|
||||||
margin-top: 60px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
color: #0c0c0d;
|
|
||||||
max-width: 614px;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
#link {
|
|
||||||
flex: 1;
|
|
||||||
height: 56px;
|
|
||||||
border: 1px solid #0297f8;
|
|
||||||
border-radius: 6px 0 0 6px;
|
|
||||||
font-size: 20px;
|
|
||||||
color: #737373;
|
|
||||||
font-family: 'SF Pro Text', sans-serif;
|
|
||||||
letter-spacing: 0;
|
|
||||||
line-height: 23px;
|
|
||||||
font-weight: 300;
|
|
||||||
padding-left: 10px;
|
|
||||||
padding-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#link:disabled {
|
|
||||||
border: 1px solid #05a700;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
#copy-btn {
|
|
||||||
flex: 0 1 165px;
|
|
||||||
background: #0297f8;
|
|
||||||
border-radius: 0 6px 6px 0;
|
|
||||||
border: 1px solid #0297f8;
|
|
||||||
color: white;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 15px;
|
|
||||||
height: 60px;
|
|
||||||
padding-left: 10px;
|
|
||||||
padding-right: 10px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
#copy-btn:hover {
|
|
||||||
background-color: #0287e8;
|
|
||||||
}
|
|
||||||
|
|
||||||
#copy-btn:disabled {
|
|
||||||
background: #05a700;
|
|
||||||
border: 1px solid #05a700;
|
|
||||||
cursor: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#delete-file {
|
|
||||||
width: 176px;
|
|
||||||
height: 44px;
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid rgba(12, 12, 13, 0.3);
|
|
||||||
border-radius: 5px;
|
|
||||||
font-size: 15px;
|
|
||||||
margin-top: 50px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
color: #313131;
|
|
||||||
}
|
|
||||||
|
|
||||||
#delete-file:hover {
|
|
||||||
background: #efeff1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.send-new {
|
|
||||||
font-size: 15px;
|
|
||||||
margin: auto;
|
|
||||||
text-align: center;
|
|
||||||
color: #0094fb;
|
|
||||||
cursor: pointer;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.send-new:hover,
|
|
||||||
.send-new:focus,
|
|
||||||
.send-new:active {
|
|
||||||
color: #0287e8;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* upload-error */
|
|
||||||
#upload-error {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#upload-error[hidden],
|
|
||||||
#unsupported-browser[hidden] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#upload-error-img {
|
|
||||||
margin: 51px 0 71px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* unsupported-browser */
|
|
||||||
#unsupported-browser {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.unsupported-description {
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 23px;
|
|
||||||
text-align: center;
|
|
||||||
color: #7d7d7d;
|
|
||||||
margin: 0 auto 23px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.firefox-logo {
|
|
||||||
width: 70px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#dl-firefox,
|
|
||||||
#update-firefox {
|
|
||||||
margin-bottom: 181px;
|
|
||||||
height: 80px;
|
|
||||||
background: #98e02b;
|
|
||||||
border-radius: 3px;
|
|
||||||
cursor: pointer;
|
|
||||||
border: 0;
|
|
||||||
box-shadow: 0 5px 3px rgb(234, 234, 234);
|
|
||||||
font-family: 'Fira Sans';
|
|
||||||
font-weight: 500;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 26px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
line-height: 1;
|
|
||||||
padding: 0 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.unsupported-button-text {
|
|
||||||
text-align: left;
|
|
||||||
margin-left: 20.4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.unsupported-button-text > span {
|
|
||||||
font-family: 'Fira Sans';
|
|
||||||
font-weight: 300;
|
|
||||||
font-size: 18px;
|
|
||||||
letter-spacing: -0.69px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** download.html **/
|
|
||||||
#download-btn {
|
|
||||||
font-size: 15px;
|
|
||||||
color: white;
|
|
||||||
width: 180px;
|
|
||||||
height: 44px;
|
|
||||||
margin-top: 20px;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
text-align: center;
|
|
||||||
background: #0297f8;
|
|
||||||
border: 1px solid #0297f8;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
#download-btn:hover {
|
|
||||||
background-color: #0287e8;
|
|
||||||
}
|
|
||||||
|
|
||||||
#download-btn:disabled {
|
|
||||||
background: #47b04b;
|
|
||||||
cursor: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#download {
|
|
||||||
margin: 0 auto;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#expired-img {
|
|
||||||
margin: 51px 0 71px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.expired-description {
|
|
||||||
font-size: 15px;
|
|
||||||
line-height: 23px;
|
|
||||||
text-align: center;
|
|
||||||
color: #7d7d7d;
|
|
||||||
margin: 0 auto 23px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#download-progress {
|
|
||||||
width: 590px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#download-progress[hidden] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#download-img {
|
|
||||||
width: 283px;
|
|
||||||
height: 196px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* footer */
|
|
||||||
.footer {
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
font-size: 13px;
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 50px 31px 41px;
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mozilla-logo {
|
|
||||||
width: 112px;
|
|
||||||
height: 32px;
|
|
||||||
margin-bottom: -5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legal-links {
|
|
||||||
max-width: 81vw;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legal-links > a {
|
|
||||||
color: #858585;
|
|
||||||
opacity: 0.9;
|
|
||||||
white-space: nowrap;
|
|
||||||
margin-right: 2vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legal-links > a:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legal-links > a:visited {
|
|
||||||
color: #858585;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-links {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
width: 94px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-links > a {
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-links > a:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.github,
|
|
||||||
.twitter {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
margin-bottom: -5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-device-width: 992px), (max-width: 992px) {
|
|
||||||
.popup .popuptext {
|
|
||||||
left: auto;
|
|
||||||
right: -40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup .popuptext::after {
|
|
||||||
left: auto;
|
|
||||||
right: 36px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-device-width: 768px), (max-width: 768px) {
|
|
||||||
.description {
|
|
||||||
margin: 0 auto 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#copy {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#link {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: flex-start;
|
|
||||||
max-width: 630px;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mozilla-logo {
|
|
||||||
margin-left: -7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legal-links {
|
|
||||||
flex-direction: column;
|
|
||||||
margin: auto;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legal-links > * {
|
|
||||||
display: block;
|
|
||||||
padding: 10px 0;
|
|
||||||
align-self: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-links {
|
|
||||||
margin-top: 20px;
|
|
||||||
align-self: flex-start;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-device-width: 520px), (max-width: 520px) {
|
|
||||||
.header {
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feedback {
|
|
||||||
margin-top: 10px;
|
|
||||||
min-width: 30px;
|
|
||||||
max-width: 300px;
|
|
||||||
text-indent: 2px;
|
|
||||||
padding: 5px 5px 5px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#copy {
|
|
||||||
width: 100%;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
#link {
|
|
||||||
font-size: 22px;
|
|
||||||
padding: 15px 10px;
|
|
||||||
border-radius: 6px 6px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#copy-btn {
|
|
||||||
border-radius: 0 0 6px 6px;
|
|
||||||
flex: 0 1 65px;
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
font-size: 14px;
|
|
||||||
padding: 0 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
td {
|
|
||||||
font-size: 13px;
|
|
||||||
padding: 17px 5px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +1 @@
|
|||||||
<svg width="30" height="27" viewBox="0 0 30 27" xmlns="http://www.w3.org/2000/svg"><title>send logo</title><g stroke="#3E3D40" fill="none" fill-rule="evenodd"><path d="M22.364 19.989l-2.153-2.103a2.046 2.046 0 0 0-2.665-.151l3.402 3.323a.531.531 0 0 1 0 .766l-2.466 2.408a.563.563 0 0 1-.784 0l-3.398-3.32a1.932 1.932 0 0 0 .188 2.564l2.153 2.103c.788.77 2.066.77 2.855 0l2.868-2.802a1.94 1.94 0 0 0 0-2.788M8.77 14.745a.534.534 0 0 0 0 .766l3.399 3.32a2.05 2.05 0 0 1-2.625-.184l-2.153-2.102a1.94 1.94 0 0 1 0-2.79l2.869-2.801a2.052 2.052 0 0 1 2.854 0l2.153 2.103c.73.713.775 1.83.154 2.603l-3.401-3.323a.565.565 0 0 0-.784 0L8.77 14.745zm9.464 5.682a.777.777 0 0 1 0 1.118.822.822 0 0 1-1.144 0l-5.6-5.47a.777.777 0 0 1 0-1.118.822.822 0 0 1 1.144 0l5.6 5.47z" stroke-width=".618" fill="#3E3D40"/><path d="M6.065 20.606c-2.913-1.586-3.988-3.656-3.988-6.468 0-2.81 2.265-6.425 5.786-6.289.1.004.55-.006.649 0 .895-3.27 2.508-6.353 6.898-6.353 4.557 0 7.336 3.716 6.75 7.785.08-.005 1.232.17 1.31.186 3.096.644 4.915 3.275 4.915 5.18 0 1.905-.107 3.029-2.023 4.947" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></g></svg>
|
<svg width="30" height="27" viewBox="0 0 30 27" xmlns="http://www.w3.org/2000/svg"><title>send logo</title><g stroke="#3E3D40" fill="none" fill-rule="evenodd" transform="translate(-0.231,0.11948695)"><path d="M22.364 19.989l-2.153-2.103a2.046 2.046 0 0 0-2.665-.151l3.402 3.323a.531.531 0 0 1 0 .766l-2.466 2.408a.563.563 0 0 1-.784 0l-3.398-3.32a1.932 1.932 0 0 0 .188 2.564l2.153 2.103c.788.77 2.066.77 2.855 0l2.868-2.802a1.94 1.94 0 0 0 0-2.788M8.77 14.745a.534.534 0 0 0 0 .766l3.399 3.32a2.05 2.05 0 0 1-2.625-.184l-2.153-2.102a1.94 1.94 0 0 1 0-2.79l2.869-2.801a2.052 2.052 0 0 1 2.854 0l2.153 2.103c.73.713.775 1.83.154 2.603l-3.401-3.323a.565.565 0 0 0-.784 0L8.77 14.745zm9.464 5.682a.777.777 0 0 1 0 1.118.822.822 0 0 1-1.144 0l-5.6-5.47a.777.777 0 0 1 0-1.118.822.822 0 0 1 1.144 0l5.6 5.47z" stroke-width=".618" fill="#3E3D40"/><path d="M6.065 20.606c-2.913-1.586-3.988-3.656-3.988-6.468 0-2.81 2.265-6.425 5.786-6.289.1.004.55-.006.649 0 .895-3.27 2.508-6.353 6.898-6.353 4.557 0 7.336 3.716 6.75 7.785.08-.005 1.232.17 1.31.186 3.096.644 4.915 3.275 4.915 5.18 0 1.905-.107 3.029-2.023 4.947" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></g></svg>
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.2 KiB |
17
assets/spinner.svg
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<!-- By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL -->
|
||||||
|
<svg width="38" height="38" viewBox="0 0 38 38" xmlns="http://www.w3.org/2000/svg" stroke="#fff">
|
||||||
|
<g fill="none" fill-rule="evenodd">
|
||||||
|
<g transform="translate(1 1)" stroke-width="2">
|
||||||
|
<circle stroke-opacity=".5" cx="18" cy="18" r="18"/>
|
||||||
|
<path d="M36 18c0-9.94-8.06-18-18-18">
|
||||||
|
<animateTransform
|
||||||
|
attributeName="transform"
|
||||||
|
type="rotate"
|
||||||
|
from="0 18 18"
|
||||||
|
to="360 18 18"
|
||||||
|
dur="1s"
|
||||||
|
repeatCount="indefinite"/>
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 694 B |
10
browserconfig.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<browserconfig>
|
||||||
|
<msapplication>
|
||||||
|
<tile>
|
||||||
|
<square70x70logo src=“favicon-76.png”/>
|
||||||
|
<square150x150logo src="favicon-228.png"/>
|
||||||
|
<TileColor>#0297F8</TileColor>
|
||||||
|
</tile>
|
||||||
|
</msapplication>
|
||||||
|
</browserconfig>
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
last 2 chrome versions
|
last 2 chrome versions
|
||||||
last 2 firefox versions
|
last 2 firefox versions
|
||||||
firefox esr
|
firefox esr
|
||||||
ie >= 9
|
safari > 9
|
||||||
safari >= 9
|
|
||||||
|
|||||||
@@ -1,32 +1,55 @@
|
|||||||
const { MessageContext } = require('fluent');
|
// TODO: when node supports 'for await' we can remove babel-polyfill
|
||||||
|
// and use 'fluent' instead of 'fluent/compat' (also below near line 42)
|
||||||
|
require('babel-polyfill');
|
||||||
|
const { MessageContext } = require('fluent/compat');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
function toJSON(map) {
|
function toJSON(map) {
|
||||||
return JSON.stringify(Array.from(map));
|
return JSON.stringify(Array.from(map));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function merge(m1, m2) {
|
||||||
|
const result = new Map(m1);
|
||||||
|
for (const [k, v] of m2) {
|
||||||
|
result.set(k, v);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = function(source) {
|
module.exports = function(source) {
|
||||||
const localeExp = this.options.locale || /([^/]+)\/[^/]+\.ftl$/;
|
const localeExp = this.options.locale || /([^/]+)\/[^/]+\.ftl$/;
|
||||||
const result = localeExp.exec(this.resourcePath);
|
const result = localeExp.exec(this.resourcePath);
|
||||||
const locale = result && result[1];
|
const locale = result && result[1];
|
||||||
// pre-parse the ftl
|
|
||||||
const context = new MessageContext(locale);
|
|
||||||
context.addMessages(source);
|
|
||||||
if (!locale) {
|
if (!locale) {
|
||||||
throw new Error(`couldn't find locale in: ${this.resourcePath}`);
|
throw new Error(`couldn't find locale in: ${this.resourcePath}`);
|
||||||
}
|
}
|
||||||
|
// load default language and "merge" contexts
|
||||||
|
// TODO: make this configurable
|
||||||
|
const en_ftl = fs.readFileSync(
|
||||||
|
require.resolve('../public/locales/en-US/send.ftl'),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
const en = new MessageContext('en-US');
|
||||||
|
en.addMessages(en_ftl);
|
||||||
|
// pre-parse the ftl
|
||||||
|
const context = new MessageContext(locale);
|
||||||
|
context.addMessages(source);
|
||||||
|
|
||||||
|
const merged = merge(en._messages, context._messages);
|
||||||
return `
|
return `
|
||||||
module.exports = \`
|
module.exports = \`
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
var fluent = require('fluent');
|
require('babel-polyfill');
|
||||||
|
var fluent = require('fluent/compat');
|
||||||
}
|
}
|
||||||
var ctx = new fluent.MessageContext('${locale}', {useIsolating: false});
|
var fluentContext = new fluent.MessageContext('${locale}', {useIsolating: false});
|
||||||
ctx._messages = new Map(${toJSON(context._messages)});
|
fluentContext._messages = new Map(${toJSON(merged)});
|
||||||
function translate(id, data) {
|
function translate(id, data) {
|
||||||
var msg = ctx.getMessage(id);
|
var msg = fluentContext.getMessage(id);
|
||||||
if (typeof(msg) !== 'string' && !msg.val && msg.attrs) {
|
if (typeof(msg) !== 'string' && !msg.val && msg.attrs) {
|
||||||
msg = msg.attrs.title || msg.attrs.alt
|
msg = msg.attrs.title || msg.attrs.alt
|
||||||
}
|
}
|
||||||
return ctx.format(msg, data);
|
return fluentContext.format(msg, data);
|
||||||
}
|
}
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
module.exports = translate;
|
module.exports = translate;
|
||||||
|
|||||||
5
build/version_loader.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
const version = require('../package.json').version;
|
||||||
|
|
||||||
|
module.exports = function(source) {
|
||||||
|
return source.replace('VERSION', version);
|
||||||
|
};
|
||||||
134
circle.yml
@@ -1,35 +1,99 @@
|
|||||||
machine:
|
version: 2.0
|
||||||
node:
|
jobs:
|
||||||
version: 8
|
build:
|
||||||
services:
|
docker:
|
||||||
- docker
|
- image: circleci/node:8
|
||||||
- redis
|
steps:
|
||||||
environment:
|
- checkout
|
||||||
PATH: "/home/ubuntu/send/firefox:$PATH"
|
- restore_cache:
|
||||||
|
key: send-{{ checksum "package-lock.json" }}
|
||||||
dependencies:
|
- run: npm install
|
||||||
pre:
|
- save_cache:
|
||||||
- npm i -g get-firefox geckodriver nsp
|
key: send-{{ checksum "package-lock.json" }}
|
||||||
- get-firefox --platform linux --extract --target /home/ubuntu/send
|
paths:
|
||||||
|
- node_modules
|
||||||
deployment:
|
- run: npm run build
|
||||||
latest:
|
- persist_to_workspace:
|
||||||
branch: master
|
root: .
|
||||||
commands:
|
paths:
|
||||||
- docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
|
- ./*
|
||||||
- docker build -t mozilla/send:latest .
|
test:
|
||||||
- docker push mozilla/send:latest
|
docker:
|
||||||
tags:
|
- image: circleci/node:8-browsers
|
||||||
tag: /.*/
|
steps:
|
||||||
owner: mozilla
|
- checkout
|
||||||
commands:
|
- restore_cache:
|
||||||
- docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
|
key: send-{{ checksum "package-lock.json" }}
|
||||||
- docker build -t mozilla/send:$CIRCLE_TAG .
|
- run: npm install
|
||||||
- docker push mozilla/send:$CIRCLE_TAG
|
- save_cache:
|
||||||
|
key: send-{{ checksum "package-lock.json" }}
|
||||||
test:
|
paths:
|
||||||
override:
|
- node_modules
|
||||||
- npm run build
|
- run: npm run check
|
||||||
- npm run lint
|
- run: npm run lint
|
||||||
- npm test
|
- run: npm test
|
||||||
- nsp check
|
- store_artifacts:
|
||||||
|
path: coverage
|
||||||
|
deploy_dev:
|
||||||
|
machine: true
|
||||||
|
steps:
|
||||||
|
- attach_workspace:
|
||||||
|
at: .
|
||||||
|
- run: docker login -u $DOCKER_USER -p $DOCKER_PASS
|
||||||
|
- run: docker build -t mozilla/send:latest .
|
||||||
|
- run: docker push mozilla/send:latest
|
||||||
|
deploy_stage:
|
||||||
|
machine: true
|
||||||
|
steps:
|
||||||
|
- attach_workspace:
|
||||||
|
at: .
|
||||||
|
- run: docker login -u $DOCKER_USER -p $DOCKER_PASS
|
||||||
|
- run: docker build -t mozilla/send:$CIRCLE_TAG .
|
||||||
|
- run: docker push mozilla/send:$CIRCLE_TAG
|
||||||
|
workflows:
|
||||||
|
version: 2
|
||||||
|
test_pr:
|
||||||
|
jobs:
|
||||||
|
- test:
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
ignore: master
|
||||||
|
build_and_deploy_dev:
|
||||||
|
jobs:
|
||||||
|
- build:
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only: master
|
||||||
|
tags:
|
||||||
|
ignore: /^v.*/
|
||||||
|
- deploy_dev:
|
||||||
|
requires:
|
||||||
|
- build
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only: master
|
||||||
|
tags:
|
||||||
|
ignore: /^v.*/
|
||||||
|
build_and_deploy_stage:
|
||||||
|
jobs:
|
||||||
|
- build:
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
ignore: /.*/
|
||||||
|
tags:
|
||||||
|
only: /^v.*/
|
||||||
|
- test:
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
ignore: /.*/
|
||||||
|
tags:
|
||||||
|
only: /^v.*/
|
||||||
|
- deploy_stage:
|
||||||
|
requires:
|
||||||
|
- build
|
||||||
|
- test
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
ignore: /.*/
|
||||||
|
tags:
|
||||||
|
only: /^v.*/
|
||||||
@@ -8,6 +8,5 @@ services:
|
|||||||
- "1443:1443"
|
- "1443:1443"
|
||||||
environment:
|
environment:
|
||||||
- REDIS_HOST=redis
|
- REDIS_HOST=redis
|
||||||
- NODE_ENV=production
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:alpine
|
image: redis:alpine
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ Data will be collected with Google Analytics and follow [Test Pilot standards](h
|
|||||||
- `cm5` - the number of files the user has ever uploaded.
|
- `cm5` - the number of files the user has ever uploaded.
|
||||||
- `cm6` - the number of unexpired files the user has uploaded.
|
- `cm6` - the number of unexpired files the user has uploaded.
|
||||||
- `cm7` - the number of files the user has ever downloaded.
|
- `cm7` - the number of files the user has ever downloaded.
|
||||||
|
- `cm8` - the number of downloads permitted by the uploader.
|
||||||
|
|
||||||
### Custom Dimensions
|
### Custom Dimensions
|
||||||
- `cd1` - the method by which the user initiated an upload. One of `drag`, `click`.
|
- `cd1` - the method by which the user initiated an upload. One of `drag`, `click`.
|
||||||
@@ -67,6 +68,25 @@ Triggered whenever a user stops uploading a file. Includes:
|
|||||||
- `cd2`
|
- `cd2`
|
||||||
- `cd6`
|
- `cd6`
|
||||||
|
|
||||||
|
#### `download-limit-changed`
|
||||||
|
Triggered whenever the sender changes the download limit. Includes:
|
||||||
|
|
||||||
|
- `ec` - `sender`
|
||||||
|
- `ea` - `download-limit-changed`
|
||||||
|
- `cm1`
|
||||||
|
- `cm5`
|
||||||
|
- `cm6`
|
||||||
|
- `cm7`
|
||||||
|
- `cm8`
|
||||||
|
|
||||||
|
#### `password-added`
|
||||||
|
Triggered whenever a password is added to a file. Includes:
|
||||||
|
|
||||||
|
- `cm1`
|
||||||
|
- `cm5`
|
||||||
|
- `cm6`
|
||||||
|
- `cm7`
|
||||||
|
|
||||||
#### `download-started`
|
#### `download-started`
|
||||||
Triggered whenever a user begins downloading a file. Includes:
|
Triggered whenever a user begins downloading a file. Includes:
|
||||||
|
|
||||||
|
|||||||