mirror of
https://gitlab.com/timvisee/send.git
synced 2025-12-08 23:18:39 +03:00
Compare commits
440 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 |
@@ -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
4
.gitignore
vendored
@@ -1,2 +1,6 @@
|
|||||||
node_modules
|
node_modules
|
||||||
|
coverage
|
||||||
dist
|
dist
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
.nyc_output
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
dist
|
dist
|
||||||
assets/*.js
|
assets/*.js
|
||||||
|
coverage
|
||||||
82
CHANGELOG.md
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
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ć)
|
||||||
|
صفا الفليج
|
||||||
|
|||||||
@@ -69,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
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
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,20 +1,33 @@
|
|||||||
import hash from 'string-hash';
|
import hash from 'string-hash';
|
||||||
|
|
||||||
const experiments = {
|
const experiments = {
|
||||||
'SyI-hI7gT9agiH-f3f0BYg': {
|
S9wqVl2SQ4ab2yZtqDI3Dw: {
|
||||||
id: 'SyI-hI7gT9agiH-f3f0BYg',
|
id: 'S9wqVl2SQ4ab2yZtqDI3Dw',
|
||||||
run: function(variant, state, emitter) {
|
run: function(variant, state, emitter) {
|
||||||
state.promo = variant === 1 ? 'body' : 'header';
|
switch (variant) {
|
||||||
|
case 1:
|
||||||
|
state.promo = 'blue';
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
state.promo = 'pink';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
state.promo = 'grey';
|
||||||
|
}
|
||||||
emitter.emit('render');
|
emitter.emit('render');
|
||||||
},
|
},
|
||||||
eligible: function() {
|
eligible: function() {
|
||||||
return (
|
return (
|
||||||
!/firefox/i.test(navigator.userAgent) &&
|
!/firefox|fxios/i.test(navigator.userAgent) &&
|
||||||
document.querySelector('html').lang === 'en-US'
|
document.querySelector('html').lang === 'en-US'
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
variant: function(state) {
|
variant: function(state) {
|
||||||
return this.luckyNumber(state) > 0.5 ? 1 : 0;
|
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(
|
||||||
|
|||||||
@@ -1,57 +1,14 @@
|
|||||||
/* global EXPIRE_SECONDS */
|
|
||||||
import FileSender from './fileSender';
|
import FileSender from './fileSender';
|
||||||
import FileReceiver from './fileReceiver';
|
import FileReceiver from './fileReceiver';
|
||||||
import { copyToClipboard, delay, fadeOut, percent } 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;
|
let updateTitle = false;
|
||||||
@@ -61,13 +18,17 @@ export default function(state, emitter) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
@@ -97,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({
|
||||||
@@ -108,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', () => {
|
||||||
@@ -125,36 +91,28 @@ export default function(state, emitter) {
|
|||||||
sender.on('progress', updateProgress);
|
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);
|
state.storage.totalUploads += 1;
|
||||||
metrics.completedUpload({ size, time, speed, type });
|
metrics.completedUpload(ownedFile);
|
||||||
|
|
||||||
|
state.storage.addFile(ownedFile);
|
||||||
|
|
||||||
document.getElementById('cancel-upload').hidden = 'hidden';
|
document.getElementById('cancel-upload').hidden = 'hidden';
|
||||||
await delay(1000);
|
await delay(1000);
|
||||||
await fadeOut('upload-progress');
|
await fadeOut('.page');
|
||||||
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);
|
openLinksInNewTab(links, false);
|
||||||
state.transfer = null;
|
emitter.emit('pushState', `/share/${ownedFile.id}`);
|
||||||
state.storage.totalUploads += 1;
|
|
||||||
emitter.emit('pushState', `/share/${info.id}`);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
state.transfer = null;
|
|
||||||
if (err.message === '0') {
|
if (err.message === '0') {
|
||||||
//cancelled. do nothing
|
//cancelled. do nothing
|
||||||
metrics.cancelledUpload({ size, type });
|
metrics.cancelledUpload({ size, type });
|
||||||
@@ -163,34 +121,39 @@ 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('pushState', '/error');
|
emitter.emit('pushState', '/error');
|
||||||
|
} finally {
|
||||||
|
state.uploading = false;
|
||||||
|
state.transfer = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on('password', async ({ password, file }) => {
|
emitter.on('password', async ({ password, file }) => {
|
||||||
try {
|
try {
|
||||||
await FileSender.setPassword(password, file);
|
state.settingPassword = true;
|
||||||
|
render();
|
||||||
|
await file.setPassword(password);
|
||||||
|
state.storage.writeFile(file);
|
||||||
metrics.addedPassword({ size: file.size });
|
metrics.addedPassword({ size: file.size });
|
||||||
file.password = password;
|
await delay(1000);
|
||||||
state.storage.writeFiles();
|
} catch (err) {
|
||||||
} catch (e) {
|
console.error(err);
|
||||||
console.error(e);
|
state.passwordSetError = err;
|
||||||
|
} finally {
|
||||||
|
state.settingPassword = false;
|
||||||
}
|
}
|
||||||
render();
|
render();
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on('preview', async () => {
|
emitter.on('getMetadata', async () => {
|
||||||
const file = state.fileInfo;
|
const file = state.fileInfo;
|
||||||
const url = `/api/download/${file.id}`;
|
const receiver = new FileReceiver(file);
|
||||||
const receiver = new FileReceiver(url, file);
|
|
||||||
receiver.on('progress', updateProgress);
|
|
||||||
receiver.on('decrypting', render);
|
|
||||||
state.transfer = receiver;
|
|
||||||
try {
|
try {
|
||||||
await receiver.getMetadata(file.nonce);
|
await receiver.getMetadata();
|
||||||
|
state.transfer = receiver;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message === '401') {
|
if (e.message === '401') {
|
||||||
file.password = null;
|
file.password = null;
|
||||||
if (!file.pwd) {
|
if (!file.requiresPassword) {
|
||||||
return emitter.emit('pushState', '/404');
|
return emitter.emit('pushState', '/404');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -199,34 +162,39 @@ export default function(state, emitter) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
emitter.on('download', async file => {
|
emitter.on('download', async file => {
|
||||||
state.transfer.on('progress', render);
|
state.transfer.on('progress', updateProgress);
|
||||||
state.transfer.on('decrypting', render);
|
state.transfer.on('decrypting', render);
|
||||||
const links = openLinksInNewTab();
|
const links = openLinksInNewTab();
|
||||||
const size = file.size;
|
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 state.transfer.download(file.nonce);
|
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 = null;
|
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) {
|
||||||
|
if (err.message === '0') {
|
||||||
|
// download cancelled
|
||||||
|
state.transfer.reset();
|
||||||
|
return render();
|
||||||
|
}
|
||||||
console.error(err);
|
console.error(err);
|
||||||
// TODO cancelled download
|
state.transfer = null;
|
||||||
const location = err.message === 'notfound' ? '/404' : '/error';
|
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('pushState', location);
|
emitter.emit('pushState', location);
|
||||||
} finally {
|
} finally {
|
||||||
state.transfer = null;
|
|
||||||
openLinksInNewTab(links, false);
|
openLinksInNewTab(links, false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -236,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,111 +1,27 @@
|
|||||||
import Nanobus from 'nanobus';
|
import Nanobus from 'nanobus';
|
||||||
import { arrayToB64, b64ToArray, 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, file) {
|
constructor(fileInfo) {
|
||||||
super('FileReceiver');
|
super('FileReceiver');
|
||||||
this.secretKeyPromise = window.crypto.subtle.importKey(
|
this.keychain = new Keychain(fileInfo.secretKey, fileInfo.nonce);
|
||||||
'raw',
|
if (fileInfo.requiresPassword) {
|
||||||
b64ToArray(file.key),
|
this.keychain.setPassword(fileInfo.password, fileInfo.url);
|
||||||
'HKDF',
|
|
||||||
false,
|
|
||||||
['deriveKey']
|
|
||||||
);
|
|
||||||
this.encryptKeyPromise = this.secretKeyPromise.then(sk => {
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
return window.crypto.subtle.deriveKey(
|
|
||||||
{
|
|
||||||
name: 'HKDF',
|
|
||||||
salt: new Uint8Array(),
|
|
||||||
info: encoder.encode('encryption'),
|
|
||||||
hash: 'SHA-256'
|
|
||||||
},
|
|
||||||
sk,
|
|
||||||
{
|
|
||||||
name: 'AES-GCM',
|
|
||||||
length: 128
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
['decrypt']
|
|
||||||
);
|
|
||||||
});
|
|
||||||
if (file.pwd) {
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
console.log(file.password + file.url);
|
|
||||||
this.authKeyPromise = window.crypto.subtle
|
|
||||||
.importKey(
|
|
||||||
'raw',
|
|
||||||
encoder.encode(file.password),
|
|
||||||
{ name: 'PBKDF2' },
|
|
||||||
false,
|
|
||||||
['deriveKey']
|
|
||||||
)
|
|
||||||
.then(pwdKey =>
|
|
||||||
window.crypto.subtle.deriveKey(
|
|
||||||
{
|
|
||||||
name: 'PBKDF2',
|
|
||||||
salt: encoder.encode(file.url),
|
|
||||||
iterations: 100,
|
|
||||||
hash: 'SHA-256'
|
|
||||||
},
|
|
||||||
pwdKey,
|
|
||||||
{
|
|
||||||
name: 'HMAC',
|
|
||||||
hash: 'SHA-256'
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
['sign']
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.authKeyPromise = this.secretKeyPromise.then(sk => {
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
return window.crypto.subtle.deriveKey(
|
|
||||||
{
|
|
||||||
name: 'HKDF',
|
|
||||||
salt: new Uint8Array(),
|
|
||||||
info: encoder.encode('authentication'),
|
|
||||||
hash: 'SHA-256'
|
|
||||||
},
|
|
||||||
sk,
|
|
||||||
{
|
|
||||||
name: 'HMAC',
|
|
||||||
hash: { name: 'SHA-256' }
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
['sign']
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
this.metaKeyPromise = this.secretKeyPromise.then(sk => {
|
this.fileInfo = fileInfo;
|
||||||
const encoder = new TextEncoder();
|
this.reset();
|
||||||
return window.crypto.subtle.deriveKey(
|
|
||||||
{
|
|
||||||
name: 'HKDF',
|
|
||||||
salt: new Uint8Array(),
|
|
||||||
info: encoder.encode('metadata'),
|
|
||||||
hash: 'SHA-256'
|
|
||||||
},
|
|
||||||
sk,
|
|
||||||
{
|
|
||||||
name: 'AES-GCM',
|
|
||||||
length: 128
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
['decrypt']
|
|
||||||
);
|
|
||||||
});
|
|
||||||
this.file = file;
|
|
||||||
this.url = url;
|
|
||||||
this.msg = 'fileSizeProgress';
|
|
||||||
this.state = 'initialized';
|
|
||||||
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]),
|
||||||
@@ -114,139 +30,95 @@ export default class FileReceiver extends Nanobus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cancel() {
|
cancel() {
|
||||||
// TODO
|
if (this.downloadRequest) {
|
||||||
|
this.downloadRequest.cancel();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchMetadata(sig) {
|
reset() {
|
||||||
return new Promise((resolve, reject) => {
|
this.msg = 'fileSizeProgress';
|
||||||
const xhr = new XMLHttpRequest();
|
this.state = 'initialized';
|
||||||
xhr.onreadystatechange = () => {
|
this.progress = [0, 1];
|
||||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
|
||||||
const nonce = xhr.getResponseHeader('WWW-Authenticate').split(' ')[1];
|
|
||||||
this.file.nonce = nonce;
|
|
||||||
if (xhr.status === 200) {
|
|
||||||
return resolve(xhr.response);
|
|
||||||
}
|
|
||||||
reject(new Error(xhr.status));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
xhr.onerror = () => reject(new Error(0));
|
|
||||||
xhr.ontimeout = () => reject(new Error(0));
|
|
||||||
xhr.open('get', `/api/metadata/${this.file.id}`);
|
|
||||||
xhr.setRequestHeader('Authorization', `send-v1 ${arrayToB64(sig)}`);
|
|
||||||
xhr.responseType = 'json';
|
|
||||||
xhr.timeout = 2000;
|
|
||||||
xhr.send();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMetadata(nonce) {
|
async getMetadata() {
|
||||||
try {
|
const meta = await metadata(this.fileInfo.id, this.keychain);
|
||||||
const authKey = await this.authKeyPromise;
|
this.keychain.setIV(meta.iv);
|
||||||
const sig = await window.crypto.subtle.sign(
|
this.fileInfo.name = meta.name;
|
||||||
{
|
this.fileInfo.type = meta.type;
|
||||||
name: 'HMAC'
|
this.fileInfo.iv = meta.iv;
|
||||||
},
|
this.fileInfo.size = meta.size;
|
||||||
authKey,
|
|
||||||
b64ToArray(nonce)
|
|
||||||
);
|
|
||||||
const data = await this.fetchMetadata(new Uint8Array(sig));
|
|
||||||
const metaKey = await this.metaKeyPromise;
|
|
||||||
const json = await window.crypto.subtle.decrypt(
|
|
||||||
{
|
|
||||||
name: 'AES-GCM',
|
|
||||||
iv: new Uint8Array(12),
|
|
||||||
tagLength: 128
|
|
||||||
},
|
|
||||||
metaKey,
|
|
||||||
b64ToArray(data.metadata)
|
|
||||||
);
|
|
||||||
const decoder = new TextDecoder();
|
|
||||||
const meta = JSON.parse(decoder.decode(json));
|
|
||||||
this.file.name = meta.name;
|
|
||||||
this.file.type = meta.type;
|
|
||||||
this.file.iv = meta.iv;
|
|
||||||
this.file.size = data.size;
|
|
||||||
this.file.ttl = data.ttl;
|
|
||||||
this.state = 'ready';
|
this.state = 'ready';
|
||||||
} catch (e) {
|
|
||||||
this.state = 'invalid';
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadFile(sig) {
|
async download(noSave = false) {
|
||||||
return new Promise((resolve, reject) => {
|
this.state = 'downloading';
|
||||||
const xhr = new XMLHttpRequest();
|
this.downloadRequest = await downloadFile(
|
||||||
|
this.fileInfo.id,
|
||||||
xhr.onprogress = event => {
|
this.keychain,
|
||||||
if (event.lengthComputable && event.target.status !== 404) {
|
p => {
|
||||||
this.progress = [event.loaded, event.total];
|
this.progress = p;
|
||||||
this.emit('progress', this.progress);
|
this.emit('progress');
|
||||||
}
|
}
|
||||||
};
|
);
|
||||||
|
try {
|
||||||
xhr.onload = event => {
|
const ciphertext = await this.downloadRequest.result;
|
||||||
if (xhr.status === 404) {
|
this.downloadRequest = null;
|
||||||
reject(new Error('notfound'));
|
this.msg = 'decryptingFile';
|
||||||
return;
|
this.state = 'decrypting';
|
||||||
}
|
this.emit('decrypting');
|
||||||
|
const plaintext = await this.keychain.decryptFile(ciphertext);
|
||||||
if (xhr.status !== 200) {
|
if (!noSave) {
|
||||||
return reject(new Error(xhr.status));
|
await saveFile({
|
||||||
}
|
plaintext,
|
||||||
|
name: decodeURIComponent(this.fileInfo.name),
|
||||||
const blob = new Blob([xhr.response]);
|
type: this.fileInfo.type
|
||||||
const fileReader = new FileReader();
|
|
||||||
fileReader.onload = function() {
|
|
||||||
resolve(this.result);
|
|
||||||
};
|
|
||||||
|
|
||||||
fileReader.readAsArrayBuffer(blob);
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.open('get', this.url);
|
|
||||||
xhr.setRequestHeader('Authorization', `send-v1 ${arrayToB64(sig)}`);
|
|
||||||
xhr.responseType = 'blob';
|
|
||||||
xhr.send();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async download(nonce) {
|
|
||||||
this.state = 'downloading';
|
|
||||||
this.emit('progress', this.progress);
|
|
||||||
try {
|
|
||||||
const encryptKey = await this.encryptKeyPromise;
|
|
||||||
const authKey = await this.authKeyPromise;
|
|
||||||
const sig = await window.crypto.subtle.sign(
|
|
||||||
{
|
|
||||||
name: 'HMAC'
|
|
||||||
},
|
|
||||||
authKey,
|
|
||||||
b64ToArray(nonce)
|
|
||||||
);
|
|
||||||
const ciphertext = await this.downloadFile(new Uint8Array(sig));
|
|
||||||
this.msg = 'decryptingFile';
|
|
||||||
this.emit('decrypting');
|
|
||||||
const plaintext = await window.crypto.subtle.decrypt(
|
|
||||||
{
|
|
||||||
name: 'AES-GCM',
|
|
||||||
iv: b64ToArray(this.file.iv),
|
|
||||||
tagLength: 128
|
|
||||||
},
|
|
||||||
encryptKey,
|
|
||||||
ciphertext
|
|
||||||
);
|
|
||||||
this.msg = 'downloadFinish';
|
this.msg = 'downloadFinish';
|
||||||
this.state = 'complete';
|
this.state = 'complete';
|
||||||
return {
|
|
||||||
plaintext,
|
|
||||||
name: decodeURIComponent(this.file.name),
|
|
||||||
type: this.file.type
|
|
||||||
};
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.state = 'invalid';
|
this.downloadRequest = null;
|
||||||
throw e;
|
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 { arrayToB64, b64ToArray, 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.rawSecret = window.crypto.getRandomValues(new Uint8Array(16));
|
|
||||||
this.secretKey = window.crypto.subtle.importKey(
|
|
||||||
'raw',
|
|
||||||
this.rawSecret,
|
|
||||||
'HKDF',
|
|
||||||
false,
|
|
||||||
['deriveKey']
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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,221 +57,56 @@ export default class FileSender extends Nanobus {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadFile(encrypted, metadata, rawAuth) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const dataView = new DataView(encrypted);
|
|
||||||
const blob = new Blob([dataView], { type: 'application/octet-stream' });
|
|
||||||
const fd = new FormData();
|
|
||||||
fd.append('data', blob);
|
|
||||||
|
|
||||||
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) {
|
|
||||||
const nonce = xhr
|
|
||||||
.getResponseHeader('WWW-Authenticate')
|
|
||||||
.split(' ')[1];
|
|
||||||
this.progress = [1, 1];
|
|
||||||
this.msg = 'notifyUploadDone';
|
|
||||||
const responseObj = JSON.parse(xhr.responseText);
|
|
||||||
return resolve({
|
|
||||||
url: responseObj.url,
|
|
||||||
id: responseObj.id,
|
|
||||||
secretKey: arrayToB64(this.rawSecret),
|
|
||||||
deleteToken: responseObj.delete,
|
|
||||||
nonce
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.msg = 'errorPageHeader';
|
|
||||||
reject(new Error(xhr.status));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.open('post', '/api/upload', true);
|
|
||||||
xhr.setRequestHeader(
|
|
||||||
'X-File-Metadata',
|
|
||||||
arrayToB64(new Uint8Array(metadata))
|
|
||||||
);
|
|
||||||
xhr.setRequestHeader('Authorization', `send-v1 ${arrayToB64(rawAuth)}`);
|
|
||||||
xhr.send(fd);
|
|
||||||
this.msg = 'fileSizeProgress';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async upload() {
|
async upload() {
|
||||||
const encoder = new TextEncoder();
|
const start = Date.now();
|
||||||
const secretKey = await this.secretKey;
|
|
||||||
const encryptKey = await window.crypto.subtle.deriveKey(
|
|
||||||
{
|
|
||||||
name: 'HKDF',
|
|
||||||
salt: new Uint8Array(),
|
|
||||||
info: encoder.encode('encryption'),
|
|
||||||
hash: 'SHA-256'
|
|
||||||
},
|
|
||||||
secretKey,
|
|
||||||
{
|
|
||||||
name: 'AES-GCM',
|
|
||||||
length: 128
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
['encrypt']
|
|
||||||
);
|
|
||||||
const authKey = await window.crypto.subtle.deriveKey(
|
|
||||||
{
|
|
||||||
name: 'HKDF',
|
|
||||||
salt: new Uint8Array(),
|
|
||||||
info: encoder.encode('authentication'),
|
|
||||||
hash: 'SHA-256'
|
|
||||||
},
|
|
||||||
secretKey,
|
|
||||||
{
|
|
||||||
name: 'HMAC',
|
|
||||||
hash: 'SHA-256'
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
['sign']
|
|
||||||
);
|
|
||||||
const metaKey = await window.crypto.subtle.deriveKey(
|
|
||||||
{
|
|
||||||
name: 'HKDF',
|
|
||||||
salt: new Uint8Array(),
|
|
||||||
info: encoder.encode('metadata'),
|
|
||||||
hash: 'SHA-256'
|
|
||||||
},
|
|
||||||
secretKey,
|
|
||||||
{
|
|
||||||
name: 'AES-GCM',
|
|
||||||
length: 128
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
['encrypt']
|
|
||||||
);
|
|
||||||
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
|
|
||||||
},
|
|
||||||
encryptKey,
|
|
||||||
plaintext
|
|
||||||
);
|
|
||||||
const metadata = await window.crypto.subtle.encrypt(
|
|
||||||
{
|
|
||||||
name: 'AES-GCM',
|
|
||||||
iv: new Uint8Array(12),
|
|
||||||
tagLength: 128
|
|
||||||
},
|
|
||||||
metaKey,
|
|
||||||
encoder.encode(
|
|
||||||
JSON.stringify({
|
|
||||||
iv: arrayToB64(this.iv),
|
|
||||||
name: this.file.name,
|
|
||||||
type: this.file.type || 'application/octet-stream'
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const rawAuth = await window.crypto.subtle.exportKey('raw', authKey);
|
|
||||||
if (this.cancelled) {
|
if (this.cancelled) {
|
||||||
throw new Error(0);
|
throw new Error(0);
|
||||||
}
|
}
|
||||||
return this.uploadFile(encrypted, metadata, new Uint8Array(rawAuth));
|
this.uploadRequest = uploadFile(
|
||||||
|
encrypted,
|
||||||
|
metadata,
|
||||||
|
authKeyB64,
|
||||||
|
this.keychain,
|
||||||
|
p => {
|
||||||
|
this.progress = p;
|
||||||
|
this.emit('progress', p);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async setPassword(password, file) {
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
const secretKey = await window.crypto.subtle.importKey(
|
|
||||||
'raw',
|
|
||||||
b64ToArray(file.secretKey),
|
|
||||||
'HKDF',
|
|
||||||
false,
|
|
||||||
['deriveKey']
|
|
||||||
);
|
);
|
||||||
const authKey = await window.crypto.subtle.deriveKey(
|
this.msg = 'fileSizeProgress';
|
||||||
{
|
try {
|
||||||
name: 'HKDF',
|
const result = await this.uploadRequest.result;
|
||||||
salt: new Uint8Array(),
|
const time = Date.now() - start;
|
||||||
info: encoder.encode('authentication'),
|
this.msg = 'notifyUploadDone';
|
||||||
hash: 'SHA-256'
|
this.uploadRequest = null;
|
||||||
},
|
this.progress = [1, 1];
|
||||||
secretKey,
|
const secretKey = arrayToB64(this.keychain.rawSecret);
|
||||||
{
|
const ownedFile = new OwnedFile({
|
||||||
name: 'HMAC',
|
id: result.id,
|
||||||
hash: 'SHA-256'
|
url: `${result.url}#${secretKey}`,
|
||||||
},
|
name: this.file.name,
|
||||||
true,
|
size: this.file.size,
|
||||||
['sign']
|
time: time,
|
||||||
);
|
speed: this.file.size / (time / 1000),
|
||||||
const sig = await window.crypto.subtle.sign(
|
createdAt: Date.now(),
|
||||||
{
|
expiresAt: Date.now() + EXPIRE_SECONDS * 1000,
|
||||||
name: 'HMAC'
|
secretKey: secretKey,
|
||||||
},
|
nonce: this.keychain.nonce,
|
||||||
authKey,
|
ownerToken: result.ownerToken
|
||||||
b64ToArray(file.nonce)
|
|
||||||
);
|
|
||||||
const pwdKey = await window.crypto.subtle.importKey(
|
|
||||||
'raw',
|
|
||||||
encoder.encode(password),
|
|
||||||
{ name: 'PBKDF2' },
|
|
||||||
false,
|
|
||||||
['deriveKey']
|
|
||||||
);
|
|
||||||
const newAuthKey = await window.crypto.subtle.deriveKey(
|
|
||||||
{
|
|
||||||
name: 'PBKDF2',
|
|
||||||
salt: encoder.encode(file.url),
|
|
||||||
iterations: 100,
|
|
||||||
hash: 'SHA-256'
|
|
||||||
},
|
|
||||||
pwdKey,
|
|
||||||
{
|
|
||||||
name: 'HMAC',
|
|
||||||
hash: 'SHA-256'
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
['sign']
|
|
||||||
);
|
|
||||||
const rawAuth = await window.crypto.subtle.exportKey('raw', newAuthKey);
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const xhr = new XMLHttpRequest();
|
|
||||||
xhr.onreadystatechange = () => {
|
|
||||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
|
||||||
if (xhr.status === 200) {
|
|
||||||
return resolve(xhr.response);
|
|
||||||
}
|
|
||||||
if (xhr.status === 401) {
|
|
||||||
const nonce = xhr
|
|
||||||
.getResponseHeader('WWW-Authenticate')
|
|
||||||
.split(' ')[1];
|
|
||||||
file.nonce = nonce;
|
|
||||||
}
|
|
||||||
reject(new Error(xhr.status));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
xhr.onerror = () => reject(new Error(0));
|
|
||||||
xhr.ontimeout = () => reject(new Error(0));
|
|
||||||
xhr.open('post', `/api/password/${file.id}`);
|
|
||||||
xhr.setRequestHeader(
|
|
||||||
'Authorization',
|
|
||||||
`send-v1 ${arrayToB64(new Uint8Array(sig))}`
|
|
||||||
);
|
|
||||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
|
||||||
xhr.responseType = 'json';
|
|
||||||
xhr.timeout = 2000;
|
|
||||||
xhr.send(JSON.stringify({ auth: arrayToB64(new Uint8Array(rawAuth)) }));
|
|
||||||
});
|
});
|
||||||
|
return ownedFile;
|
||||||
|
} catch (e) {
|
||||||
|
this.msg = 'errorPageHeader';
|
||||||
|
this.uploadRequest = null;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
209
app/keychain.js
Normal file
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
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';
|
||||||
25
app/main.js
25
app/main.js
@@ -1,3 +1,4 @@
|
|||||||
|
import 'fluent-intl-polyfill';
|
||||||
import app from './routes';
|
import app from './routes';
|
||||||
import locale from '../common/locales';
|
import locale from '../common/locales';
|
||||||
import fileManager from './fileManager';
|
import fileManager from './fileManager';
|
||||||
@@ -14,30 +15,34 @@ if (navigator.doNotTrack !== '1' && window.RAVEN_CONFIG) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
emitter.on('DOMContentLoaded', async () => {
|
window.appState = state;
|
||||||
let reason = null;
|
emitter.on('DOMContentLoaded', async function checkSupport() {
|
||||||
|
let unsupportedReason = null;
|
||||||
if (
|
if (
|
||||||
|
// Firefox < 50
|
||||||
/firefox/i.test(navigator.userAgent) &&
|
/firefox/i.test(navigator.userAgent) &&
|
||||||
parseInt(navigator.userAgent.match(/firefox\/*([^\n\r]*)\./i)[1], 10) <=
|
parseInt(navigator.userAgent.match(/firefox\/*([^\n\r]*)\./i)[1], 10) < 50
|
||||||
49
|
|
||||||
) {
|
) {
|
||||||
reason = 'outdated';
|
unsupportedReason = 'outdated';
|
||||||
}
|
}
|
||||||
if (/edge\/\d+/i.test(navigator.userAgent)) {
|
if (/edge\/\d+/i.test(navigator.userAgent)) {
|
||||||
reason = 'edge';
|
unsupportedReason = 'edge';
|
||||||
}
|
}
|
||||||
const ok = await canHasSend(assets.get('cryptofill.js'));
|
const ok = await canHasSend(assets.get('cryptofill.js'));
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
reason = /firefox/i.test(navigator.userAgent) ? 'outdated' : 'gcm';
|
unsupportedReason = /firefox/i.test(navigator.userAgent)
|
||||||
|
? 'outdated'
|
||||||
|
: 'gcm';
|
||||||
}
|
}
|
||||||
if (reason) {
|
if (unsupportedReason) {
|
||||||
setTimeout(() => emitter.emit('replaceState', `/unsupported/${reason}`));
|
setTimeout(() =>
|
||||||
|
emitter.emit('replaceState', `/unsupported/${unsupportedReason}`)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ let experiment = null;
|
|||||||
export default function initialize(state, emitter) {
|
export default function initialize(state, emitter) {
|
||||||
appState = state;
|
appState = state;
|
||||||
emitter.on('DOMContentLoaded', () => {
|
emitter.on('DOMContentLoaded', () => {
|
||||||
// addExitHandlers();
|
addExitHandlers();
|
||||||
experiment = storage.enrolled[0];
|
experiment = storage.enrolled[0];
|
||||||
sendEvent(category(), 'visit', {
|
sendEvent(category(), 'visit', {
|
||||||
cm5: storage.totalUploads,
|
cm5: storage.totalUploads,
|
||||||
@@ -29,9 +29,8 @@ export default function initialize(state, emitter) {
|
|||||||
});
|
});
|
||||||
//TODO restart handlers... somewhere
|
//TODO restart handlers... somewhere
|
||||||
});
|
});
|
||||||
emitter.on('exit', evt => {
|
emitter.on('exit', exitEvent);
|
||||||
exitEvent(evt);
|
emitter.on('experiment', experimentEvent);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function category() {
|
function category() {
|
||||||
@@ -205,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,
|
||||||
@@ -249,6 +258,10 @@ function exitEvent(target) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function experimentEvent(params) {
|
||||||
|
return sendEvent(category(), 'experiment', params);
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
// 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'));
|
||||||
@@ -272,6 +285,7 @@ export {
|
|||||||
cancelledUpload,
|
cancelledUpload,
|
||||||
stoppedUpload,
|
stoppedUpload,
|
||||||
completedUpload,
|
completedUpload,
|
||||||
|
changedDownloadLimit,
|
||||||
deletedUpload,
|
deletedUpload,
|
||||||
startedDownload,
|
startedDownload,
|
||||||
cancelledDownload,
|
cancelledDownload,
|
||||||
|
|||||||
84
app/ownedFile.js
Normal file
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
const html = require('choo/html');
|
const html = require('choo/html');
|
||||||
|
|
||||||
module.exports = function() {
|
module.exports = function() {
|
||||||
const div = html`<div id="page-one"></div>`;
|
return html`<div></div>`;
|
||||||
return div;
|
|
||||||
};
|
};
|
||||||
26
app/pages/completed/index.js
Normal file
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
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
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
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
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
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
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
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
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
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
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
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,12 +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.fileInfo) {
|
||||||
|
// 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let pageAction = null; //default state: we don't have file metadata
|
||||||
if (state.transfer) {
|
if (state.transfer) {
|
||||||
const s = state.transfer.state;
|
const s = state.transfer.state;
|
||||||
if (s === 'downloading' || s === 'complete') {
|
if (['downloading', 'decrypting', 'complete'].indexOf(s) > -1) {
|
||||||
|
// Downloading is in progress
|
||||||
return download(state, emit);
|
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, emit);
|
return preview(state, pageAction);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +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 && state.transfer.iv) {
|
if (state.uploading) {
|
||||||
//TODO relying on 'iv' is gross
|
|
||||||
return upload(state, emit);
|
return upload(state, emit);
|
||||||
}
|
}
|
||||||
return welcome(state, emit);
|
return welcome(state, emit);
|
||||||
|
|||||||
@@ -1,28 +1,43 @@
|
|||||||
const choo = require('choo');
|
const choo = require('choo');
|
||||||
const html = require('choo/html');
|
const html = require('choo/html');
|
||||||
|
const nanotiming = require('nanotiming');
|
||||||
const download = require('./download');
|
const download = require('./download');
|
||||||
const header = require('../templates/header');
|
const header = require('../templates/header');
|
||||||
const footer = require('../templates/footer');
|
const footer = require('../templates/footer');
|
||||||
const fxPromo = require('../templates/fxPromo');
|
const fxPromo = require('../templates/fxPromo');
|
||||||
|
|
||||||
|
nanotiming.disabled = true;
|
||||||
const app = choo();
|
const app = choo();
|
||||||
|
|
||||||
|
function banner(state, emit) {
|
||||||
|
if (state.promo && !state.route.startsWith('/unsupported/')) {
|
||||||
|
return fxPromo(state, emit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function body(template) {
|
function body(template) {
|
||||||
return function(state, emit) {
|
return function(state, emit) {
|
||||||
const b = html`<body>
|
const b = html`<body>
|
||||||
${state.promo === 'header' ? fxPromo(state, emit) : ''}
|
${banner(state, emit)}
|
||||||
${header(state)}
|
${header(state)}
|
||||||
<div class="all">
|
<main class="main">
|
||||||
<noscript>
|
<noscript>
|
||||||
<h2>Firefox Send requires JavaScript</h2>
|
<div class="noscript">
|
||||||
<p><a href="https://github.com/mozilla/send/blob/master/docs/faq.md#why-does-firefox-send-require-javascript">Why does Firefox Send require JavaScript?</a></p>
|
<h2>${state.translate('javascriptRequired')}</h2>
|
||||||
<p>Please enable JavaScript and try again.</p>
|
<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>
|
</noscript>
|
||||||
${template(state, emit)}
|
${template(state, emit)}
|
||||||
</div>
|
</main>
|
||||||
${footer(state)}
|
${footer(state)}
|
||||||
</body>`;
|
</body>`;
|
||||||
if (state.layout) {
|
if (state.layout) {
|
||||||
|
// server side only
|
||||||
return state.layout(state, b);
|
return state.layout(state, b);
|
||||||
}
|
}
|
||||||
return b;
|
return b;
|
||||||
@@ -30,14 +45,14 @@ function body(template) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.route('/', body(require('./home')));
|
app.route('/', body(require('./home')));
|
||||||
app.route('/share/:id', body(require('../templates/share')));
|
app.route('/share/:id', body(require('../pages/share')));
|
||||||
app.route('/download/:id', body(download));
|
app.route('/download/:id', body(download));
|
||||||
app.route('/download/:id/:key', body(download));
|
app.route('/download/:id/:key', body(download));
|
||||||
app.route('/completed', body(require('../templates/completed')));
|
app.route('/completed', body(require('../pages/completed')));
|
||||||
app.route('/unsupported/:reason', body(require('../templates/unsupported')));
|
app.route('/unsupported/:reason', body(require('../pages/unsupported')));
|
||||||
app.route('/legal', body(require('../templates/legal')));
|
app.route('/legal', body(require('../pages/legal')));
|
||||||
app.route('/error', body(require('../templates/error')));
|
app.route('/error', body(require('../pages/error')));
|
||||||
app.route('/blank', body(require('../templates/blank')));
|
app.route('/blank', body(require('../pages/blank')));
|
||||||
app.route('*', body(require('../templates/notFound')));
|
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;
|
||||||
}
|
}
|
||||||
@@ -108,11 +109,15 @@ 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() {
|
writeFiles() {
|
||||||
this._files.forEach(f => this.engine.setItem(f.id, JSON.stringify(f)));
|
this._files.forEach(f => this.writeFile(f));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
const progress = require('./progress');
|
|
||||||
const { fadeOut } = require('../utils');
|
|
||||||
const fxPromo = require('./fxPromo');
|
|
||||||
|
|
||||||
module.exports = function(state, emit) {
|
|
||||||
const div = html`
|
|
||||||
<div id="page-one">
|
|
||||||
<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>
|
|
||||||
${state.promo === 'body' ? fxPromo(state, emit) : ''}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
async function sendNew(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
await fadeOut('download');
|
|
||||||
emit('pushState', '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
return div;
|
|
||||||
};
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
const progress = require('./progress');
|
|
||||||
const { bytes } = require('../utils');
|
|
||||||
const fxPromo = require('./fxPromo');
|
|
||||||
|
|
||||||
module.exports = function(state, emit) {
|
|
||||||
const transfer = state.transfer;
|
|
||||||
const div = html`
|
|
||||||
<div id="page-one">
|
|
||||||
<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>
|
|
||||||
${state.promo === 'body' ? fxPromo(state, emit) : ''}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
return div;
|
|
||||||
};
|
|
||||||
6
app/templates/downloadButton/downloadButton.css
Normal file
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
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
|
|
||||||
module.exports = function(state, emit) {
|
|
||||||
const fileInfo = state.fileInfo;
|
|
||||||
const label =
|
|
||||||
fileInfo.password === null
|
|
||||||
? html`
|
|
||||||
<label class="red"
|
|
||||||
for="unlock-input">${state.translate('passwordTryAgain')}</label>`
|
|
||||||
: html`
|
|
||||||
<label for="unlock-input">
|
|
||||||
${state.translate('unlockInputLabel')}
|
|
||||||
</label>`;
|
|
||||||
const div = html`
|
|
||||||
<div class="enterPassword">
|
|
||||||
${label}
|
|
||||||
<form id="unlock" onsubmit=${checkPassword} data-no-csrf>
|
|
||||||
<input id="unlock-input"
|
|
||||||
class="unlock-input input-no-btn"
|
|
||||||
maxlength="64"
|
|
||||||
autocomplete="off"
|
|
||||||
placeholder="${state.translate('unlockInputPlaceholder')}"
|
|
||||||
oninput=${inputChanged}
|
|
||||||
type="password"/>
|
|
||||||
<input type="submit"
|
|
||||||
id="unlock-btn"
|
|
||||||
class="btn btn-hidden"
|
|
||||||
value="${state.translate('unlockButtonLabel')}"/>
|
|
||||||
</form>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
function inputChanged() {
|
|
||||||
const input = document.getElementById('unlock-input');
|
|
||||||
const btn = document.getElementById('unlock-btn');
|
|
||||||
if (input.value.length > 0) {
|
|
||||||
btn.classList.remove('btn-hidden');
|
|
||||||
input.classList.remove('input-no-btn');
|
|
||||||
} else {
|
|
||||||
btn.classList.add('btn-hidden');
|
|
||||||
input.classList.add('input-no-btn');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkPassword(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const password = document.getElementById('unlock-input').value;
|
|
||||||
if (password.length > 0) {
|
|
||||||
document.getElementById('unlock-btn').disabled = true;
|
|
||||||
state.fileInfo.url = window.location.href;
|
|
||||||
state.fileInfo.password = password;
|
|
||||||
emit('preview');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return div;
|
|
||||||
};
|
|
||||||
22
app/templates/downloadPassword/downloadPassword.css
Normal file
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
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
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
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
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
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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
const assets = require('../../common/assets');
|
|
||||||
|
|
||||||
module.exports = function(state) {
|
|
||||||
return html`<div class="footer">
|
|
||||||
<div class="legal-links">
|
|
||||||
<a href="https://www.mozilla.org" role="presentation"><img class="mozilla-logo" src="${assets.get(
|
|
||||||
'mozilla-logo.svg'
|
|
||||||
)}" alt="mozilla"/></a>
|
|
||||||
<a href="https://www.mozilla.org/about/legal">${state.translate(
|
|
||||||
'footerLinkLegal'
|
|
||||||
)}</a>
|
|
||||||
<a href="https://testpilot.firefox.com/about">${state.translate(
|
|
||||||
'footerLinkAbout'
|
|
||||||
)}</a>
|
|
||||||
<a href="/legal">${state.translate('footerLinkPrivacy')}</a>
|
|
||||||
<a href="/legal">${state.translate('footerLinkTerms')}</a>
|
|
||||||
<a href="https://www.mozilla.org/privacy/websites/#cookies">${state.translate(
|
|
||||||
'footerLinkCookies'
|
|
||||||
)}</a>
|
|
||||||
</div>
|
|
||||||
<div class="social-links">
|
|
||||||
<a href="https://github.com/mozilla/send" role="presentation"><img class="github" src="${assets.get(
|
|
||||||
'github-icon.svg'
|
|
||||||
)}" alt="github"/></a>
|
|
||||||
<a href="https://twitter.com/FxTestPilot" role="presentation"><img class="twitter" src="${assets.get(
|
|
||||||
'twitter-icon.svg'
|
|
||||||
)}" alt="twitter"/></a>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
};
|
|
||||||
93
app/templates/footer/footer.css
Normal file
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
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>`;
|
||||||
|
};
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
const assets = require('../../common/assets');
|
|
||||||
|
|
||||||
// function replaceLinks(str, urls) {
|
|
||||||
// let i = -1;
|
|
||||||
// const s = str.replace(/<a>([^<]+)<\/a>/g, (m, v) => {
|
|
||||||
// i++;
|
|
||||||
// return `<a class="link" href="${urls[i]}">${v}</a>`;
|
|
||||||
// });
|
|
||||||
// return [`<span>${s}</span>`];
|
|
||||||
// }
|
|
||||||
|
|
||||||
module.exports = function(state, emit) {
|
|
||||||
// function close() {
|
|
||||||
// document.querySelector('.banner').remove();
|
|
||||||
// }
|
|
||||||
|
|
||||||
function clicked(evt) {
|
|
||||||
emit('exit', evt);
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div class="banner">
|
|
||||||
<div>
|
|
||||||
<img
|
|
||||||
src="${assets.get('firefox_logo-only.svg')}"
|
|
||||||
class="firefox-logo-small"
|
|
||||||
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>`;
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
<img
|
|
||||||
src="${assets.get('close-16.svg')}"
|
|
||||||
class="icon-delete"
|
|
||||||
onclick=${close}>
|
|
||||||
*/
|
|
||||||
56
app/templates/fxPromo/fxPromo.css
Normal file
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
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' });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
const assets = require('../../common/assets');
|
|
||||||
|
|
||||||
module.exports = function(state) {
|
|
||||||
return html`<header class="header">
|
|
||||||
<div class="send-logo">
|
|
||||||
<a href="/">
|
|
||||||
<img src="${assets.get(
|
|
||||||
'send_logo.svg'
|
|
||||||
)}" alt="Send"/><h1 class="site-title">Send</h1>
|
|
||||||
</a>
|
|
||||||
<div class="site-subtitle">
|
|
||||||
<a href="https://testpilot.firefox.com">Firefox Test Pilot</a>
|
|
||||||
<div>${state.translate('siteSubtitle')}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a href="https://qsurvey.mozilla.com/s3/txp-firefox-send" rel="noreferrer noopener" class="feedback" target="_blank">${state.translate(
|
|
||||||
'siteFeedback'
|
|
||||||
)}</a>
|
|
||||||
</header>`;
|
|
||||||
};
|
|
||||||
104
app/templates/header/header.css
Normal file
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
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,34 +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>
|
|
||||||
`;
|
|
||||||
return div;
|
|
||||||
};
|
|
||||||
@@ -1,21 +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>`;
|
|
||||||
return div;
|
|
||||||
};
|
|
||||||
109
app/templates/passwordInput/index.js
Normal file
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
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
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
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,74 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
const assets = require('../../common/assets');
|
|
||||||
const notFound = require('./notFound');
|
|
||||||
const downloadPassword = require('./downloadPassword');
|
|
||||||
const { bytes } = require('../utils');
|
|
||||||
const fxPromo = require('./fxPromo');
|
|
||||||
|
|
||||||
function getFileFromDOM() {
|
|
||||||
const el = document.getElementById('dl-file');
|
|
||||||
if (!el) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
nonce: el.getAttribute('data-nonce'),
|
|
||||||
pwd: !!+el.getAttribute('data-requires-password')
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = fileInfo.size
|
|
||||||
? state.translate('downloadFileSize', { size: bytes(fileInfo.size) })
|
|
||||||
: '';
|
|
||||||
let action = html`
|
|
||||||
<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>`;
|
|
||||||
if (fileInfo.pwd && !fileInfo.password) {
|
|
||||||
action = downloadPassword(state, emit);
|
|
||||||
} else if (!state.transfer) {
|
|
||||||
emit('preview');
|
|
||||||
}
|
|
||||||
const title = fileInfo.name
|
|
||||||
? state.translate('downloadFileName', { filename: fileInfo.name })
|
|
||||||
: state.translate('downloadFileTitle');
|
|
||||||
const div = html`
|
|
||||||
<div id="page-one">
|
|
||||||
<div id="download">
|
|
||||||
<div id="download-page-one">
|
|
||||||
<div class="title">
|
|
||||||
<span id="dl-file"
|
|
||||||
data-nonce="${fileInfo.nonce}"
|
|
||||||
data-requires-password="${fileInfo.pwd}">${title}</span>
|
|
||||||
<span id="dl-filesize">${' ' + size}</span>
|
|
||||||
</div>
|
|
||||||
<div class="description">${state.translate('downloadMessage')}</div>
|
|
||||||
${action}
|
|
||||||
</div>
|
|
||||||
<a class="send-new" href="/">${state.translate('sendYourFilesLink')}</a>
|
|
||||||
</div>
|
|
||||||
${state.promo === 'body' ? fxPromo(state, emit) : ''}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
function download(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
emit('download', fileInfo);
|
|
||||||
}
|
|
||||||
return div;
|
|
||||||
};
|
|
||||||
@@ -1,29 +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
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
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
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
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
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
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,90 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
const assets = require('../../common/assets');
|
|
||||||
const notFound = require('./notFound');
|
|
||||||
const uploadPassword = require('./uploadPassword');
|
|
||||||
const { allowedCopy, delay, fadeOut } = require('../utils');
|
|
||||||
|
|
||||||
function passwordComplete(state, password) {
|
|
||||||
const el = html([
|
|
||||||
`<div class="selectPassword">${state.translate('passwordResult', {
|
|
||||||
password: '<pre></pre>'
|
|
||||||
})}</div>`
|
|
||||||
]);
|
|
||||||
el.lastElementChild.textContent = password;
|
|
||||||
return el;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = function(state, emit) {
|
|
||||||
const file = state.storage.getFileById(state.params.id);
|
|
||||||
if (!file) {
|
|
||||||
return notFound(state, emit);
|
|
||||||
}
|
|
||||||
|
|
||||||
file.password = file.password || '';
|
|
||||||
|
|
||||||
const passwordSection = file.password
|
|
||||||
? passwordComplete(state, file.password)
|
|
||||||
: uploadPassword(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"
|
|
||||||
title="${state.translate('copyUrlFormButton')}"
|
|
||||||
onclick=${copyLink}>${state.translate('copyUrlFormButton')}</button>
|
|
||||||
</div>
|
|
||||||
${passwordSection}
|
|
||||||
<button id="delete-file"
|
|
||||||
class="btn"
|
|
||||||
title="${state.translate('deleteFileButton')}"
|
|
||||||
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.classList.add('success');
|
|
||||||
copyBtn.replaceChild(
|
|
||||||
html`<img src="${assets.get('check-16.svg')}" class="icon-check">`,
|
|
||||||
copyBtn.firstChild
|
|
||||||
);
|
|
||||||
await delay(2000);
|
|
||||||
input.disabled = false;
|
|
||||||
if (!copyBtn.parentNode.classList.contains('wait-password')) {
|
|
||||||
copyBtn.disabled = false;
|
|
||||||
}
|
|
||||||
copyBtn.classList.remove('success');
|
|
||||||
copyBtn.textContent = state.translate('copyUrlFormButton');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteFile() {
|
|
||||||
emit('delete', { file, location: 'success-screen' });
|
|
||||||
await fadeOut('share-link');
|
|
||||||
emit('pushState', '/');
|
|
||||||
}
|
|
||||||
return div;
|
|
||||||
};
|
|
||||||
@@ -1,46 +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>`;
|
|
||||||
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" 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');
|
|
||||||
}
|
|
||||||
return div;
|
|
||||||
};
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
|
|
||||||
module.exports = function(state, emit) {
|
|
||||||
const file = state.storage.getFileById(state.params.id);
|
|
||||||
const div = html`
|
|
||||||
<div class="selectPassword">
|
|
||||||
<div id="addPasswordWrapper">
|
|
||||||
<input id="addPassword" type="checkbox" autocomplete="off" onchange=${
|
|
||||||
togglePasswordInput
|
|
||||||
}/>
|
|
||||||
<label for="addPassword">
|
|
||||||
${state.translate('requirePasswordCheckbox')}</label>
|
|
||||||
</div>
|
|
||||||
<form class="setPassword hidden" onsubmit=${setPassword} data-no-csrf>
|
|
||||||
<input id="unlock-input"
|
|
||||||
class="unlock-input input-no-btn"
|
|
||||||
maxlength="64"
|
|
||||||
autocomplete="off"
|
|
||||||
placeholder="${state.translate('unlockInputPlaceholder')}"
|
|
||||||
oninput=${inputChanged}/>
|
|
||||||
<input type="submit"
|
|
||||||
id="unlock-btn"
|
|
||||||
class="btn btn-hidden"
|
|
||||||
value="${state.translate('addPasswordButton')}"/>
|
|
||||||
</form>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
function inputChanged() {
|
|
||||||
const input = document.getElementById('unlock-input');
|
|
||||||
const btn = document.getElementById('unlock-btn');
|
|
||||||
if (input.value.length > 0) {
|
|
||||||
btn.classList.remove('btn-hidden');
|
|
||||||
input.classList.remove('input-no-btn');
|
|
||||||
} else {
|
|
||||||
btn.classList.add('btn-hidden');
|
|
||||||
input.classList.add('input-no-btn');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function togglePasswordInput(e) {
|
|
||||||
const unlockInput = document.getElementById('unlock-input');
|
|
||||||
const boxChecked = e.target.checked;
|
|
||||||
document
|
|
||||||
.querySelector('.setPassword')
|
|
||||||
.classList.toggle('hidden', !boxChecked);
|
|
||||||
if (boxChecked) {
|
|
||||||
unlockInput.focus();
|
|
||||||
} else {
|
|
||||||
unlockInput.value = '';
|
|
||||||
}
|
|
||||||
inputChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
function setPassword(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const password = document.getElementById('unlock-input').value;
|
|
||||||
if (password.length > 0) {
|
|
||||||
document.getElementById('copy').classList.remove('wait-password');
|
|
||||||
document.getElementById('copy-btn').disabled = false;
|
|
||||||
emit('password', { password, file });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return div;
|
|
||||||
};
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
const assets = require('../../common/assets');
|
|
||||||
const fileList = require('./fileList');
|
|
||||||
const fxPromo = require('./fxPromo');
|
|
||||||
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="upload-window"
|
|
||||||
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>
|
|
||||||
<input id="file-upload"
|
|
||||||
type="file"
|
|
||||||
name="fileUploaded"
|
|
||||||
onfocus=${onfocus}
|
|
||||||
onblur=${onblur}
|
|
||||||
onchange=${upload} />
|
|
||||||
<label for="file-upload"
|
|
||||||
id="browse"
|
|
||||||
class="btn browse"
|
|
||||||
title="${state.translate('uploadPageBrowseButton1')}">
|
|
||||||
${state.translate('uploadPageBrowseButton1')}</label>
|
|
||||||
</div>
|
|
||||||
${state.promo === 'body' ? fxPromo(state, emit) : ''}
|
|
||||||
${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');
|
|
||||||
}
|
|
||||||
|
|
||||||
function onfocus(event) {
|
|
||||||
event.target.classList.add('has-focus');
|
|
||||||
}
|
|
||||||
|
|
||||||
function onblur(event) {
|
|
||||||
event.target.classList.remove('has-focus');
|
|
||||||
}
|
|
||||||
|
|
||||||
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' });
|
|
||||||
}
|
|
||||||
return div;
|
|
||||||
};
|
|
||||||
51
app/utils.js
51
app/utils.js
@@ -15,21 +15,6 @@ function b64ToArray(str) {
|
|||||||
return b64.toByteArray(str);
|
return b64.toByteArray(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const shim = document.createElement('script');
|
const shim = document.createElement('script');
|
||||||
@@ -132,6 +117,14 @@ function percent(ratio) {
|
|||||||
return `${Math.floor(ratio * 100)}%`;
|
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() {
|
||||||
const support = !!document.queryCommandSupported;
|
const support = !!document.queryCommandSupported;
|
||||||
return support ? document.queryCommandSupported('copy') : false;
|
return support ? document.queryCommandSupported('copy') : false;
|
||||||
@@ -141,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,
|
||||||
@@ -156,11 +163,11 @@ module.exports = {
|
|||||||
allowedCopy,
|
allowedCopy,
|
||||||
bytes,
|
bytes,
|
||||||
percent,
|
percent,
|
||||||
|
number,
|
||||||
copyToClipboard,
|
copyToClipboard,
|
||||||
arrayToB64,
|
arrayToB64,
|
||||||
b64ToArray,
|
b64ToArray,
|
||||||
notify,
|
|
||||||
canHasSend,
|
canHasSend,
|
||||||
isFile,
|
isFile,
|
||||||
ONE_DAY_IN_MS
|
openLinksInNewTab
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 |
1097
assets/main.css
1097
assets/main.css
File diff suppressed because it is too large
Load Diff
17
assets/spinner.svg
Normal file
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 |
@@ -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
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);
|
||||||
|
};
|
||||||
132
circle.yml
132
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 .
|
|
||||||
- docker push mozilla/send:latest
|
|
||||||
tags:
|
|
||||||
tag: /.*/
|
|
||||||
owner: mozilla
|
|
||||||
commands:
|
|
||||||
- docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
|
|
||||||
- docker build -t mozilla/send:$CIRCLE_TAG .
|
|
||||||
- docker push mozilla/send:$CIRCLE_TAG
|
|
||||||
|
|
||||||
test:
|
test:
|
||||||
override:
|
docker:
|
||||||
- npm run build
|
- image: circleci/node:8-browsers
|
||||||
- npm run lint
|
steps:
|
||||||
- npm test
|
- checkout
|
||||||
- nsp check
|
- restore_cache:
|
||||||
|
key: send-{{ checksum "package-lock.json" }}
|
||||||
|
- run: npm install
|
||||||
|
- save_cache:
|
||||||
|
key: send-{{ checksum "package-lock.json" }}
|
||||||
|
paths:
|
||||||
|
- node_modules
|
||||||
|
- run: npm run check
|
||||||
|
- run: npm run lint
|
||||||
|
- run: npm test
|
||||||
|
- 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,17 @@ 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`
|
#### `password-added`
|
||||||
Triggered whenever a password is added to a file. Includes:
|
Triggered whenever a password is added to a file. Includes:
|
||||||
|
|
||||||
|
|||||||
19
docs/takedowns.md
Normal file
19
docs/takedowns.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
## Take-down process
|
||||||
|
|
||||||
|
In cases of a DMCA notice, or other abuse yet to be determined, a file has to be removed from the service.
|
||||||
|
|
||||||
|
Files can be delisted and made inaccessible by removing their record from Redis.
|
||||||
|
|
||||||
|
Send share links contain the `id` of the file, for example `https://send.firefox.com/download/3d9d2bb9a1`
|
||||||
|
|
||||||
|
From a host with access to the Redis server run a `DEL` command with the file id.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
redis-cli DEL 3d9d2bb9a1
|
||||||
|
```
|
||||||
|
|
||||||
|
Other redis-cli parameters like `-h` may also be required. See [redis-cli docs](https://redis.io/topics/rediscli) for more info.
|
||||||
|
|
||||||
|
The encrypted file resides on S3 as the same `id` under the bucket that the app was configured with as `S3_BUCKET`. The file can be managed if it has not already expired with the [AWS cli](https://docs.aws.amazon.com/cli/latest/reference/s3/index.html) or AWS web console.
|
||||||
12813
package-lock.json
generated
12813
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
110
package.json
110
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "firefox-send",
|
"name": "firefox-send",
|
||||||
"description": "File Sharing Experiment",
|
"description": "File Sharing Experiment",
|
||||||
"version": "2.0.0",
|
"version": "2.4.0",
|
||||||
"author": "Mozilla (https://mozilla.org)",
|
"author": "Mozilla (https://mozilla.org)",
|
||||||
"repository": "mozilla/send",
|
"repository": "mozilla/send",
|
||||||
"homepage": "https://github.com/mozilla/send/",
|
"homepage": "https://github.com/mozilla/send/",
|
||||||
@@ -9,10 +9,12 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"precommit": "lint-staged",
|
"precommit": "lint-staged",
|
||||||
|
"prepush": "npm test",
|
||||||
|
"check": "nsp check",
|
||||||
"clean": "rimraf dist",
|
"clean": "rimraf dist",
|
||||||
"build": "npm run clean && webpack -p",
|
"build": "npm run clean && webpack -p",
|
||||||
"lint": "npm-run-all lint:*",
|
"lint": "npm-run-all lint:*",
|
||||||
"lint:css": "stylelint 'assets/*.css'",
|
"lint:css": "stylelint app/*.css app/**/*.css",
|
||||||
"lint:js": "eslint .",
|
"lint:js": "eslint .",
|
||||||
"lint-locales": "node scripts/lint-locales",
|
"lint-locales": "node scripts/lint-locales",
|
||||||
"lint-locales:dev": "npm run lint-locales",
|
"lint-locales:dev": "npm run lint-locales",
|
||||||
@@ -23,9 +25,12 @@
|
|||||||
"changelog": "github-changes -o mozilla -r send --only-pulls --use-commit-body --no-merges",
|
"changelog": "github-changes -o mozilla -r send --only-pulls --use-commit-body --no-merges",
|
||||||
"contributors": "git shortlog -s | awk -F\\t '{print $2}' > CONTRIBUTORS",
|
"contributors": "git shortlog -s | awk -F\\t '{print $2}' > CONTRIBUTORS",
|
||||||
"release": "npm-run-all contributors changelog",
|
"release": "npm-run-all contributors changelog",
|
||||||
"test": "mocha test/unit",
|
"test": "npm-run-all test:*",
|
||||||
|
"test:backend": "nyc mocha --reporter=min test/unit",
|
||||||
|
"test:frontend": "cross-env NODE_ENV=development node test/frontend/runner.js && nyc report --reporter=html",
|
||||||
"start": "cross-env NODE_ENV=development webpack-dev-server",
|
"start": "cross-env NODE_ENV=development webpack-dev-server",
|
||||||
"prod": "node server/prod.js"
|
"prod": "node server/prod.js",
|
||||||
|
"cover": "nyc --reporter=html mocha test/unit"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.js": [
|
"*.js": [
|
||||||
@@ -39,77 +44,93 @@
|
|||||||
"git add"
|
"git add"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"nyc": {
|
||||||
|
"reporter": [
|
||||||
|
"text"
|
||||||
|
],
|
||||||
|
"cache": true
|
||||||
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.2.0"
|
"node": ">=8.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"autoprefixer": "^7.1.6",
|
|
||||||
"babel-core": "^6.26.0",
|
"babel-core": "^6.26.0",
|
||||||
"babel-loader": "^7.1.2",
|
"babel-loader": "^7.1.3",
|
||||||
"babel-plugin-yo-yoify": "^1.0.1",
|
"babel-plugin-istanbul": "^4.1.5",
|
||||||
"babel-polyfill": "^6.26.0",
|
"babel-plugin-yo-yoify": "^1.0.2",
|
||||||
"babel-preset-env": "^1.6.1",
|
"babel-preset-env": "^1.6.1",
|
||||||
"babel-preset-es2015": "^6.24.1",
|
"babel-preset-es2015": "^6.24.1",
|
||||||
"babel-preset-stage-2": "^6.24.1",
|
"babel-preset-stage-2": "^6.24.1",
|
||||||
"base64-js": "^1.2.1",
|
"babel-preset-stage-3": "^6.24.1",
|
||||||
"copy-webpack-plugin": "^4.2.0",
|
"base64-js": "^1.2.3",
|
||||||
"cross-env": "^5.1.1",
|
"copy-webpack-plugin": "^4.4.2",
|
||||||
"css-loader": "^0.28.7",
|
"cross-env": "^5.1.3",
|
||||||
"css-mqpacker": "^6.0.1",
|
"css-loader": "^0.28.10",
|
||||||
"cssnano": "^3.10.0",
|
"css-mqpacker": "^6.0.2",
|
||||||
"eslint": "^4.10.0",
|
"eslint": "^4.18.1",
|
||||||
"eslint-plugin-mocha": "^4.11.0",
|
"eslint-plugin-mocha": "^4.11.0",
|
||||||
"eslint-plugin-node": "^5.2.1",
|
"eslint-plugin-node": "^6.0.1",
|
||||||
"eslint-plugin-security": "^1.4.0",
|
"eslint-plugin-security": "^1.4.0",
|
||||||
"expose-loader": "^0.7.3",
|
"expose-loader": "^0.7.4",
|
||||||
"extract-loader": "^1.0.1",
|
"extract-loader": "^1.0.2",
|
||||||
"file-loader": "^1.1.5",
|
"extract-text-webpack-plugin": "^3.0.2",
|
||||||
"git-rev-sync": "^1.9.1",
|
"file-loader": "^1.1.9",
|
||||||
"github-changes": "^1.1.1",
|
"fluent-intl-polyfill": "^0.1.0",
|
||||||
"html-loader": "^0.5.1",
|
"git-rev-sync": "^1.10.0",
|
||||||
|
"github-changes": "^1.1.2",
|
||||||
|
"html-loader": "^0.5.5",
|
||||||
"husky": "^0.14.3",
|
"husky": "^0.14.3",
|
||||||
"lint-staged": "^4.3.0",
|
"lint-staged": "^7.0.0",
|
||||||
"mocha": "^3.5.3",
|
"mocha": "^5.0.0",
|
||||||
"nanobus": "^4.3.0",
|
"nanobus": "^4.3.2",
|
||||||
|
"nanotiming": "^7.3.0",
|
||||||
"npm-run-all": "^4.1.2",
|
"npm-run-all": "^4.1.2",
|
||||||
"postcss-loader": "^2.0.8",
|
"nsp": "^3.2.1",
|
||||||
"prettier": "^1.8.2",
|
"nyc": "^11.5.0",
|
||||||
|
"postcss-cssnext": "^3.1.0",
|
||||||
|
"postcss-import": "^11.1.0",
|
||||||
|
"postcss-loader": "^2.1.0",
|
||||||
|
"prettier": "^1.10.2",
|
||||||
"proxyquire": "^1.8.0",
|
"proxyquire": "^1.8.0",
|
||||||
"raven-js": "^3.19.1",
|
"puppeteer": "^1.1.1",
|
||||||
"redis-mock": "^0.20.0",
|
"raven-js": "^3.22.2",
|
||||||
|
"redis-mock": "^0.21.0",
|
||||||
"require-from-string": "^2.0.1",
|
"require-from-string": "^2.0.1",
|
||||||
"rimraf": "^2.6.2",
|
"rimraf": "^2.6.2",
|
||||||
"selenium-webdriver": "^3.6.0",
|
"sinon": "^4.4.2",
|
||||||
"sinon": "^4.1.2",
|
|
||||||
"string-hash": "^1.1.3",
|
"string-hash": "^1.1.3",
|
||||||
"stylelint-config-standard": "^17.0.0",
|
"stylelint": "^9.1.1",
|
||||||
"stylelint-no-unsupported-browser-features": "^1.0.1",
|
"stylelint-config-standard": "^18.1.0",
|
||||||
"supertest": "^3.0.0",
|
"stylelint-no-unsupported-browser-features": "^2.0.0",
|
||||||
|
"svgo": "^1.0.5",
|
||||||
|
"svgo-loader": "^2.1.0",
|
||||||
"testpilot-ga": "^0.3.0",
|
"testpilot-ga": "^0.3.0",
|
||||||
"val-loader": "^1.0.2",
|
"val-loader": "^1.1.0",
|
||||||
"webpack": "^3.8.1",
|
"webpack": "^3.11.0",
|
||||||
|
"webpack-dev-middleware": "^2.0.6",
|
||||||
"webpack-dev-server": "2.9.1",
|
"webpack-dev-server": "2.9.1",
|
||||||
"webpack-manifest-plugin": "^1.3.2",
|
"webpack-manifest-plugin": "^1.3.2",
|
||||||
"webpack-unassert-loader": "^1.2.0"
|
"webpack-unassert-loader": "^1.2.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"aws-sdk": "^2.149.0",
|
"aws-sdk": "^2.202.0",
|
||||||
"body-parser": "^1.18.2",
|
"babel-polyfill": "^6.26.0",
|
||||||
"choo": "^6.5.1",
|
"choo": "^6.7.0",
|
||||||
"cldr-core": "^32.0.0",
|
"cldr-core": "^32.0.0",
|
||||||
"connect-busboy": "0.0.2",
|
"connect-busboy": "0.0.2",
|
||||||
"convict": "^4.0.1",
|
"convict": "^4.0.1",
|
||||||
"express": "^4.16.2",
|
"express": "^4.16.2",
|
||||||
"fluent": "^0.4.1",
|
"fluent": "^0.6.3",
|
||||||
"fluent-langneg": "^0.1.0",
|
"fluent-langneg": "^0.1.0",
|
||||||
"helmet": "^3.9.0",
|
"helmet": "^3.11.0",
|
||||||
"mkdirp": "^0.5.1",
|
"mkdirp": "^0.5.1",
|
||||||
"mozlog": "^2.1.1",
|
"mozlog": "^2.2.0",
|
||||||
"raven": "^2.2.1",
|
"raven": "^2.4.1",
|
||||||
"redis": "^2.8.0"
|
"redis": "^2.8.0"
|
||||||
},
|
},
|
||||||
"availableLanguages": [
|
"availableLanguages": [
|
||||||
"en-US",
|
"en-US",
|
||||||
|
"ar",
|
||||||
"ast",
|
"ast",
|
||||||
"az",
|
"az",
|
||||||
"bs",
|
"bs",
|
||||||
@@ -130,6 +151,7 @@
|
|||||||
"fy-NL",
|
"fy-NL",
|
||||||
"hsb",
|
"hsb",
|
||||||
"hu",
|
"hu",
|
||||||
|
"ia",
|
||||||
"id",
|
"id",
|
||||||
"it",
|
"it",
|
||||||
"ja",
|
"ja",
|
||||||
@@ -142,9 +164,11 @@
|
|||||||
"nn-NO",
|
"nn-NO",
|
||||||
"pt-BR",
|
"pt-BR",
|
||||||
"pt-PT",
|
"pt-PT",
|
||||||
|
"ro",
|
||||||
"ru",
|
"ru",
|
||||||
"sk",
|
"sk",
|
||||||
"sl",
|
"sl",
|
||||||
|
"sq",
|
||||||
"sr",
|
"sr",
|
||||||
"sv-SE",
|
"sv-SE",
|
||||||
"tl",
|
"tl",
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
const autoprefixer = require('autoprefixer');
|
|
||||||
const cssnano = require('cssnano');
|
|
||||||
const mqpacker = require('css-mqpacker');
|
|
||||||
|
|
||||||
const config = require('./server/config');
|
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
plugins: [autoprefixer, mqpacker, cssnano]
|
plugins: {
|
||||||
|
'postcss-import': {},
|
||||||
|
'postcss-cssnext': {},
|
||||||
|
'css-mqpacker': {}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (config.env === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
options.map = { inline: true };
|
options.map = { inline: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Firefox Send is a brand name and should not be localized.
|
# Firefox Send is a brand name and should not be localized.
|
||||||
title = فَيَرفُكس سِنْد
|
title = فَيَرفُكس سِنْد
|
||||||
siteSubtitle = تجربة وِبّيّة
|
siteSubtitle = تجربة وِبّيّة
|
||||||
siteFeedback = الانطباعات
|
siteFeedback = الانطباعات
|
||||||
@@ -11,7 +11,10 @@ uploadPageBrowseButton = اختر ملفّا على حاسوبك
|
|||||||
.title = اختر ملفّا على حاسوبك
|
.title = اختر ملفّا على حاسوبك
|
||||||
uploadPageBrowseButton1 = اختر ملفّا لرفعه
|
uploadPageBrowseButton1 = اختر ملفّا لرفعه
|
||||||
uploadPageMultipleFilesAlert = رفع عدة ملفات (أو رفع مجلد) ليس مدعوما حاليا.
|
uploadPageMultipleFilesAlert = رفع عدة ملفات (أو رفع مجلد) ليس مدعوما حاليا.
|
||||||
|
uploadPageBrowseButtonTitle = ارفع ملفًا
|
||||||
|
uploadingPageProgress = يرفع { $filename } ({ $size })
|
||||||
importingFile = يستورد…
|
importingFile = يستورد…
|
||||||
|
verifyingFile = يتحقق…
|
||||||
encryptingFile = يعمّي…
|
encryptingFile = يعمّي…
|
||||||
decryptingFile = يفك التعمية…
|
decryptingFile = يفك التعمية…
|
||||||
notifyUploadDone = انتهى الرفع.
|
notifyUploadDone = انتهى الرفع.
|
||||||
@@ -21,8 +24,27 @@ uploadingPageCancel = ألغِ الرفع
|
|||||||
uploadCancelNotification = أُلغي الرفع.
|
uploadCancelNotification = أُلغي الرفع.
|
||||||
uploadingPageLargeFileMessage = هذا الملف كبير الحجم وسيأخذ رفعه وقتا. انتظر رجاءً.
|
uploadingPageLargeFileMessage = هذا الملف كبير الحجم وسيأخذ رفعه وقتا. انتظر رجاءً.
|
||||||
uploadingFileNotification = أعلِمني عندما يكتمل الرفع.
|
uploadingFileNotification = أعلِمني عندما يكتمل الرفع.
|
||||||
uploadSvgAlt
|
uploadSuccessConfirmHeader = جاهز للإرسال
|
||||||
|
uploadSvgAlt =
|
||||||
.alt = ارفع
|
.alt = ارفع
|
||||||
|
uploadSuccessTimingHeader = ستنتهي صلاحية الرابط الذي يشير إلى الملف في حال: نُزِّل لأول مرة، أو مرّ ٢٤ ساعة على رفعه.
|
||||||
|
expireInfo = ستنتهي صلاحية رابط الملف بعد { $downloadCount } أو { $timespan }.
|
||||||
|
downloadCount = { $num ->
|
||||||
|
[zero] لا تنزيلات
|
||||||
|
[one] تنزيل واحد
|
||||||
|
[two] تنزيلين
|
||||||
|
[few] { $num } تنزيلات
|
||||||
|
[many] { $num } تنزيلًا
|
||||||
|
*[other] { $num } تنزيل
|
||||||
|
}
|
||||||
|
timespanHours = { $num ->
|
||||||
|
[zero] أقل من ساعة
|
||||||
|
[one] ساعة
|
||||||
|
[two] ساعتين
|
||||||
|
[few] { $num } ساعات
|
||||||
|
[many] { $num } ساعة
|
||||||
|
*[other] { $num } ساعة
|
||||||
|
}
|
||||||
copyUrlFormLabelWithName = انسخ الرابط وشاركه لإرسال الملف: { $filename }
|
copyUrlFormLabelWithName = انسخ الرابط وشاركه لإرسال الملف: { $filename }
|
||||||
copyUrlFormButton = انسخ إلى الحافظة
|
copyUrlFormButton = انسخ إلى الحافظة
|
||||||
.title = انسخ إلى الحافظة
|
.title = انسخ إلى الحافظة
|
||||||
@@ -31,50 +53,84 @@ deleteFileButton = احذف الملف
|
|||||||
.title = احذف الملف
|
.title = احذف الملف
|
||||||
sendAnotherFileLink = أرسل ملفّا آخر
|
sendAnotherFileLink = أرسل ملفّا آخر
|
||||||
.title = أرسل ملفّا آخر
|
.title = أرسل ملفّا آخر
|
||||||
// Alternative text used on the download link/button (indicates an action).
|
# Alternative text used on the download link/button (indicates an action).
|
||||||
downloadAltText
|
downloadAltText =
|
||||||
.alt = نزّل
|
.alt = نزّل
|
||||||
|
downloadsFileList = التنزيلات
|
||||||
|
# Used as header in a column indicating the amount of time left before a
|
||||||
|
# download link expires (e.g. "10h 5m")
|
||||||
|
timeFileList = الوقت
|
||||||
|
# Used as header in a column indicating the number of times a file has been
|
||||||
|
# downloaded
|
||||||
downloadFileName = نزّل { $filename }
|
downloadFileName = نزّل { $filename }
|
||||||
|
downloadFileSize = ({ $size })
|
||||||
unlockInputLabel = أدخل كلمة السر
|
unlockInputLabel = أدخل كلمة السر
|
||||||
unlockInputPlaceholder = كلمة السر
|
unlockInputPlaceholder = كلمة السر
|
||||||
|
unlockButtonLabel = افتح القفل
|
||||||
downloadFileTitle = نزِّل الملف المعمّى
|
downloadFileTitle = نزِّل الملف المعمّى
|
||||||
// Firefox Send is a brand name and should not be localized.
|
# Firefox Send is a brand name and should not be localized.
|
||||||
downloadMessage = يُرسل إليك صديقك ملفا عبر «فَيَرفُكس سِنْد»، وهي خدمة تتيح لك مشاركة الملفات عبر رابط آمن وخاص ومعمّى، حيث تنتهي صلاحياتها تلقائيا لتضمن عدم بقاء ما ترسله إلى الأبد.
|
downloadMessage = يُرسل إليك صديقك ملفا عبر «فَيَرفُكس سِنْد»، وهي خدمة تتيح لك مشاركة الملفات عبر رابط آمن وخاص ومعمّى، حيث تنتهي صلاحياتها تلقائيا لتضمن عدم بقاء ما ترسله إلى الأبد.
|
||||||
// Text and title used on the download link/button (indicates an action).
|
# Text and title used on the download link/button (indicates an action).
|
||||||
downloadButtonLabel = نزّل
|
downloadButtonLabel = نزّل
|
||||||
.title = نزّل
|
.title = نزّل
|
||||||
downloadNotification = لقد اكتمل التنزيل.
|
downloadNotification = لقد اكتمل التنزيل.
|
||||||
downloadFinish = اكتمل التنزيل
|
downloadFinish = اكتمل التنزيل
|
||||||
// This message is displayed when uploading or downloading a file, e.g. "(1,3 MB of 10 MB)".
|
# This message is displayed when uploading or downloading a file, e.g. "(1,3 MB of 10 MB)".
|
||||||
fileSizeProgress = ({ $partialSize } من أصل { $totalSize })
|
fileSizeProgress = ({ $partialSize } من أصل { $totalSize })
|
||||||
// Firefox Send is a brand name and should not be localized.
|
# Firefox Send is a brand name and should not be localized.
|
||||||
sendYourFilesLink = جرِّب «فَيَرفُكس سِنْد»
|
sendYourFilesLink = جرِّب «فَيَرفُكس سِنْد»
|
||||||
|
downloadingPageProgress = ينزّل { $filename } ({ $size })
|
||||||
downloadingPageMessage = رجاء أبقِ هذا اللسان مفتوحا حتى نجلب الملف ونفك تعميته.
|
downloadingPageMessage = رجاء أبقِ هذا اللسان مفتوحا حتى نجلب الملف ونفك تعميته.
|
||||||
errorAltText
|
errorAltText =
|
||||||
.alt = خطأ أثناء الرفع
|
.alt = خطأ أثناء الرفع
|
||||||
errorPageHeader = حدث خطب ما.
|
errorPageHeader = حدث خطب ما.
|
||||||
errorPageMessage = حدث خطب ما أثناء رفع الملف.
|
errorPageMessage = حدث خطب ما أثناء رفع الملف.
|
||||||
errorPageLink = أرسل ملفا آخر
|
errorPageLink = أرسل ملفا آخر
|
||||||
fileTooBig = حجم الملف كبير للغاية لرفعه. يجب أن يكون أصغر من { $size }.
|
fileTooBig = حجم الملف كبير للغاية لرفعه. يجب أن يكون أصغر من { $size }.
|
||||||
|
linkExpiredAlt = انتهت صلاحية الرابط
|
||||||
|
expiredPageHeader = انتهت صلاحية هذا الرابط أو لم يكن موجودا في المقام الأول!
|
||||||
notSupportedHeader = متصفحك غير مدعوم.
|
notSupportedHeader = متصفحك غير مدعوم.
|
||||||
// Firefox Send is a brand name and should not be localized.
|
# Firefox Send is a brand name and should not be localized.
|
||||||
notSupportedDetail = للأسف فإن متصفحك لا يدعم تقنية الوِب التي يعتمد عليها «فَيَرفُكس سِنْد». عليك تجربة متصفح آخر، ونحن ننصحك بِفَيَرفُكس!
|
notSupportedDetail = للأسف فإن متصفحك لا يدعم تقنية الوِب التي يعتمد عليها «فَيَرفُكس سِنْد». عليك تجربة متصفح آخر، ونحن ننصحك بِفَيَرفُكس!
|
||||||
notSupportedLink = لماذا متصفحي غير مدعوم؟
|
notSupportedLink = لماذا متصفحي غير مدعوم؟
|
||||||
notSupportedOutdatedDetail = للأسف فإن إصدارة فَيَرفُكس هذه لا تدعم تقنية الوِب التي يعتمد عليها «فَيَرفُكس سِنْد». عليك تحديث متصفحك.
|
notSupportedOutdatedDetail = للأسف فإن إصدارة فَيَرفُكس هذه لا تدعم تقنية الوِب التي يعتمد عليها «فَيَرفُكس سِنْد». عليك تحديث متصفحك.
|
||||||
updateFirefox = حدّث فَيَرفُكس
|
updateFirefox = حدّث فَيَرفُكس
|
||||||
|
downloadFirefoxButtonSub = تنزيل مجاني
|
||||||
|
uploadedFile = ملف
|
||||||
copyFileList = انسخ الرابط
|
copyFileList = انسخ الرابط
|
||||||
|
# expiryFileList is used as a column header
|
||||||
|
expiryFileList = ينتهي في
|
||||||
deleteFileList = احذف
|
deleteFileList = احذف
|
||||||
|
nevermindButton = لا بأس
|
||||||
legalHeader = الشروط والخصوصية
|
legalHeader = الشروط والخصوصية
|
||||||
|
legalNoticeTestPilot = «فَيَرفُكس سِنْد» جزء من اختبار تجريبي حاليًا و يخضع <a>لبنود خدمة</a> الاختبار التجريبي و <a>تنويه الخصوصية</a>. يمكنك التعرف على مزيد من المعلومات حول هذه التجربة وجمع البيانات<a>هنا</a>.
|
||||||
|
legalNoticeMozilla = يخضع استخدام موقع «فَيَرفُكس سِنْد» إلى<a>تنويه خصوصية المواقع</a> و <a>بنود خدمة المواقع</a>.
|
||||||
deletePopupText = أأحذف هذا الملف؟
|
deletePopupText = أأحذف هذا الملف؟
|
||||||
deletePopupYes = نعم
|
deletePopupYes = نعم
|
||||||
deletePopupCancel = ألغِ
|
deletePopupCancel = ألغِ
|
||||||
deleteButtonHover
|
deleteButtonHover =
|
||||||
.title = احذف
|
.title = احذف
|
||||||
copyUrlHover
|
copyUrlHover =
|
||||||
.title = انسخ الرابط
|
.title = انسخ الرابط
|
||||||
|
footerLinkLegal = القانونية
|
||||||
|
# Test Pilot is a proper name and should not be localized.
|
||||||
|
footerLinkAbout = حول الاختبار التجريبي
|
||||||
|
footerLinkPrivacy = الخصوصية
|
||||||
footerLinkTerms = الشروط
|
footerLinkTerms = الشروط
|
||||||
footerLinkCookies = الكعكات
|
footerLinkCookies = الكعكات
|
||||||
requirePasswordCheckbox = اطلب كلمة سر لتنزيل هذا الملف
|
requirePasswordCheckbox = اطلب كلمة سر لتنزيل هذا الملف
|
||||||
addPasswordButton = أضِف كلمة سر
|
addPasswordButton = أضِف كلمة سر
|
||||||
// This label is followed by the password needed to download a file
|
changePasswordButton = غيّر
|
||||||
|
passwordTryAgain = كلمة السر خاطئة. أعِد المحاولة.
|
||||||
|
# This label is followed by the password needed to download a file
|
||||||
passwordResult = كلمة السر: { $password }
|
passwordResult = كلمة السر: { $password }
|
||||||
|
reportIPInfringement = أبلغ عن انتهاك للملكية الفكرية
|
||||||
|
javascriptRequired = يتطلب فَيَرفُكس سِنْد جافاسكربت
|
||||||
|
whyJavascript = لماذا يتطلب فَيَرفُكس سِنْد جافاسكربت؟
|
||||||
|
enableJavascript = رجاء فعّل جافاسكربت ثم أعد المحاولة.
|
||||||
|
# A short representation of a countdown timer containing the number of hours and minutes remaining as digits, example "13h 47m"
|
||||||
|
expiresHoursMinutes = { $hours }س { $minutes }د
|
||||||
|
# A short representation of a countdown timer containing the number of minutes remaining as digits, example "56m"
|
||||||
|
expiresMinutes = { $minutes }د
|
||||||
|
# A short status message shown when a password is successfully set
|
||||||
|
passwordIsSet = ضُبطت كلمة السر
|
||||||
|
|||||||
@@ -25,6 +25,15 @@ uploadingFileNotification = Yükləmə bitdiyində xəbər ver.
|
|||||||
uploadSuccessConfirmHeader = Göndərməyə hazır
|
uploadSuccessConfirmHeader = Göndərməyə hazır
|
||||||
uploadSvgAlt = Yüklə
|
uploadSvgAlt = Yüklə
|
||||||
uploadSuccessTimingHeader = Faylınızın keçidinin 1 endirmədən və ya 24 saatdan sonra vaxtı çıxacaq.
|
uploadSuccessTimingHeader = Faylınızın keçidinin 1 endirmədən və ya 24 saatdan sonra vaxtı çıxacaq.
|
||||||
|
expireInfo = Faylınız üçün keçidin vaxtı { $downloadCount } sonra və ya { $timespan } tarixində keçəcək.
|
||||||
|
downloadCount = { $num ->
|
||||||
|
[one] 1 endirmə
|
||||||
|
*[other] { $num } endirmə
|
||||||
|
}
|
||||||
|
timespanHours = { $num ->
|
||||||
|
[one] 1 saat
|
||||||
|
*[other] { $num } saat
|
||||||
|
}
|
||||||
copyUrlFormLabelWithName = Faylınızı göndərmək üçün keçidi köçürün: { $filename }
|
copyUrlFormLabelWithName = Faylınızı göndərmək üçün keçidi köçürün: { $filename }
|
||||||
copyUrlFormButton = Buferə köçür
|
copyUrlFormButton = Buferə köçür
|
||||||
copiedUrl = Köçürüldü!
|
copiedUrl = Köçürüldü!
|
||||||
@@ -32,6 +41,12 @@ deleteFileButton = Faylı sil
|
|||||||
sendAnotherFileLink = Başqa fayl göndər
|
sendAnotherFileLink = Başqa fayl göndər
|
||||||
// Alternative text used on the download link/button (indicates an action).
|
// Alternative text used on the download link/button (indicates an action).
|
||||||
downloadAltText = Endir
|
downloadAltText = Endir
|
||||||
|
downloadsFileList = Endirmələr
|
||||||
|
// Used as header in a column indicating the amount of time left before a
|
||||||
|
// download link expires (e.g. "10h 5m")
|
||||||
|
timeFileList = Vaxt
|
||||||
|
// Used as header in a column indicating the number of times a file has been
|
||||||
|
// downloaded
|
||||||
downloadFileName = { $filename } faylını endir
|
downloadFileName = { $filename } faylını endir
|
||||||
downloadFileSize = ({ $size })
|
downloadFileSize = ({ $size })
|
||||||
unlockInputLabel = Parol daxil edin
|
unlockInputLabel = Parol daxil edin
|
||||||
@@ -86,4 +101,15 @@ footerLinkTerms = Şərtlər
|
|||||||
footerLinkCookies = Çərəzlər
|
footerLinkCookies = Çərəzlər
|
||||||
requirePasswordCheckbox = Bu faylı endirmək üçün parol tələb et
|
requirePasswordCheckbox = Bu faylı endirmək üçün parol tələb et
|
||||||
addPasswordButton = Parol əlavə et
|
addPasswordButton = Parol əlavə et
|
||||||
incorrectPassword = Xətalı parol. Təkrar yoxlayın.
|
changePasswordButton = Dəyişdir
|
||||||
|
passwordTryAgain = Səhv parol. Təkrar yoxlayın.
|
||||||
|
// This label is followed by the password needed to download a file
|
||||||
|
passwordResult = Parol: { $password }
|
||||||
|
reportIPInfringement = Əqli-mülkiyyət pozuntusu bildir
|
||||||
|
javascriptRequired = Firefox Send üçün JavaScript lazımdır
|
||||||
|
whyJavascript = Firefox Send niyə JavaScript tələb edir?
|
||||||
|
enableJavascript = Lütfən JavaScript-i aktiv edib təkrar yoxlayın.
|
||||||
|
// A short representation of a countdown timer containing the number of hours and minutes remaining as digits, example "13h 47m"
|
||||||
|
expiresHoursMinutes = { $hours } saat { $minutes } dəq
|
||||||
|
// A short representation of a countdown timer containing the number of minutes remaining as digits, example "56m"
|
||||||
|
expiresMinutes = { $minutes } dəq
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
title = Firefox Send
|
title = Firefox Send
|
||||||
siteSubtitle = ওয়েব গবেষণা
|
siteSubtitle = ওয়েব গবেষণা
|
||||||
siteFeedback = প্রতিক্রিয়া
|
siteFeedback = প্রতিক্রিয়া
|
||||||
|
uploadPageHeader = ব্যক্তিগত, এনক্রিপ্ট করা ফাইল শেয়ারিং
|
||||||
uploadPageLearnMore = আরও জানুন
|
uploadPageLearnMore = আরও জানুন
|
||||||
uploadPageBrowseButton = আপনার কম্পিউটারে ফাইল নির্বাচন করুন
|
uploadPageBrowseButton = আপনার কম্পিউটারে ফাইল নির্বাচন করুন
|
||||||
uploadPageBrowseButtonTitle = ফাইল আপলোড
|
uploadPageBrowseButtonTitle = ফাইল আপলোড
|
||||||
@@ -14,14 +15,31 @@ uploadingPageCancel = আপলোড বাতিল করুন
|
|||||||
uploadCancelNotification = আপনার অাপলোড বাতিল করা হয়েছে।
|
uploadCancelNotification = আপনার অাপলোড বাতিল করা হয়েছে।
|
||||||
uploadSuccessConfirmHeader = পাঠানোর জন্য প্রস্তুত
|
uploadSuccessConfirmHeader = পাঠানোর জন্য প্রস্তুত
|
||||||
uploadSvgAlt = আপলোড
|
uploadSvgAlt = আপলোড
|
||||||
|
downloadCount = { $num ->
|
||||||
|
[one] 1 ডাউনলোড
|
||||||
|
*[other] { $num } ডাউনলোডগুলো
|
||||||
|
}
|
||||||
|
timespanHours = { $num ->
|
||||||
|
[one] 1 ঘন্টা
|
||||||
|
*[other] { $num } ঘন্টা
|
||||||
|
}
|
||||||
copyUrlFormButton = ক্লিপবোর্ডে কপি করুন
|
copyUrlFormButton = ক্লিপবোর্ডে কপি করুন
|
||||||
copiedUrl = কপি করা হয়েছে!
|
copiedUrl = কপি করা হয়েছে!
|
||||||
deleteFileButton = ফাইল মুছুন
|
deleteFileButton = ফাইল মুছুন
|
||||||
sendAnotherFileLink = আরেকটি ফাইল পাঠান
|
sendAnotherFileLink = আরেকটি ফাইল পাঠান
|
||||||
// Alternative text used on the download link/button (indicates an action).
|
// Alternative text used on the download link/button (indicates an action).
|
||||||
downloadAltText = ডাউনলোড
|
downloadAltText = ডাউনলোড
|
||||||
|
downloadsFileList = ডাউনলোডগুলো
|
||||||
|
// Used as header in a column indicating the amount of time left before a
|
||||||
|
// download link expires (e.g. "10h 5m")
|
||||||
|
timeFileList = সময়
|
||||||
|
// Used as header in a column indicating the number of times a file has been
|
||||||
|
// downloaded
|
||||||
downloadFileName = ডাউনলোড { $filename }
|
downloadFileName = ডাউনলোড { $filename }
|
||||||
downloadFileSize = ({ $size })
|
downloadFileSize = ({ $size })
|
||||||
|
unlockInputLabel = পাসওয়ার্ড লিখুন
|
||||||
|
unlockInputPlaceholder = পাসওয়ার্ড
|
||||||
|
unlockButtonLabel = আনলক করুন
|
||||||
// Text and title used on the download link/button (indicates an action).
|
// Text and title used on the download link/button (indicates an action).
|
||||||
downloadButtonLabel = ডাউনলোড
|
downloadButtonLabel = ডাউনলোড
|
||||||
downloadNotification = আপনার ডাউনলোড সম্পন্ন হয়েছে।
|
downloadNotification = আপনার ডাউনলোড সম্পন্ন হয়েছে।
|
||||||
@@ -29,6 +47,8 @@ downloadFinish = ডাউনলোড সম্পন্ন
|
|||||||
errorAltText = আপালোডে ত্রুটি
|
errorAltText = আপালোডে ত্রুটি
|
||||||
errorPageHeader = কোন সমস্যা হয়েছে!
|
errorPageHeader = কোন সমস্যা হয়েছে!
|
||||||
errorPageLink = আরেকটি ফাইল পাঠান
|
errorPageLink = আরেকটি ফাইল পাঠান
|
||||||
|
expiredPageHeader = এই লিঙ্কটির মেয়াদ শেষ হয়ে গেছে বা কখনই প্রথম স্থানে নেই!
|
||||||
|
notSupportedHeader = আপনার ব্রাউজার সমর্থিত নয়।
|
||||||
updateFirefox = Firefox হালনাগাদ করুন
|
updateFirefox = Firefox হালনাগাদ করুন
|
||||||
downloadFirefoxButtonSub = বিনামূল্যে ডাউনলোড
|
downloadFirefoxButtonSub = বিনামূল্যে ডাউনলোড
|
||||||
uploadedFile = ফাইল
|
uploadedFile = ফাইল
|
||||||
@@ -49,3 +69,4 @@ footerLinkAbout = Test Pilot পরিচিতি
|
|||||||
footerLinkPrivacy = গোপনীয়তা
|
footerLinkPrivacy = গোপনীয়তা
|
||||||
footerLinkTerms = শর্তাবলী
|
footerLinkTerms = শর্তাবলী
|
||||||
footerLinkCookies = কুকি
|
footerLinkCookies = কুকি
|
||||||
|
changePasswordButton = পরিবর্তন
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Firefox Send is a brand name and should not be localized.
|
# Firefox Send is a brand name and should not be localized.
|
||||||
title = Firefox Send
|
title = Firefox Send
|
||||||
siteSubtitle = web eksperiment
|
siteSubtitle = web eksperiment
|
||||||
siteFeedback = Povratne informacije
|
siteFeedback = Povratne informacije
|
||||||
@@ -25,28 +25,45 @@ uploadingFileNotification = Obavijesti me kada otpremanje bude gotovo.
|
|||||||
uploadSuccessConfirmHeader = Spremno za slanje
|
uploadSuccessConfirmHeader = Spremno za slanje
|
||||||
uploadSvgAlt = Otpremi
|
uploadSvgAlt = Otpremi
|
||||||
uploadSuccessTimingHeader = Veza prema vašoj datoteci će isteći nakon prvog preuzimanja ili za 24 sata.
|
uploadSuccessTimingHeader = Veza prema vašoj datoteci će isteći nakon prvog preuzimanja ili za 24 sata.
|
||||||
|
expireInfo = Link za vašu datoteku će isteći nakon { $downloadCount } ili { $timespan }.
|
||||||
|
downloadCount = { $num ->
|
||||||
|
[one] 1 preuzimanja
|
||||||
|
[few] { $num } preuzimanja
|
||||||
|
*[other] { $num } preuzimanja
|
||||||
|
}
|
||||||
|
timespanHours = { $num ->
|
||||||
|
[one] 1 sat
|
||||||
|
[few] { $num } sata
|
||||||
|
*[other] { $num } sati
|
||||||
|
}
|
||||||
copyUrlFormLabelWithName = Iskopirajte i podijelite vezu da biste poslali datoteku: { $filename }
|
copyUrlFormLabelWithName = Iskopirajte i podijelite vezu da biste poslali datoteku: { $filename }
|
||||||
copyUrlFormButton = Kopiraj u međuspremnik
|
copyUrlFormButton = Kopiraj u međuspremnik
|
||||||
copiedUrl = Kopirano!
|
copiedUrl = Kopirano!
|
||||||
deleteFileButton = Izbriši datoteku
|
deleteFileButton = Izbriši datoteku
|
||||||
sendAnotherFileLink = Pošalji drugu datoteku
|
sendAnotherFileLink = Pošalji drugu datoteku
|
||||||
// Alternative text used on the download link/button (indicates an action).
|
# Alternative text used on the download link/button (indicates an action).
|
||||||
downloadAltText = Preuzmi
|
downloadAltText = Preuzmi
|
||||||
|
downloadsFileList = Preuzimanja
|
||||||
|
# Used as header in a column indicating the amount of time left before a
|
||||||
|
# download link expires (e.g. "10h 5m")
|
||||||
|
timeFileList = Vrijeme
|
||||||
|
# Used as header in a column indicating the number of times a file has been
|
||||||
|
# downloaded
|
||||||
downloadFileName = Preuzmi { $filename }
|
downloadFileName = Preuzmi { $filename }
|
||||||
downloadFileSize = ({ $size })
|
downloadFileSize = ({ $size })
|
||||||
unlockInputLabel = Unesite lozinku
|
unlockInputLabel = Unesite lozinku
|
||||||
unlockInputPlaceholder = Lozinka
|
unlockInputPlaceholder = Lozinka
|
||||||
unlockButtonLabel = Otključaj
|
unlockButtonLabel = Otključaj
|
||||||
downloadFileTitle = Preuzmi šifrovanu datoteku
|
downloadFileTitle = Preuzmi šifrovanu datoteku
|
||||||
// Firefox Send is a brand name and should not be localized.
|
# Firefox Send is a brand name and should not be localized.
|
||||||
downloadMessage = Vaš prijatelj vam je poslao datoteku preko usluge Firefox Send koja vam omogućava da dijelite datoteke preko sigurne, privatne i šifrovane veze koja samostalno ističe da vaše stvari ne ostanu zauvijek na internetu.
|
downloadMessage = Vaš prijatelj vam je poslao datoteku preko usluge Firefox Send koja vam omogućava da dijelite datoteke preko sigurne, privatne i šifrovane veze koja samostalno ističe da vaše stvari ne ostanu zauvijek na internetu.
|
||||||
// Text and title used on the download link/button (indicates an action).
|
# Text and title used on the download link/button (indicates an action).
|
||||||
downloadButtonLabel = Preuzmi
|
downloadButtonLabel = Preuzmi
|
||||||
downloadNotification = Vaše preuzimanje je završeno.
|
downloadNotification = Vaše preuzimanje je završeno.
|
||||||
downloadFinish = Preuzimanje završeno
|
downloadFinish = Preuzimanje završeno
|
||||||
// This message is displayed when uploading or downloading a file, e.g. "(1,3 MB of 10 MB)".
|
# This message is displayed when uploading or downloading a file, e.g. "(1,3 MB of 10 MB)".
|
||||||
fileSizeProgress = ({ $partialSize } od { $totalSize })
|
fileSizeProgress = ({ $partialSize } od { $totalSize })
|
||||||
// Firefox Send is a brand name and should not be localized.
|
# Firefox Send is a brand name and should not be localized.
|
||||||
sendYourFilesLink = Probajte Firefox Send
|
sendYourFilesLink = Probajte Firefox Send
|
||||||
downloadingPageProgress = Preuzimanje { $filename } ({ $size })
|
downloadingPageProgress = Preuzimanje { $filename } ({ $size })
|
||||||
downloadingPageMessage = Ostavite ovaj tab otvorenim dok ne dobavimo vašu datoteku i dok je ne dešifrujemo.
|
downloadingPageMessage = Ostavite ovaj tab otvorenim dok ne dobavimo vašu datoteku i dok je ne dešifrujemo.
|
||||||
@@ -58,7 +75,7 @@ fileTooBig = Ta datoteka je prevelika za otpremanje. Treba biti manja od { $size
|
|||||||
linkExpiredAlt = Veza istekla
|
linkExpiredAlt = Veza istekla
|
||||||
expiredPageHeader = Veza je istekla ili nikad nije postojala!
|
expiredPageHeader = Veza je istekla ili nikad nije postojala!
|
||||||
notSupportedHeader = Vaš pretraživač nije podržan.
|
notSupportedHeader = Vaš pretraživač nije podržan.
|
||||||
// Firefox Send is a brand name and should not be localized.
|
# Firefox Send is a brand name and should not be localized.
|
||||||
notSupportedDetail = Ovaj pretraživač nažalost ne podržava web tehnologiju koja omogućava Firefox Send. Trebate probati drugi pretraživač. Preporučujemo Firefox!
|
notSupportedDetail = Ovaj pretraživač nažalost ne podržava web tehnologiju koja omogućava Firefox Send. Trebate probati drugi pretraživač. Preporučujemo Firefox!
|
||||||
notSupportedLink = Zašto moj pretraživač nije podržan?
|
notSupportedLink = Zašto moj pretraživač nije podržan?
|
||||||
notSupportedOutdatedDetail = Nažalost ova verzija Firefoxa ne podržava web tehnologiju koja omogućava Firefox Send. Morate ažurirati vaš pretraživač.
|
notSupportedOutdatedDetail = Nažalost ova verzija Firefoxa ne podržava web tehnologiju koja omogućava Firefox Send. Morate ažurirati vaš pretraživač.
|
||||||
@@ -66,7 +83,7 @@ updateFirefox = Ažuriraj Firefox
|
|||||||
downloadFirefoxButtonSub = Besplatno preuzimanje
|
downloadFirefoxButtonSub = Besplatno preuzimanje
|
||||||
uploadedFile = Datoteka
|
uploadedFile = Datoteka
|
||||||
copyFileList = Kopiraj URL
|
copyFileList = Kopiraj URL
|
||||||
// expiryFileList is used as a column header
|
# expiryFileList is used as a column header
|
||||||
expiryFileList = Ističe za
|
expiryFileList = Ističe za
|
||||||
deleteFileList = Izbriši
|
deleteFileList = Izbriši
|
||||||
nevermindButton = Zanemari
|
nevermindButton = Zanemari
|
||||||
@@ -79,13 +96,26 @@ deletePopupCancel = Otkaži
|
|||||||
deleteButtonHover = Izbriši
|
deleteButtonHover = Izbriši
|
||||||
copyUrlHover = Kopiraj URL
|
copyUrlHover = Kopiraj URL
|
||||||
footerLinkLegal = Pravno
|
footerLinkLegal = Pravno
|
||||||
// Test Pilot is a proper name and should not be localized.
|
# Test Pilot is a proper name and should not be localized.
|
||||||
footerLinkAbout = O Test Pilotu
|
footerLinkAbout = O Test Pilotu
|
||||||
footerLinkPrivacy = Privatnost
|
footerLinkPrivacy = Privatnost
|
||||||
footerLinkTerms = Uslovi
|
footerLinkTerms = Uslovi
|
||||||
footerLinkCookies = Kolačići
|
footerLinkCookies = Kolačići
|
||||||
requirePasswordCheckbox = Zahtjevaj lozinku za preuzimanje ove datoteke
|
requirePasswordCheckbox = Zahtjevaj lozinku za preuzimanje ove datoteke
|
||||||
addPasswordButton = Dodaj lozinku
|
addPasswordButton = Dodaj lozinku
|
||||||
|
changePasswordButton = Promijeni
|
||||||
passwordTryAgain = Netačna lozinka. Pokušajte ponovo.
|
passwordTryAgain = Netačna lozinka. Pokušajte ponovo.
|
||||||
// This label is followed by the password needed to download a file
|
reportIPInfringement = Prijavite IP prekršaj
|
||||||
passwordResult = Lozinka: { $password }
|
javascriptRequired = Firefox Send zahtjeva JavaScript
|
||||||
|
whyJavascript = Zašto Firefox Send zahtjeva JavaScript?
|
||||||
|
enableJavascript = Molimo omogućite JavaScript i pokušajte ponovo.
|
||||||
|
# A short representation of a countdown timer containing the number of hours and minutes remaining as digits, example "13h 47m"
|
||||||
|
expiresHoursMinutes = { $hours }h { $minutes }m
|
||||||
|
# A short representation of a countdown timer containing the number of minutes remaining as digits, example "56m"
|
||||||
|
expiresMinutes = { $minutes }m
|
||||||
|
# A short status message shown when a password is successfully set
|
||||||
|
passwordIsSet = Lozinka postavljena
|
||||||
|
# A short status message shown when the user enters a long password
|
||||||
|
maxPasswordLength = Maksimalna veličina lozinke: { $length }
|
||||||
|
# A short status message shown when there was an error setting the password
|
||||||
|
passwordSetError = Ova lozinka se ne može postaviti
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Firefox Send is a brand name and should not be localized.
|
# Firefox Send is a brand name and should not be localized.
|
||||||
title = Firefox Send
|
title = Firefox Send
|
||||||
siteSubtitle = experiment web
|
siteSubtitle = experiment web
|
||||||
siteFeedback = Comentaris
|
siteFeedback = Comentaris
|
||||||
@@ -25,24 +25,43 @@ uploadingFileNotification = Notifica'm quan s'acabi de pujar.
|
|||||||
uploadSuccessConfirmHeader = Llest per enviar
|
uploadSuccessConfirmHeader = Llest per enviar
|
||||||
uploadSvgAlt = Puja
|
uploadSvgAlt = Puja
|
||||||
uploadSuccessTimingHeader = L'enllaç al fitxer caducarà quan es baixi una vegada o d'aquí 24 hores.
|
uploadSuccessTimingHeader = L'enllaç al fitxer caducarà quan es baixi una vegada o d'aquí 24 hores.
|
||||||
|
expireInfo = L'enllaç al fitxer caducarà en fer { $downloadCount } o d'aquí { $timespan }.
|
||||||
|
downloadCount = { $num ->
|
||||||
|
[one] 1 baixada
|
||||||
|
*[other] { $num } baixades
|
||||||
|
}
|
||||||
|
timespanHours = { $num ->
|
||||||
|
[one] 1 hora
|
||||||
|
*[other] { $num } hores
|
||||||
|
}
|
||||||
copyUrlFormLabelWithName = Copieu l'enllaç i compartiu-lo per enviar el fitxer: { $filename }
|
copyUrlFormLabelWithName = Copieu l'enllaç i compartiu-lo per enviar el fitxer: { $filename }
|
||||||
copyUrlFormButton = Copia al porta-retalls
|
copyUrlFormButton = Copia al porta-retalls
|
||||||
copiedUrl = Copiat!
|
copiedUrl = Copiat!
|
||||||
deleteFileButton = Suprimeix el fitxer
|
deleteFileButton = Suprimeix el fitxer
|
||||||
sendAnotherFileLink = Envieu un altre fitxer
|
sendAnotherFileLink = Envieu un altre fitxer
|
||||||
// Alternative text used on the download link/button (indicates an action).
|
# Alternative text used on the download link/button (indicates an action).
|
||||||
downloadAltText = Baixa
|
downloadAltText = Baixa
|
||||||
|
downloadsFileList = Baixades
|
||||||
|
# Used as header in a column indicating the amount of time left before a
|
||||||
|
# download link expires (e.g. "10h 5m")
|
||||||
|
timeFileList = Temps
|
||||||
|
# Used as header in a column indicating the number of times a file has been
|
||||||
|
# downloaded
|
||||||
downloadFileName = Baixeu { $filename }
|
downloadFileName = Baixeu { $filename }
|
||||||
downloadFileSize = ({ $size })
|
downloadFileSize = ({ $size })
|
||||||
// Firefox Send is a brand name and should not be localized.
|
unlockInputLabel = Introduïu la contrasenya
|
||||||
|
unlockInputPlaceholder = Contrasenya
|
||||||
|
unlockButtonLabel = Desbloca
|
||||||
|
downloadFileTitle = Baixa el fitxer xifrat
|
||||||
|
# Firefox Send is a brand name and should not be localized.
|
||||||
downloadMessage = Un amic us ha enviat un fitxer amb el Firefox Send, un servei que permet compartir fitxers mitjançant un enllaç segur, privat i xifrat que caduca automàticament per tal que les vostres dades no es conservin a Internet per sempre.
|
downloadMessage = Un amic us ha enviat un fitxer amb el Firefox Send, un servei que permet compartir fitxers mitjançant un enllaç segur, privat i xifrat que caduca automàticament per tal que les vostres dades no es conservin a Internet per sempre.
|
||||||
// Text and title used on the download link/button (indicates an action).
|
# Text and title used on the download link/button (indicates an action).
|
||||||
downloadButtonLabel = Baixa
|
downloadButtonLabel = Baixa
|
||||||
downloadNotification = La baixada ha acabat.
|
downloadNotification = La baixada ha acabat.
|
||||||
downloadFinish = Ha acabat la baixada
|
downloadFinish = Ha acabat la baixada
|
||||||
// This message is displayed when uploading or downloading a file, e.g. "(1,3 MB of 10 MB)".
|
# This message is displayed when uploading or downloading a file, e.g. "(1,3 MB of 10 MB)".
|
||||||
fileSizeProgress = ({ $partialSize } de { $totalSize })
|
fileSizeProgress = ({ $partialSize } de { $totalSize })
|
||||||
// Firefox Send is a brand name and should not be localized.
|
# Firefox Send is a brand name and should not be localized.
|
||||||
sendYourFilesLink = Proveu el Firefox Send
|
sendYourFilesLink = Proveu el Firefox Send
|
||||||
downloadingPageProgress = S'està baixant { $filename } ({ $size })
|
downloadingPageProgress = S'està baixant { $filename } ({ $size })
|
||||||
downloadingPageMessage = Deixeu aquesta pestanya oberta per tal que el fitxer es pugui baixar i desxifrar.
|
downloadingPageMessage = Deixeu aquesta pestanya oberta per tal que el fitxer es pugui baixar i desxifrar.
|
||||||
@@ -54,7 +73,7 @@ fileTooBig = Aquest fitxer és massa gros per pujar-lo. Ha de tenir menys de { $
|
|||||||
linkExpiredAlt = L'enllaç ha caducat
|
linkExpiredAlt = L'enllaç ha caducat
|
||||||
expiredPageHeader = Aquest enllaç ha caducat o no existeix.
|
expiredPageHeader = Aquest enllaç ha caducat o no existeix.
|
||||||
notSupportedHeader = El vostre navegador no és compatible.
|
notSupportedHeader = El vostre navegador no és compatible.
|
||||||
// Firefox Send is a brand name and should not be localized.
|
# Firefox Send is a brand name and should not be localized.
|
||||||
notSupportedDetail = Aquest navegador no admet la tecnologia web amb què funciona el Firefox Send. Haureu d'utilitzar un altre navegador. Us recomanem el Firefox!
|
notSupportedDetail = Aquest navegador no admet la tecnologia web amb què funciona el Firefox Send. Haureu d'utilitzar un altre navegador. Us recomanem el Firefox!
|
||||||
notSupportedLink = Per què el meu navegador no és compatible?
|
notSupportedLink = Per què el meu navegador no és compatible?
|
||||||
notSupportedOutdatedDetail = Aquesta versió del Firefox no admet la tecnologia web amb què funciona el Firefox Send. Haureu d'actualitzar el navegador.
|
notSupportedOutdatedDetail = Aquesta versió del Firefox no admet la tecnologia web amb què funciona el Firefox Send. Haureu d'actualitzar el navegador.
|
||||||
@@ -62,7 +81,7 @@ updateFirefox = Actualitza el Firefox
|
|||||||
downloadFirefoxButtonSub = Baixada gratuïta
|
downloadFirefoxButtonSub = Baixada gratuïta
|
||||||
uploadedFile = Fitxer
|
uploadedFile = Fitxer
|
||||||
copyFileList = Copia l'URL
|
copyFileList = Copia l'URL
|
||||||
// expiryFileList is used as a column header
|
# expiryFileList is used as a column header
|
||||||
expiryFileList = Caduca d'aquí
|
expiryFileList = Caduca d'aquí
|
||||||
deleteFileList = Suprimeix
|
deleteFileList = Suprimeix
|
||||||
nevermindButton = No, gràcies
|
nevermindButton = No, gràcies
|
||||||
@@ -75,8 +94,24 @@ deletePopupCancel = Cancel·la
|
|||||||
deleteButtonHover = Suprimeix
|
deleteButtonHover = Suprimeix
|
||||||
copyUrlHover = Copia l'URL
|
copyUrlHover = Copia l'URL
|
||||||
footerLinkLegal = Avís legal
|
footerLinkLegal = Avís legal
|
||||||
// Test Pilot is a proper name and should not be localized.
|
# Test Pilot is a proper name and should not be localized.
|
||||||
footerLinkAbout = Quant al Test Pilot
|
footerLinkAbout = Quant al Test Pilot
|
||||||
footerLinkPrivacy = Privadesa
|
footerLinkPrivacy = Privadesa
|
||||||
footerLinkTerms = Condicions d'ús
|
footerLinkTerms = Condicions d'ús
|
||||||
footerLinkCookies = Galetes
|
footerLinkCookies = Galetes
|
||||||
|
requirePasswordCheckbox = Sol·licita una contrasenya per baixar aquest fitxer
|
||||||
|
addPasswordButton = Afegeix una contrasenya
|
||||||
|
changePasswordButton = Canvia
|
||||||
|
passwordTryAgain = La contrasenya és incorrecta. Torneu-ho a provar.
|
||||||
|
# This label is followed by the password needed to download a file
|
||||||
|
passwordResult = Contrasenya: { $password }
|
||||||
|
reportIPInfringement = Denuncieu una infracció de propietat intel·lectual
|
||||||
|
javascriptRequired = El Firefox Send necessita JavaScript
|
||||||
|
whyJavascript = Per què el Firefox Send necessita JavaScript?
|
||||||
|
enableJavascript = Activeu el JavaScript i torneu-ho a provar.
|
||||||
|
# A short representation of a countdown timer containing the number of hours and minutes remaining as digits, example "13h 47m"
|
||||||
|
expiresHoursMinutes = { $hours } h { $minutes } min
|
||||||
|
# A short representation of a countdown timer containing the number of minutes remaining as digits, example "56m"
|
||||||
|
expiresMinutes = { $minutes } min
|
||||||
|
# A short status message shown when a password is successfully set
|
||||||
|
passwordIsSet = S'ha definit la contrasenya
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Firefox Send is a brand name and should not be localized.
|
# Firefox Send is a brand name and should not be localized.
|
||||||
title = Firefox Send
|
title = Firefox Send
|
||||||
siteSubtitle = ajk'amaya'l solna'onem
|
siteSubtitle = ajk'amaya'l solna'onem
|
||||||
siteFeedback = Rutzijol
|
siteFeedback = Rutzijol
|
||||||
@@ -25,28 +25,43 @@ uploadingFileNotification = Tiya' pe rutzijol chwe toq xtitz'aqät rujotob'axik.
|
|||||||
uploadSuccessConfirmHeader = Ütz chik richin Nitaq
|
uploadSuccessConfirmHeader = Ütz chik richin Nitaq
|
||||||
uploadSvgAlt = Tijotob'äx
|
uploadSvgAlt = Tijotob'äx
|
||||||
uploadSuccessTimingHeader = Ri ruximonel yakb'äl xtik'is ruq'ijul toq xtiqasäx jumul o pa 24 ramaj.
|
uploadSuccessTimingHeader = Ri ruximonel yakb'äl xtik'is ruq'ijul toq xtiqasäx jumul o pa 24 ramaj.
|
||||||
|
expireInfo = Ri ruximöy ayakb'al xtik'is ruq'ijul chi rij ri { $downloadCount } o { $timespan }.
|
||||||
|
downloadCount = { $num ->
|
||||||
|
[one] 1 qasanïk
|
||||||
|
*[other] { $num } taq qasanïk
|
||||||
|
}
|
||||||
|
timespanHours = { $num ->
|
||||||
|
[one] 1 ramaj
|
||||||
|
*[other] { $num } taq ramaj
|
||||||
|
}
|
||||||
copyUrlFormLabelWithName = Tiwachib'ëx chuqa' tikomonïx ri ximonel richin nitaq ri ayakb'äl: { $filename }
|
copyUrlFormLabelWithName = Tiwachib'ëx chuqa' tikomonïx ri ximonel richin nitaq ri ayakb'äl: { $filename }
|
||||||
copyUrlFormButton = Tiwachib'ëx pa molwuj
|
copyUrlFormButton = Tiwachib'ëx pa molwuj
|
||||||
copiedUrl = ¡Xwachib'ëx!
|
copiedUrl = ¡Xwachib'ëx!
|
||||||
deleteFileButton = Tiyuj yakb'äl
|
deleteFileButton = Tiyuj yakb'äl
|
||||||
sendAnotherFileLink = Titaq jun chik yakb'äl
|
sendAnotherFileLink = Titaq jun chik yakb'äl
|
||||||
// Alternative text used on the download link/button (indicates an action).
|
# Alternative text used on the download link/button (indicates an action).
|
||||||
downloadAltText = Tiqasäx
|
downloadAltText = Tiqasäx
|
||||||
|
downloadsFileList = Taq qasanïk
|
||||||
|
# Used as header in a column indicating the amount of time left before a
|
||||||
|
# download link expires (e.g. "10h 5m")
|
||||||
|
timeFileList = Q'ijul
|
||||||
|
# Used as header in a column indicating the number of times a file has been
|
||||||
|
# downloaded
|
||||||
downloadFileName = Tiqasäx { $filename }
|
downloadFileName = Tiqasäx { $filename }
|
||||||
downloadFileSize = ({ $size })
|
downloadFileSize = ({ $size })
|
||||||
unlockInputLabel = Titz'ib'äx Ewan Tzij
|
unlockInputLabel = Titz'ib'äx Ewan Tzij
|
||||||
unlockInputPlaceholder = Ewan tzij
|
unlockInputPlaceholder = Ewan tzij
|
||||||
unlockButtonLabel = Titzij chik
|
unlockButtonLabel = Titzij chik
|
||||||
downloadFileTitle = Tiqasäx Yakb'äl Ewan Rusik'ixik
|
downloadFileTitle = Tiqasäx Yakb'äl Ewan Rusik'ixik
|
||||||
// Firefox Send is a brand name and should not be localized.
|
# Firefox Send is a brand name and should not be localized.
|
||||||
downloadMessage = Jun awachib'il xutäq jun yakb'äl chawe rik'in ri Firefox Send, jun samaj ri nuya' q'ij chawe ye'akomonij taq yakb'äl rik'in jun jikïl, ichinan chuqa' ewan rusik'ixik ximonel, ri nik'is ruq'ijul pa ruyonil richin chi ri taq awachinaq man junelïk ta e okel pa k'amab'ey.
|
downloadMessage = Jun awachib'il xutäq jun yakb'äl chawe rik'in ri Firefox Send, jun samaj ri nuya' q'ij chawe ye'akomonij taq yakb'äl rik'in jun jikïl, ichinan chuqa' ewan rusik'ixik ximonel, ri nik'is ruq'ijul pa ruyonil richin chi ri taq awachinaq man junelïk ta e okel pa k'amab'ey.
|
||||||
// Text and title used on the download link/button (indicates an action).
|
# Text and title used on the download link/button (indicates an action).
|
||||||
downloadButtonLabel = Tiqasäx
|
downloadButtonLabel = Tiqasäx
|
||||||
downloadNotification = Xtz'aqät ri aqasanik.
|
downloadNotification = Xtz'aqät ri aqasanik.
|
||||||
downloadFinish = Xtz'aqät qasanïk
|
downloadFinish = Xtz'aqät qasanïk
|
||||||
// This message is displayed when uploading or downloading a file, e.g. "(1,3 MB of 10 MB)".
|
# This message is displayed when uploading or downloading a file, e.g. "(1,3 MB of 10 MB)".
|
||||||
fileSizeProgress = ({ $partialSize } richin { $totalSize })
|
fileSizeProgress = ({ $partialSize } richin { $totalSize })
|
||||||
// Firefox Send is a brand name and should not be localized.
|
# Firefox Send is a brand name and should not be localized.
|
||||||
sendYourFilesLink = Titojtob'ëx Firefox Send
|
sendYourFilesLink = Titojtob'ëx Firefox Send
|
||||||
downloadingPageProgress = Tajin niqasäx { $filename } ({ $size })
|
downloadingPageProgress = Tajin niqasäx { $filename } ({ $size })
|
||||||
downloadingPageMessage = Tijaq kan re ruwi' re' richin niqaqasaj ri yakb'äl chuqa' richin niqetamaj rusik'ixik.
|
downloadingPageMessage = Tijaq kan re ruwi' re' richin niqaqasaj ri yakb'äl chuqa' richin niqetamaj rusik'ixik.
|
||||||
@@ -58,7 +73,7 @@ fileTooBig = Yalan nïm re yakb'äl re' richin nijotob'äx. K'o ta chi man nik'o
|
|||||||
linkExpiredAlt = Xk'is ruq'ijul ri ximonel
|
linkExpiredAlt = Xk'is ruq'ijul ri ximonel
|
||||||
expiredPageHeader = ¡Xk'is ruq'ijul re ximonel re' o rik'in jub'a' majub'ey xk'oje'!
|
expiredPageHeader = ¡Xk'is ruq'ijul re ximonel re' o rik'in jub'a' majub'ey xk'oje'!
|
||||||
notSupportedHeader = Man koch'el ta ri awokik'amaya'l.
|
notSupportedHeader = Man koch'el ta ri awokik'amaya'l.
|
||||||
// Firefox Send is a brand name and should not be localized.
|
# Firefox Send is a brand name and should not be localized.
|
||||||
notSupportedDetail = K'ayew ruma re okik'amaya'l re' man nuköch' ta ajk'amaya'l na'ob'äl nik'atzin chi re ri Firefox Send. K'o chi natojtob'ej jun chik okik'amaya'l. ¡Niqachilab'ej chawe ri Firefox!
|
notSupportedDetail = K'ayew ruma re okik'amaya'l re' man nuköch' ta ajk'amaya'l na'ob'äl nik'atzin chi re ri Firefox Send. K'o chi natojtob'ej jun chik okik'amaya'l. ¡Niqachilab'ej chawe ri Firefox!
|
||||||
notSupportedLink = ¿Achike ruma man nikoch' taq ri wokik'amaya'l?
|
notSupportedLink = ¿Achike ruma man nikoch' taq ri wokik'amaya'l?
|
||||||
notSupportedOutdatedDetail = K'ayew ruma re ruwäch Firefox re' man nuköch' ta ri ajk'amaya'l na'ob'äl nrajo' ri Firefox Send. Rajowaxik nak'ëx ri awokik'amaya'l.
|
notSupportedOutdatedDetail = K'ayew ruma re ruwäch Firefox re' man nuköch' ta ri ajk'amaya'l na'ob'äl nrajo' ri Firefox Send. Rajowaxik nak'ëx ri awokik'amaya'l.
|
||||||
@@ -66,7 +81,7 @@ updateFirefox = Tik'ex ri Firefox
|
|||||||
downloadFirefoxButtonSub = Sipan Ruqasaxik
|
downloadFirefoxButtonSub = Sipan Ruqasaxik
|
||||||
uploadedFile = Yakb'äl
|
uploadedFile = Yakb'äl
|
||||||
copyFileList = Tiwachib'ëx URL
|
copyFileList = Tiwachib'ëx URL
|
||||||
// expiryFileList is used as a column header
|
# expiryFileList is used as a column header
|
||||||
expiryFileList = Nik'is Ruq'ijul Pa
|
expiryFileList = Nik'is Ruq'ijul Pa
|
||||||
deleteFileList = Tiyuj
|
deleteFileList = Tiyuj
|
||||||
nevermindButton = Junam nub'än
|
nevermindButton = Junam nub'än
|
||||||
@@ -79,13 +94,26 @@ deletePopupCancel = Tiq'at
|
|||||||
deleteButtonHover = Tiyuj
|
deleteButtonHover = Tiyuj
|
||||||
copyUrlHover = Tiwachib'ëx URL
|
copyUrlHover = Tiwachib'ëx URL
|
||||||
footerLinkLegal = Taqanel tzijol
|
footerLinkLegal = Taqanel tzijol
|
||||||
// Test Pilot is a proper name and should not be localized.
|
# Test Pilot is a proper name and should not be localized.
|
||||||
footerLinkAbout = Chi rij Test Pilot
|
footerLinkAbout = Chi rij Test Pilot
|
||||||
footerLinkPrivacy = Ichinanem
|
footerLinkPrivacy = Ichinanem
|
||||||
footerLinkTerms = Taq ojqanem
|
footerLinkTerms = Taq ojqanem
|
||||||
footerLinkCookies = Taq kaxlanwey
|
footerLinkCookies = Taq kaxlanwey
|
||||||
requirePasswordCheckbox = Tik'utüx jun ewan tzij richin niqasäx re yakb'äl re'
|
requirePasswordCheckbox = Tik'utüx jun ewan tzij richin niqasäx re yakb'äl re'
|
||||||
addPasswordButton = Titz'aqatisäx Ewan Tzij
|
addPasswordButton = Titz'aqatisäx Ewan Tzij
|
||||||
|
changePasswordButton = Tijalwachïx
|
||||||
passwordTryAgain = Itzel ri ewan tzij. Tatojtob'ej chik.
|
passwordTryAgain = Itzel ri ewan tzij. Tatojtob'ej chik.
|
||||||
// This label is followed by the password needed to download a file
|
reportIPInfringement = Tiya' rutzijol ri Ritzelanik Ajna'oj Ichinil
|
||||||
passwordResult = Ewan tzij: { $password }
|
javascriptRequired = K'atzinel JavaScript chi re ri Firefox Send
|
||||||
|
whyJavascript = ¿Achike ruma toq ri Firefox Send nrajo' JavaScript?
|
||||||
|
enableJavascript = Titz'ij JavaScript richin nitojtob'ëx chik.
|
||||||
|
# A short representation of a countdown timer containing the number of hours and minutes remaining as digits, example "13h 47m"
|
||||||
|
expiresHoursMinutes = { $hours }r { $minutes }ch
|
||||||
|
# A short representation of a countdown timer containing the number of minutes remaining as digits, example "56m"
|
||||||
|
expiresMinutes = { $minutes }ch
|
||||||
|
# A short status message shown when a password is successfully set
|
||||||
|
passwordIsSet = Xjikib'äx ewan tzij
|
||||||
|
# A short status message shown when the user enters a long password
|
||||||
|
maxPasswordLength = Nïm raqän ewan tzij: { $length }
|
||||||
|
# A short status message shown when there was an error setting the password
|
||||||
|
passwordSetError = Man tikirel ta ninuk' re ewan tzij re'
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Firefox Send is a brand name and should not be localized.
|
# Firefox Send is a brand name and should not be localized.
|
||||||
title = Firefox Send
|
title = Firefox Send
|
||||||
siteSubtitle = webový experiment
|
siteSubtitle = webový experiment
|
||||||
siteFeedback = Zpětná vazba
|
siteFeedback = Zpětná vazba
|
||||||
@@ -25,28 +25,45 @@ uploadingFileNotification = Upozornit, až bude nahrávání dokončeno.
|
|||||||
uploadSuccessConfirmHeader = Připraveno k odeslání
|
uploadSuccessConfirmHeader = Připraveno k odeslání
|
||||||
uploadSvgAlt = Nahrát
|
uploadSvgAlt = Nahrát
|
||||||
uploadSuccessTimingHeader = Platnost odkazu na váš soubor vyprší po jeho prvním stažení, nebo po 24 hodinách.
|
uploadSuccessTimingHeader = Platnost odkazu na váš soubor vyprší po jeho prvním stažení, nebo po 24 hodinách.
|
||||||
|
expireInfo = Platnost odkazu na váš soubor vyprší po { $downloadCount } nebo { $timespan }.
|
||||||
|
downloadCount = { $num ->
|
||||||
|
[one] jednom stažení
|
||||||
|
[few] { $num } staženích
|
||||||
|
*[other] { $num } staženích
|
||||||
|
}
|
||||||
|
timespanHours = { $num ->
|
||||||
|
[one] hodina
|
||||||
|
[few] hodiny
|
||||||
|
*[other] hodin
|
||||||
|
}
|
||||||
copyUrlFormLabelWithName = Zkopírujte a sdílejte odkaz na váš soubor: { $filename }
|
copyUrlFormLabelWithName = Zkopírujte a sdílejte odkaz na váš soubor: { $filename }
|
||||||
copyUrlFormButton = Zkopírovat do schránky
|
copyUrlFormButton = Zkopírovat do schránky
|
||||||
copiedUrl = Zkopírováno!
|
copiedUrl = Zkopírováno!
|
||||||
deleteFileButton = Smazat soubor
|
deleteFileButton = Smazat soubor
|
||||||
sendAnotherFileLink = Poslat další soubor
|
sendAnotherFileLink = Poslat další soubor
|
||||||
// Alternative text used on the download link/button (indicates an action).
|
# Alternative text used on the download link/button (indicates an action).
|
||||||
downloadAltText = Stáhnout
|
downloadAltText = Stáhnout
|
||||||
|
downloadsFileList = Stažení
|
||||||
|
# Used as header in a column indicating the amount of time left before a
|
||||||
|
# download link expires (e.g. "10h 5m")
|
||||||
|
timeFileList = Zbývá
|
||||||
|
# Used as header in a column indicating the number of times a file has been
|
||||||
|
# downloaded
|
||||||
downloadFileName = Stáhnout { $filename }
|
downloadFileName = Stáhnout { $filename }
|
||||||
downloadFileSize = ({ $size })
|
downloadFileSize = ({ $size })
|
||||||
unlockInputLabel = Zadejte heslo
|
unlockInputLabel = Zadejte heslo
|
||||||
unlockInputPlaceholder = Heslo
|
unlockInputPlaceholder = Heslo
|
||||||
unlockButtonLabel = Odemknout
|
unlockButtonLabel = Odemknout
|
||||||
downloadFileTitle = Stáhnout šifrovaný soubor
|
downloadFileTitle = Stáhnout šifrovaný soubor
|
||||||
// Firefox Send is a brand name and should not be localized.
|
# Firefox Send is a brand name and should not be localized.
|
||||||
downloadMessage = Někdo vám posílá soubor pomocí služby Firefox Send, které umožňuje bezpečné, soukromé a šifrované sdílení souborů, které jsou pak automaticky smazány, aby nezůstaly na internetu navěky.
|
downloadMessage = Někdo vám posílá soubor pomocí služby Firefox Send, které umožňuje bezpečné, soukromé a šifrované sdílení souborů, které jsou pak automaticky smazány, aby nezůstaly na internetu navěky.
|
||||||
// Text and title used on the download link/button (indicates an action).
|
# Text and title used on the download link/button (indicates an action).
|
||||||
downloadButtonLabel = Stáhnout
|
downloadButtonLabel = Stáhnout
|
||||||
downloadNotification = Stahování bylo dokončeno.
|
downloadNotification = Stahování bylo dokončeno.
|
||||||
downloadFinish = Stahování dokončeno
|
downloadFinish = Stahování dokončeno
|
||||||
// This message is displayed when uploading or downloading a file, e.g. "(1,3 MB of 10 MB)".
|
# This message is displayed when uploading or downloading a file, e.g. "(1,3 MB of 10 MB)".
|
||||||
fileSizeProgress = ({ $partialSize } z { $totalSize })
|
fileSizeProgress = ({ $partialSize } z { $totalSize })
|
||||||
// Firefox Send is a brand name and should not be localized.
|
# Firefox Send is a brand name and should not be localized.
|
||||||
sendYourFilesLink = Vyzkoušejte Firefox Send
|
sendYourFilesLink = Vyzkoušejte Firefox Send
|
||||||
downloadingPageProgress = Stahování { $filename } ({ $size })
|
downloadingPageProgress = Stahování { $filename } ({ $size })
|
||||||
downloadingPageMessage = Ponechte prosím tento panel otevřený, dokud nepřipravíme váš soubor a nedešifrujeme ho.
|
downloadingPageMessage = Ponechte prosím tento panel otevřený, dokud nepřipravíme váš soubor a nedešifrujeme ho.
|
||||||
@@ -58,7 +75,7 @@ fileTooBig = Tento soubor je příliš veliký. Velikost nahrávaných souborů
|
|||||||
linkExpiredAlt = Platnost odkazu vypršela
|
linkExpiredAlt = Platnost odkazu vypršela
|
||||||
expiredPageHeader = Platnost tohoto odkazu buď vypršela, nebo vůbec nikdy neexistoval.
|
expiredPageHeader = Platnost tohoto odkazu buď vypršela, nebo vůbec nikdy neexistoval.
|
||||||
notSupportedHeader = Váš prohlížeč není podporován.
|
notSupportedHeader = Váš prohlížeč není podporován.
|
||||||
// Firefox Send is a brand name and should not be localized.
|
# Firefox Send is a brand name and should not be localized.
|
||||||
notSupportedDetail = Bohužel tento prohlížeč nepodporuje technologii, kterou Firefox Send používá. Zkuste prosím jiný prohlížeč, doporučujeme Firefox!
|
notSupportedDetail = Bohužel tento prohlížeč nepodporuje technologii, kterou Firefox Send používá. Zkuste prosím jiný prohlížeč, doporučujeme Firefox!
|
||||||
notSupportedLink = Proč není můj prohlížeč podporovaný?
|
notSupportedLink = Proč není můj prohlížeč podporovaný?
|
||||||
notSupportedOutdatedDetail = Tato verze Firefoxu bohužel nepodporuje webovou technologii, která pohání Firefox Send. Musíte aktualizovat svůj prohlížeč.
|
notSupportedOutdatedDetail = Tato verze Firefoxu bohužel nepodporuje webovou technologii, která pohání Firefox Send. Musíte aktualizovat svůj prohlížeč.
|
||||||
@@ -66,7 +83,7 @@ updateFirefox = Aktualizovat Firefox
|
|||||||
downloadFirefoxButtonSub = Stáhnout zdarma
|
downloadFirefoxButtonSub = Stáhnout zdarma
|
||||||
uploadedFile = Soubor
|
uploadedFile = Soubor
|
||||||
copyFileList = Kopírovat URL
|
copyFileList = Kopírovat URL
|
||||||
// expiryFileList is used as a column header
|
# expiryFileList is used as a column header
|
||||||
expiryFileList = Platnost vyprší za
|
expiryFileList = Platnost vyprší za
|
||||||
deleteFileList = Smazat
|
deleteFileList = Smazat
|
||||||
nevermindButton = Nevadí
|
nevermindButton = Nevadí
|
||||||
@@ -79,13 +96,26 @@ deletePopupCancel = Zrušit
|
|||||||
deleteButtonHover = Smazat
|
deleteButtonHover = Smazat
|
||||||
copyUrlHover = Kopírovat URL
|
copyUrlHover = Kopírovat URL
|
||||||
footerLinkLegal = Právní informace
|
footerLinkLegal = Právní informace
|
||||||
// Test Pilot is a proper name and should not be localized.
|
# Test Pilot is a proper name and should not be localized.
|
||||||
footerLinkAbout = O programu Test Pilot
|
footerLinkAbout = O programu Test Pilot
|
||||||
footerLinkPrivacy = Soukromí
|
footerLinkPrivacy = Soukromí
|
||||||
footerLinkTerms = Podmínky
|
footerLinkTerms = Podmínky
|
||||||
footerLinkCookies = Cookies
|
footerLinkCookies = Cookies
|
||||||
requirePasswordCheckbox = Vyžadovat heslo pro stažení tohoto souboru
|
requirePasswordCheckbox = Vyžadovat heslo pro stažení tohoto souboru
|
||||||
addPasswordButton = Přidat heslo
|
addPasswordButton = Přidat heslo
|
||||||
|
changePasswordButton = Změnit
|
||||||
passwordTryAgain = Špatné heslo. Zkuste to znovu.
|
passwordTryAgain = Špatné heslo. Zkuste to znovu.
|
||||||
// This label is followed by the password needed to download a file
|
reportIPInfringement = Nahlásit porušení autorských práv
|
||||||
passwordResult = Heslo: { $password }
|
javascriptRequired = Firefox Send vyžaduje povolený JavaScript
|
||||||
|
whyJavascript = Proč Firefox Send vyžaduje povolený JavaScript?
|
||||||
|
enableJavascript = Povolte JavaScript a zkuste to znovu.
|
||||||
|
# A short representation of a countdown timer containing the number of hours and minutes remaining as digits, example "13h 47m"
|
||||||
|
expiresHoursMinutes = { $hours }h { $minutes }m
|
||||||
|
# A short representation of a countdown timer containing the number of minutes remaining as digits, example "56m"
|
||||||
|
expiresMinutes = { $minutes }m
|
||||||
|
# A short status message shown when a password is successfully set
|
||||||
|
passwordIsSet = Heslo nastaveno
|
||||||
|
# A short status message shown when the user enters a long password
|
||||||
|
maxPasswordLength = Maximální délka hesla: { $length }
|
||||||
|
# A short status message shown when there was an error setting the password
|
||||||
|
passwordSetError = Toto heslo nemohlo být nastaveno
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user