mirror of
https://gitlab.com/timvisee/send.git
synced 2025-12-06 22:20:55 +03:00
Compare commits
333 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7013f5cf80 | ||
|
|
6184a70ba4 | ||
|
|
e264d0da62 | ||
|
|
23a6e338e8 | ||
|
|
2c1dfdbe07 | ||
|
|
994e77a38b | ||
|
|
173ca461a9 | ||
|
|
53426b950a | ||
|
|
9bb36cd827 | ||
|
|
103aa8a0c8 | ||
|
|
62d507120c | ||
|
|
8ccb1c449a | ||
|
|
a07eb1ad1c | ||
|
|
78c6d83462 | ||
|
|
5d41da0e16 | ||
|
|
5afe9ff2af | ||
|
|
a2eee15a7d | ||
|
|
603a352595 | ||
|
|
7b8655a079 | ||
|
|
d3ba54d05a | ||
|
|
83e2aec3f5 | ||
|
|
0abf890fc4 | ||
|
|
38fcd7227d | ||
|
|
6d29cebabb | ||
|
|
8305b9bd2f | ||
|
|
5c542008ab | ||
|
|
e1d6467de4 | ||
|
|
7dc34ba646 | ||
|
|
d5a30b710d | ||
|
|
c90310405c | ||
|
|
1bd85ee656 | ||
|
|
45452c7153 | ||
|
|
59ba89262f | ||
|
|
d906e927ed | ||
|
|
527e9f09c9 | ||
|
|
5ff92c6452 | ||
|
|
dfea1e96fb | ||
|
|
7e30fe8d33 | ||
|
|
a74a560f0a | ||
|
|
93072c0c1e | ||
|
|
b3ad207326 | ||
|
|
dced61eb30 | ||
|
|
42574af2cc | ||
|
|
ab699ebcc6 | ||
|
|
de37804973 | ||
|
|
7654ec3b7c | ||
|
|
fa84c653ea | ||
|
|
7f9b43753e | ||
|
|
a0c221750b | ||
|
|
bcb3936e08 | ||
|
|
66ec29eee9 | ||
|
|
58535c8c2e | ||
|
|
563686849f | ||
|
|
5b607af8d1 | ||
|
|
c5061ec51e | ||
|
|
32074a9bab | ||
|
|
26478fc444 | ||
|
|
7c66c07634 | ||
|
|
4ef6f8e3bd | ||
|
|
5591fb03f1 | ||
|
|
fc35acc6e4 | ||
|
|
5c8c6d56a4 | ||
|
|
4f9e63beca | ||
|
|
6b7f6426a1 | ||
|
|
1b6fad9d87 | ||
|
|
016d9bcf91 | ||
|
|
f199c8b96a | ||
|
|
06321126e7 | ||
|
|
08f9ad5046 | ||
|
|
9aefe7f7c9 | ||
|
|
ea2ff28f32 | ||
|
|
339bf12857 | ||
|
|
7c605ac7af | ||
|
|
288b878700 | ||
|
|
db84b331c2 | ||
|
|
2fde21c522 | ||
|
|
fb3747785b | ||
|
|
93d151a29a | ||
|
|
e28a92848d | ||
|
|
f83784e033 | ||
|
|
11173c520b | ||
|
|
608112d56c | ||
|
|
480a06c426 | ||
|
|
f0530975ac | ||
|
|
6d4973391a | ||
|
|
0edfc8405f | ||
|
|
5274b732b2 | ||
|
|
0a71c8c724 | ||
|
|
9feb6866ee | ||
|
|
196d4211b6 | ||
|
|
a50762ebd7 | ||
|
|
3e65f3a906 | ||
|
|
bea7d30836 | ||
|
|
6acf58f9e9 | ||
|
|
33993eda88 | ||
|
|
8242e2088d | ||
|
|
48457f7ac1 | ||
|
|
8a496022f4 | ||
|
|
9c398ad98b | ||
|
|
dc1b754692 | ||
|
|
dc7203ea59 | ||
|
|
af9973e35b | ||
|
|
aeb44379c8 | ||
|
|
7841dec5d8 | ||
|
|
7d62a23b36 | ||
|
|
f36ac24ac5 | ||
|
|
e6c2736f1f | ||
|
|
0d46997ab3 | ||
|
|
896a0f035b | ||
|
|
fdd07a06be | ||
|
|
8f8595e750 | ||
|
|
7a83fc6d8f | ||
|
|
30df33e189 | ||
|
|
7aac0426e0 | ||
|
|
bc423d4d78 | ||
|
|
3ddfc822d1 | ||
|
|
f99e4db25f | ||
|
|
fd71e7f957 | ||
|
|
2bfeb75380 | ||
|
|
21f7fd7dbc | ||
|
|
3e08c35740 | ||
|
|
54e78b6274 | ||
|
|
f1499abbe8 | ||
|
|
1ad78e2844 | ||
|
|
af436f9506 | ||
|
|
4dfdc8b0c7 | ||
|
|
b4c0c36f3a | ||
|
|
d52ca850cb | ||
|
|
c3e0787d12 | ||
|
|
3f65e55f86 | ||
|
|
2db56fac3a | ||
|
|
464c0c4c47 | ||
|
|
71ed7d71fb | ||
|
|
cda38f9bcf | ||
|
|
cc9b622bde | ||
|
|
fb91fd03cc | ||
|
|
77e3b5a3e6 | ||
|
|
0ed5c7f1e7 | ||
|
|
5afadd4ff1 | ||
|
|
0f53c718a2 | ||
|
|
ad4e6c8dec | ||
|
|
9e0195deaa | ||
|
|
253216e6fc | ||
|
|
78eab6335d | ||
|
|
1d20b5ba11 | ||
|
|
1edc571b36 | ||
|
|
e3556aa7e1 | ||
|
|
aa94a75da9 | ||
|
|
ecd61830aa | ||
|
|
da82ef814b | ||
|
|
b840173429 | ||
|
|
e1dc1687fc | ||
|
|
3e6a88d31d | ||
|
|
94714ecb62 | ||
|
|
07a817266c | ||
|
|
706708876c | ||
|
|
fc4cbe4b74 | ||
|
|
9e8429cff3 | ||
|
|
f8db7ca923 | ||
|
|
70e4d9eeb0 | ||
|
|
410ec72fff | ||
|
|
a42a517896 | ||
|
|
d9c9d95b89 | ||
|
|
0ed10649ef | ||
|
|
4edf82cc21 | ||
|
|
775726ae6d | ||
|
|
5aeaf2e987 | ||
|
|
773244f320 | ||
|
|
5079d9a317 | ||
|
|
18e1609cb3 | ||
|
|
41796840c4 | ||
|
|
171b64bc98 | ||
|
|
cfc94fd9af | ||
|
|
914394054e | ||
|
|
7a237b9b68 | ||
|
|
80e9f129d8 | ||
|
|
fddf1c40dc | ||
|
|
557db53d39 | ||
|
|
c16e00e5af | ||
|
|
cd7da20024 | ||
|
|
5f44ed2598 | ||
|
|
5c0cfdcf38 | ||
|
|
2de5378208 | ||
|
|
a2aca89550 | ||
|
|
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 |
@@ -1,8 +1,11 @@
|
|||||||
node_modules
|
node_modules
|
||||||
.git
|
.git
|
||||||
|
.tox
|
||||||
.DS_Store
|
.DS_Store
|
||||||
firefox
|
firefox
|
||||||
assets
|
assets
|
||||||
docs
|
docs
|
||||||
public
|
public
|
||||||
test
|
test
|
||||||
|
coverage
|
||||||
|
.nyc_output
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
dist
|
dist
|
||||||
assets
|
assets
|
||||||
firefox
|
firefox
|
||||||
|
coverage
|
||||||
|
|||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,3 +1,8 @@
|
|||||||
node_modules
|
node_modules
|
||||||
|
coverage
|
||||||
dist
|
dist
|
||||||
.idea
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
.nyc_output
|
||||||
|
.tox
|
||||||
|
.pytest_cache
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
dist
|
dist
|
||||||
assets/*.js
|
assets/*.js
|
||||||
|
coverage
|
||||||
7
.pyup.yml
Normal file
7
.pyup.yml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# autogenerated pyup.io config file
|
||||||
|
# see https://pyup.io/docs/configuration/ for all available options
|
||||||
|
|
||||||
|
schedule: every week
|
||||||
|
requirements:
|
||||||
|
- test/integration/Pipfile
|
||||||
|
- test/integration/pipenv.txt
|
||||||
93
CHANGELOG.md
93
CHANGELOG.md
@@ -1,5 +1,98 @@
|
|||||||
## Change Log
|
## Change Log
|
||||||
|
|
||||||
|
### v2.5.1 (2018/03/12 19:26 +00:00)
|
||||||
|
- [#789](https://github.com/mozilla/send/pull/789) Fixed #775 : Made text not-selectable (@RCMainak)
|
||||||
|
|
||||||
|
### v2.5.0 (2018/03/08 19:31 +00:00)
|
||||||
|
- [#782](https://github.com/mozilla/send/pull/782) updated docs (@dannycoates)
|
||||||
|
- [#781](https://github.com/mozilla/send/pull/781) Don't translate URL-safe chars, b64 is doing it for us (@timvisee)
|
||||||
|
- [#779](https://github.com/mozilla/send/pull/779) implemented crypto polyfills for ms edge (@dannycoates)
|
||||||
|
|
||||||
|
### v2.4.1 (2018/02/28 17:05 +00:00)
|
||||||
|
- [#777](https://github.com/mozilla/send/pull/777) use a separate circle in the progress svg for indefinite progress (@dannycoates)
|
||||||
|
|
||||||
|
### v2.4.0 (2018/02/27 01:55 +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)
|
||||||
|
|||||||
65
CONTRIBUTORS
65
CONTRIBUTORS
@@ -1,74 +1,126 @@
|
|||||||
|
Abdalrahman Hwoij
|
||||||
Abhinav Adduri
|
Abhinav Adduri
|
||||||
|
Adnan Kičin
|
||||||
|
Alberto Castro
|
||||||
Alexander Slovesnik
|
Alexander Slovesnik
|
||||||
|
Alfredos-Panagiotis Damkalis
|
||||||
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
|
||||||
|
Gabriela
|
||||||
Gautam krishna.R
|
Gautam krishna.R
|
||||||
|
Georgianizator
|
||||||
|
Gonçalo Matos
|
||||||
|
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
|
||||||
|
Vitaliy Krutko
|
||||||
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 +129,29 @@ 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
|
||||||
|
rcmainak
|
||||||
reza.habibi2008
|
reza.habibi2008
|
||||||
|
savemore99.sm
|
||||||
|
shikhar-scs
|
||||||
siparon
|
siparon
|
||||||
skystar-p
|
skystar-p
|
||||||
|
tiagomoraismorgado
|
||||||
|
timvisee
|
||||||
xcffl
|
xcffl
|
||||||
|
ybouhamam
|
||||||
Μιχάλης
|
Μιχάλης
|
||||||
Марко Костић (Marko Kostić)
|
Марко Костић (Marko Kostić)
|
||||||
|
صفا الفليج
|
||||||
|
వీవెన్
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
# Firefox Send
|
# Firefox Send
|
||||||
|
|
||||||
|
[](https://www.browserstack.com/automate/public-build/aFFIMHNEWFcrNHJaMU1LRkJnUDhOQkNHMmh2WHBscjJsZHcwK1h0dkhwdz0tLXRpN1RXcysybUtxTFFTVGRtWjVGeHc9PQ==--c56129be8c75941b115c5b5e5d3ed10b3c7dca6b)
|
||||||
[](https://circleci.com/gh/mozilla/send)
|
[](https://circleci.com/gh/mozilla/send)
|
||||||
[](https://testpilot.firefox.com/experiments/send)
|
[](https://testpilot.firefox.com/experiments/send)
|
||||||
|
|
||||||
**Docs:** [Docker](docs/docker.md), [Metrics](docs/metrics.md)
|
**Docs:** [FAQ](docs/faq.md), [Encryption](docs/encryption.md), [Build](docs/build.md), [Docker](docs/docker.md), [Metrics](docs/metrics.md), [More](docs/)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -71,6 +72,8 @@ The server is configured with environment variables. See [server/config.js](serv
|
|||||||
|
|
||||||
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.
|
||||||
|
|
||||||
|
see also [docs/localization.md](docs/localization.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|||||||
120
app/api.js
120
app/api.js
@@ -91,10 +91,15 @@ export async function setPassword(id, owner_token, keychain) {
|
|||||||
return response.ok;
|
return response.ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function uploadFile(encrypted, metadata, verifierB64, keychain) {
|
export function uploadFile(
|
||||||
|
encrypted,
|
||||||
|
metadata,
|
||||||
|
verifierB64,
|
||||||
|
keychain,
|
||||||
|
onprogress
|
||||||
|
) {
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
const upload = {
|
const upload = {
|
||||||
onprogress: function() {},
|
|
||||||
cancel: function() {
|
cancel: function() {
|
||||||
xhr.abort();
|
xhr.abort();
|
||||||
},
|
},
|
||||||
@@ -122,7 +127,7 @@ export function uploadFile(encrypted, metadata, verifierB64, keychain) {
|
|||||||
fd.append('data', blob);
|
fd.append('data', blob);
|
||||||
xhr.upload.addEventListener('progress', function(event) {
|
xhr.upload.addEventListener('progress', function(event) {
|
||||||
if (event.lengthComputable) {
|
if (event.lengthComputable) {
|
||||||
upload.onprogress([event.loaded, event.total]);
|
onprogress([event.loaded, event.total]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
xhr.open('post', '/api/upload', true);
|
xhr.open('post', '/api/upload', true);
|
||||||
@@ -132,82 +137,65 @@ export function uploadFile(encrypted, metadata, verifierB64, keychain) {
|
|||||||
return upload;
|
return upload;
|
||||||
}
|
}
|
||||||
|
|
||||||
function download(id, keychain) {
|
function download(id, keychain, onprogress, canceller) {
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
const download = {
|
canceller.oncancel = function() {
|
||||||
onprogress: function() {},
|
xhr.abort();
|
||||||
cancel: function() {
|
|
||||||
xhr.abort();
|
|
||||||
},
|
|
||||||
result: new Promise(async function(resolve, reject) {
|
|
||||||
xhr.addEventListener('loadend', function() {
|
|
||||||
const authHeader = xhr.getResponseHeader('WWW-Authenticate');
|
|
||||||
if (authHeader) {
|
|
||||||
keychain.nonce = parseNonce(authHeader);
|
|
||||||
}
|
|
||||||
if (xhr.status === 404) {
|
|
||||||
return reject(new Error('notfound'));
|
|
||||||
}
|
|
||||||
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) {
|
|
||||||
download.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();
|
|
||||||
})
|
|
||||||
};
|
};
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
return download;
|
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.target.status === 200) {
|
||||||
|
onprogress(event.loaded);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const auth = await keychain.authHeader();
|
||||||
|
xhr.open('get', `/api/download/${id}`);
|
||||||
|
xhr.setRequestHeader('Authorization', auth);
|
||||||
|
xhr.responseType = 'blob';
|
||||||
|
xhr.send();
|
||||||
|
onprogress(0);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function tryDownload(id, keychain, onprogress, tries = 1) {
|
async function tryDownload(id, keychain, onprogress, canceller, tries = 1) {
|
||||||
const dl = download(id, keychain);
|
|
||||||
dl.onprogress = onprogress;
|
|
||||||
try {
|
try {
|
||||||
const result = await dl.result;
|
const result = await download(id, keychain, onprogress, canceller);
|
||||||
return result;
|
return result;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message === '401' && --tries > 0) {
|
if (e.message === '401' && --tries > 0) {
|
||||||
return tryDownload(id, keychain, onprogress, tries);
|
return tryDownload(id, keychain, onprogress, canceller, tries);
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function downloadFile(id, keychain) {
|
export function downloadFile(id, keychain, onprogress) {
|
||||||
let cancelled = false;
|
const canceller = {
|
||||||
function updateProgress(p) {
|
oncancel: function() {} // download() sets this
|
||||||
if (cancelled) {
|
};
|
||||||
// This is a bit of a hack
|
function cancel() {
|
||||||
// We piggyback off of the progress event as a chance to cancel.
|
canceller.oncancel();
|
||||||
// Otherwise wiring the xhr abort up while allowing retries
|
}
|
||||||
// gets pretty nasty.
|
return {
|
||||||
// 'this' here is the object returned by download(id, keychain)
|
cancel,
|
||||||
return this.cancel();
|
result: tryDownload(id, keychain, onprogress, canceller, 2)
|
||||||
}
|
|
||||||
dl.onprogress(p);
|
|
||||||
}
|
|
||||||
const dl = {
|
|
||||||
onprogress: function() {},
|
|
||||||
cancel: function() {
|
|
||||||
cancelled = true;
|
|
||||||
},
|
|
||||||
result: tryDownload(id, keychain, updateProgress, 2)
|
|
||||||
};
|
};
|
||||||
return dl;
|
|
||||||
}
|
}
|
||||||
|
|||||||
268
app/base.css
Normal file
268
app/base.css
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
: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 */
|
||||||
|
/* stylelint-disable-next-line plugin/no-unsupported-browser-features */
|
||||||
|
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 => {
|
||||||
@@ -8,15 +11,26 @@ export default function(state, emitter) {
|
|||||||
document.body.addEventListener('drop', event => {
|
document.body.addEventListener('drop', event => {
|
||||||
if (state.route === '/' && !state.uploading) {
|
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) {
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
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) {
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
|
alert(state.translate('fileTooBig', { size: bytes(MAXFILESIZE) }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
emitter.emit('upload', { file, type: 'drop' });
|
emitter.emit('upload', { file, type: 'drop' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const experiments = {
|
|||||||
},
|
},
|
||||||
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'
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import {
|
|||||||
delay,
|
delay,
|
||||||
fadeOut,
|
fadeOut,
|
||||||
openLinksInNewTab,
|
openLinksInNewTab,
|
||||||
percent,
|
percent
|
||||||
saveFile
|
|
||||||
} from './utils';
|
} from './utils';
|
||||||
import * as metrics from './metrics';
|
import * as metrics from './metrics';
|
||||||
|
|
||||||
@@ -19,7 +18,7 @@ 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 oldLimit = file.dlimit;
|
const oldLimit = file.dlimit;
|
||||||
@@ -100,28 +99,32 @@ export default function(state, emitter) {
|
|||||||
try {
|
try {
|
||||||
metrics.startedUpload({ size, type });
|
metrics.startedUpload({ size, type });
|
||||||
const ownedFile = await sender.upload();
|
const ownedFile = await sender.upload();
|
||||||
|
ownedFile.type = type;
|
||||||
state.storage.totalUploads += 1;
|
state.storage.totalUploads += 1;
|
||||||
metrics.completedUpload(ownedFile);
|
metrics.completedUpload(ownedFile);
|
||||||
|
|
||||||
state.storage.addFile(ownedFile);
|
state.storage.addFile(ownedFile);
|
||||||
|
const cancelBtn = document.getElementById('cancel-upload');
|
||||||
document.getElementById('cancel-upload').hidden = 'hidden';
|
if (cancelBtn) {
|
||||||
|
cancelBtn.hidden = 'hidden';
|
||||||
|
}
|
||||||
await delay(1000);
|
await delay(1000);
|
||||||
await fadeOut('upload-progress');
|
await fadeOut('.page');
|
||||||
openLinksInNewTab(links, false);
|
|
||||||
emitter.emit('pushState', `/share/${ownedFile.id}`);
|
emitter.emit('pushState', `/share/${ownedFile.id}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
|
||||||
|
|
||||||
if (err.message === '0') {
|
if (err.message === '0') {
|
||||||
//cancelled. do nothing
|
//cancelled. do nothing
|
||||||
metrics.cancelledUpload({ size, type });
|
metrics.cancelledUpload({ size, type });
|
||||||
return render();
|
render();
|
||||||
|
} else {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(err);
|
||||||
|
state.raven.captureException(err);
|
||||||
|
metrics.stoppedUpload({ size, type, err });
|
||||||
|
emitter.emit('pushState', '/error');
|
||||||
}
|
}
|
||||||
state.raven.captureException(err);
|
|
||||||
metrics.stoppedUpload({ size, type, err });
|
|
||||||
emitter.emit('pushState', '/error');
|
|
||||||
} finally {
|
} finally {
|
||||||
|
openLinksInNewTab(links, false);
|
||||||
state.uploading = false;
|
state.uploading = false;
|
||||||
state.transfer = null;
|
state.transfer = null;
|
||||||
}
|
}
|
||||||
@@ -129,11 +132,18 @@ export default function(state, emitter) {
|
|||||||
|
|
||||||
emitter.on('password', async ({ password, file }) => {
|
emitter.on('password', async ({ password, file }) => {
|
||||||
try {
|
try {
|
||||||
|
state.settingPassword = true;
|
||||||
|
render();
|
||||||
await file.setPassword(password);
|
await file.setPassword(password);
|
||||||
state.storage.writeFile(file);
|
state.storage.writeFile(file);
|
||||||
metrics.addedPassword({ size: file.size });
|
metrics.addedPassword({ size: file.size });
|
||||||
|
await delay(1000);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
state.passwordSetError = err;
|
||||||
|
} finally {
|
||||||
|
state.settingPassword = false;
|
||||||
}
|
}
|
||||||
render();
|
render();
|
||||||
});
|
});
|
||||||
@@ -143,8 +153,6 @@ export default function(state, emitter) {
|
|||||||
const receiver = new FileReceiver(file);
|
const receiver = new FileReceiver(file);
|
||||||
try {
|
try {
|
||||||
await receiver.getMetadata();
|
await receiver.getMetadata();
|
||||||
receiver.on('progress', updateProgress);
|
|
||||||
receiver.on('decrypting', render);
|
|
||||||
state.transfer = receiver;
|
state.transfer = receiver;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message === '401') {
|
if (e.message === '401') {
|
||||||
@@ -158,36 +166,41 @@ 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();
|
const dl = state.transfer.download();
|
||||||
|
render();
|
||||||
|
await dl;
|
||||||
const time = Date.now() - start;
|
const time = Date.now() - start;
|
||||||
const speed = size / (time / 1000);
|
const speed = size / (time / 1000);
|
||||||
await delay(1000);
|
await delay(1000);
|
||||||
await fadeOut('download-progress');
|
await fadeOut('.page');
|
||||||
saveFile(f);
|
|
||||||
state.storage.totalDownloads += 1;
|
state.storage.totalDownloads += 1;
|
||||||
|
state.transfer.reset();
|
||||||
metrics.completedDownload({ size, time, speed });
|
metrics.completedDownload({ size, time, speed });
|
||||||
emitter.emit('pushState', '/completed');
|
emitter.emit('pushState', '/completed');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.message === '0') {
|
if (err.message === '0') {
|
||||||
// download cancelled
|
// download cancelled
|
||||||
return render();
|
state.transfer.reset();
|
||||||
|
render();
|
||||||
|
} else {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(err);
|
||||||
|
state.transfer = null;
|
||||||
|
const location = err.message === '404' ? '/404' : '/error';
|
||||||
|
if (location === '/error') {
|
||||||
|
state.raven.captureException(err);
|
||||||
|
metrics.stoppedDownload({ size, err });
|
||||||
|
}
|
||||||
|
emitter.emit('pushState', location);
|
||||||
}
|
}
|
||||||
console.error(err);
|
|
||||||
const location = err.message === 'notfound' ? '/404' : '/error';
|
|
||||||
if (location === '/error') {
|
|
||||||
state.raven.captureException(err);
|
|
||||||
metrics.stoppedDownload({ size, err });
|
|
||||||
}
|
|
||||||
emitter.emit('pushState', location);
|
|
||||||
} finally {
|
} finally {
|
||||||
state.transfer = null;
|
|
||||||
openLinksInNewTab(links, false);
|
openLinksInNewTab(links, false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,17 +11,17 @@ export default class FileReceiver extends Nanobus {
|
|||||||
this.keychain.setPassword(fileInfo.password, fileInfo.url);
|
this.keychain.setPassword(fileInfo.password, fileInfo.url);
|
||||||
}
|
}
|
||||||
this.fileInfo = fileInfo;
|
this.fileInfo = fileInfo;
|
||||||
this.fileDownload = null;
|
this.reset();
|
||||||
this.msg = 'fileSizeProgress';
|
|
||||||
this.state = 'initialized';
|
|
||||||
this.progress = [0, 1];
|
|
||||||
this.cancelled = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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]),
|
||||||
@@ -30,56 +30,95 @@ export default class FileReceiver extends Nanobus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cancel() {
|
cancel() {
|
||||||
this.cancelled = true;
|
if (this.downloadRequest) {
|
||||||
if (this.fileDownload) {
|
this.downloadRequest.cancel();
|
||||||
this.fileDownload.cancel();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.msg = 'fileSizeProgress';
|
||||||
|
this.state = 'initialized';
|
||||||
|
this.progress = [0, 1];
|
||||||
|
}
|
||||||
|
|
||||||
async getMetadata() {
|
async getMetadata() {
|
||||||
const meta = await metadata(this.fileInfo.id, this.keychain);
|
const meta = await metadata(this.fileInfo.id, this.keychain);
|
||||||
if (meta) {
|
this.keychain.setIV(meta.iv);
|
||||||
this.keychain.setIV(meta.iv);
|
this.fileInfo.name = meta.name;
|
||||||
this.fileInfo.name = meta.name;
|
this.fileInfo.type = meta.type;
|
||||||
this.fileInfo.type = meta.type;
|
this.fileInfo.iv = meta.iv;
|
||||||
this.fileInfo.iv = meta.iv;
|
this.fileInfo.size = +meta.size;
|
||||||
this.fileInfo.size = meta.size;
|
this.state = 'ready';
|
||||||
this.state = 'ready';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.state = 'invalid';
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async download() {
|
async download(noSave = false) {
|
||||||
this.state = 'downloading';
|
this.state = 'downloading';
|
||||||
this.emit('progress', this.progress);
|
this.downloadRequest = await downloadFile(
|
||||||
|
this.fileInfo.id,
|
||||||
|
this.keychain,
|
||||||
|
p => {
|
||||||
|
this.progress = [p, this.fileInfo.size];
|
||||||
|
this.emit('progress');
|
||||||
|
}
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
const download = await downloadFile(this.fileInfo.id, this.keychain);
|
const ciphertext = await this.downloadRequest.result;
|
||||||
download.onprogress = p => {
|
this.downloadRequest = null;
|
||||||
this.progress = p;
|
|
||||||
this.emit('progress', p);
|
|
||||||
};
|
|
||||||
this.fileDownload = download;
|
|
||||||
const ciphertext = await download.result;
|
|
||||||
this.fileDownload = null;
|
|
||||||
this.msg = 'decryptingFile';
|
this.msg = 'decryptingFile';
|
||||||
this.state = 'decrypting';
|
this.state = 'decrypting';
|
||||||
this.emit('decrypting');
|
this.emit('decrypting');
|
||||||
const plaintext = await this.keychain.decryptFile(ciphertext);
|
const plaintext = await this.keychain.decryptFile(ciphertext);
|
||||||
if (this.cancelled) {
|
if (!noSave) {
|
||||||
throw new Error(0);
|
await saveFile({
|
||||||
|
plaintext,
|
||||||
|
name: decodeURIComponent(this.fileInfo.name),
|
||||||
|
type: this.fileInfo.type
|
||||||
|
});
|
||||||
}
|
}
|
||||||
this.msg = 'downloadFinish';
|
this.msg = 'downloadFinish';
|
||||||
this.state = 'complete';
|
this.state = 'complete';
|
||||||
return {
|
|
||||||
plaintext,
|
|
||||||
name: decodeURIComponent(this.fileInfo.name),
|
|
||||||
type: this.fileInfo.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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,17 +9,18 @@ export default class FileSender extends Nanobus {
|
|||||||
constructor(file) {
|
constructor(file) {
|
||||||
super('FileSender');
|
super('FileSender');
|
||||||
this.file = file;
|
this.file = file;
|
||||||
this.uploadRequest = null;
|
|
||||||
this.msg = 'importingFile';
|
|
||||||
this.progress = [0, 1];
|
|
||||||
this.cancelled = false;
|
|
||||||
this.keychain = new Keychain();
|
this.keychain = new Keychain();
|
||||||
|
this.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
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]),
|
||||||
@@ -27,6 +28,13 @@ 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.uploadRequest) {
|
if (this.uploadRequest) {
|
||||||
@@ -67,13 +75,14 @@ export default class FileSender extends Nanobus {
|
|||||||
encrypted,
|
encrypted,
|
||||||
metadata,
|
metadata,
|
||||||
authKeyB64,
|
authKeyB64,
|
||||||
this.keychain
|
this.keychain,
|
||||||
|
p => {
|
||||||
|
this.progress = p;
|
||||||
|
this.emit('progress');
|
||||||
|
}
|
||||||
);
|
);
|
||||||
this.msg = 'fileSizeProgress';
|
this.msg = 'fileSizeProgress';
|
||||||
this.uploadRequest.onprogress = p => {
|
this.emit('progress'); // HACK to kick MS Edge
|
||||||
this.progress = p;
|
|
||||||
this.emit('progress', p);
|
|
||||||
};
|
|
||||||
try {
|
try {
|
||||||
const result = await this.uploadRequest.result;
|
const result = await this.uploadRequest.result;
|
||||||
const time = Date.now() - start;
|
const time = Date.now() - start;
|
||||||
@@ -86,7 +95,6 @@ export default class FileSender extends Nanobus {
|
|||||||
url: `${result.url}#${secretKey}`,
|
url: `${result.url}#${secretKey}`,
|
||||||
name: this.file.name,
|
name: this.file.name,
|
||||||
size: this.file.size,
|
size: this.file.size,
|
||||||
type: this.file.type, //TODO 'click' ?
|
|
||||||
time: time,
|
time: time,
|
||||||
speed: this.file.size / (time / 1000),
|
speed: this.file.size / (time / 1000),
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
|
|||||||
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';
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
|
import 'fast-text-encoding'; // MS Edge support
|
||||||
import 'fluent-intl-polyfill';
|
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';
|
||||||
import dragManager from './dragManager';
|
import dragManager from './dragManager';
|
||||||
|
import pasteManager from './pasteManager';
|
||||||
import { canHasSend } from './utils';
|
import { canHasSend } from './utils';
|
||||||
import assets from '../common/assets';
|
|
||||||
import storage from './storage';
|
import storage from './storage';
|
||||||
import metrics from './metrics';
|
import metrics from './metrics';
|
||||||
import experiments from './experiments';
|
import experiments from './experiments';
|
||||||
@@ -30,10 +31,7 @@ app.use((state, emitter) => {
|
|||||||
) {
|
) {
|
||||||
unsupportedReason = 'outdated';
|
unsupportedReason = 'outdated';
|
||||||
}
|
}
|
||||||
if (/edge\/\d+/i.test(navigator.userAgent)) {
|
const ok = await canHasSend();
|
||||||
unsupportedReason = 'edge';
|
|
||||||
}
|
|
||||||
const ok = await canHasSend(assets.get('cryptofill.js'));
|
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
unsupportedReason = /firefox/i.test(navigator.userAgent)
|
unsupportedReason = /firefox/i.test(navigator.userAgent)
|
||||||
? 'outdated'
|
? 'outdated'
|
||||||
@@ -51,5 +49,6 @@ app.use(metrics);
|
|||||||
app.use(fileManager);
|
app.use(fileManager);
|
||||||
app.use(dragManager);
|
app.use(dragManager);
|
||||||
app.use(experiments);
|
app.use(experiments);
|
||||||
|
app.use(pasteManager);
|
||||||
|
|
||||||
app.mount('body');
|
app.mount('body');
|
||||||
|
|||||||
@@ -16,16 +16,22 @@ export default class OwnedFile {
|
|||||||
this.ownerToken = obj.ownerToken;
|
this.ownerToken = obj.ownerToken;
|
||||||
this.dlimit = obj.dlimit || 1;
|
this.dlimit = obj.dlimit || 1;
|
||||||
this.dtotal = obj.dtotal || 0;
|
this.dtotal = obj.dtotal || 0;
|
||||||
this.keychain = new Keychain(obj.secretKey);
|
this.keychain = new Keychain(obj.secretKey, obj.nonce);
|
||||||
this._hasPassword = !!obj.hasPassword;
|
this._hasPassword = !!obj.hasPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setPassword(password) {
|
async setPassword(password) {
|
||||||
this.password = password;
|
try {
|
||||||
this._hasPassword = true;
|
this.password = password;
|
||||||
this.keychain.setPassword(password, this.url);
|
this._hasPassword = true;
|
||||||
const result = await setPassword(this.id, this.ownerToken, this.keychain);
|
this.keychain.setPassword(password, this.url);
|
||||||
return result;
|
const result = await setPassword(this.id, this.ownerToken, this.keychain);
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
this.password = null;
|
||||||
|
this._hasPassword = false;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
del() {
|
del() {
|
||||||
@@ -53,6 +59,7 @@ export default class OwnedFile {
|
|||||||
if (e.message === '404') {
|
if (e.message === '404') {
|
||||||
this.dtotal = this.dlimit;
|
this.dtotal = this.dlimit;
|
||||||
}
|
}
|
||||||
|
// ignore other errors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
const html = require('choo/html');
|
const html = require('choo/html');
|
||||||
|
|
||||||
module.exports = function() {
|
module.exports = function() {
|
||||||
const div = html`<div></div>`;
|
return html`<div></div>`;
|
||||||
return div;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
const progress = require('../templates/progress');
|
|
||||||
const { fadeOut } = require('../utils');
|
|
||||||
|
|
||||||
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>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
async function sendNew(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
await fadeOut('download');
|
|
||||||
emit('pushState', '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
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', '/');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
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-upload"
|
|
||||||
title="${state.translate('deletePopupCancel')}"
|
|
||||||
onclick=${cancel}>
|
|
||||||
${state.translate('deletePopupCancel')}
|
|
||||||
</button>`;
|
|
||||||
|
|
||||||
const div = html`
|
|
||||||
<div id="page-one">
|
|
||||||
<div id="download">
|
|
||||||
<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>
|
|
||||||
${transfer.state === 'downloading' ? cancelBtn : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
function cancel() {
|
|
||||||
const btn = document.getElementById('cancel-upload');
|
|
||||||
btn.remove();
|
|
||||||
emit('cancel');
|
|
||||||
}
|
|
||||||
return div;
|
|
||||||
};
|
|
||||||
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>`;
|
||||||
};
|
};
|
||||||
@@ -1,34 +1,32 @@
|
|||||||
const html = require('choo/html');
|
const html = require('choo/html');
|
||||||
|
const raw = require('choo/html/raw');
|
||||||
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) {
|
module.exports = function(state) {
|
||||||
const div = html`
|
return html`
|
||||||
<div id="page-one">
|
<div>
|
||||||
<div id="legal">
|
<div class="title">${state.translate('legalHeader')}</div>
|
||||||
<div class="title">${state.translate('legalHeader')}</div>
|
${raw(
|
||||||
${html(
|
replaceLinks(state.translate('legalNoticeTestPilot'), [
|
||||||
replaceLinks(state.translate('legalNoticeTestPilot'), [
|
'https://testpilot.firefox.com/terms',
|
||||||
'https://testpilot.firefox.com/terms',
|
'https://testpilot.firefox.com/privacy',
|
||||||
'https://testpilot.firefox.com/privacy',
|
'https://testpilot.firefox.com/experiments/send'
|
||||||
'https://testpilot.firefox.com/experiments/send'
|
])
|
||||||
])
|
)}
|
||||||
)}
|
${raw(
|
||||||
${html(
|
replaceLinks(state.translate('legalNoticeMozilla'), [
|
||||||
replaceLinks(state.translate('legalNoticeMozilla'), [
|
'https://www.mozilla.org/privacy/websites/',
|
||||||
'https://www.mozilla.org/privacy/websites/',
|
'https://www.mozilla.org/about/legal/terms/mozilla/'
|
||||||
'https://www.mozilla.org/about/legal/terms/mozilla/'
|
])
|
||||||
])
|
)}
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
return 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>`;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
};
|
|
||||||
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>`;
|
||||||
|
};
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
const div = html`
|
|
||||||
<div id="page-one">
|
|
||||||
<div id="download">
|
|
||||||
<div id="download-page-one">
|
|
||||||
<div class="title">
|
|
||||||
<span>${title}</span>
|
|
||||||
<span id="dl-filesize">${' ' + size}</span>
|
|
||||||
</div>
|
|
||||||
<div class="description">${state.translate('downloadMessage')}</div>
|
|
||||||
<img
|
|
||||||
src="${assets.get('illustration_download.svg')}"
|
|
||||||
id="download-img"
|
|
||||||
title="${state.translate('downloadAltText')}"/>
|
|
||||||
${pageAction}
|
|
||||||
</div>
|
|
||||||
<a class="send-new" href="/">${state.translate('sendYourFilesLink')}</a>
|
|
||||||
</div>
|
|
||||||
${info}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
return 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>
|
||||||
|
`;
|
||||||
|
};
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
/* global EXPIRE_SECONDS */
|
|
||||||
const html = require('choo/html');
|
|
||||||
const assets = require('../../common/assets');
|
|
||||||
const notFound = require('./notFound');
|
|
||||||
const uploadPasswordSet = require('../templates/uploadPasswordSet');
|
|
||||||
const uploadPasswordUnset = require('../templates/uploadPasswordUnset');
|
|
||||||
const selectbox = require('../templates/selectbox');
|
|
||||||
const { allowedCopy, delay, fadeOut } = require('../utils');
|
|
||||||
|
|
||||||
function expireInfo(file, translate, emit) {
|
|
||||||
const hours = Math.floor(EXPIRE_SECONDS / 60 / 60);
|
|
||||||
const el = html([
|
|
||||||
`<div>${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;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = function(state, emit) {
|
|
||||||
const file = state.storage.getFileById(state.params.id);
|
|
||||||
if (!file) {
|
|
||||||
return notFound(state, emit);
|
|
||||||
}
|
|
||||||
|
|
||||||
const passwordSection = file.hasPassword
|
|
||||||
? uploadPasswordSet(state, emit)
|
|
||||||
: uploadPasswordUnset(state, emit);
|
|
||||||
const div = html`
|
|
||||||
<div id="share-link" class="fadeIn">
|
|
||||||
<div class="title">${expireInfo(file, state.translate, emit)}</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=${showPopup}>${state.translate('deleteFileButton')}
|
|
||||||
</button>
|
|
||||||
<div id="deletePopup" 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>
|
|
||||||
<a class="send-new"
|
|
||||||
data-state="completed"
|
|
||||||
href="/"
|
|
||||||
onclick=${sendNew}>${state.translate('sendAnotherFileLink')}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
function showPopup() {
|
|
||||||
const popupText = document.querySelector('.popuptext');
|
|
||||||
popupText.classList.add('show');
|
|
||||||
popupText.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancel(e) {
|
|
||||||
e.stopPropagation();
|
|
||||||
const popupText = document.querySelector('.popuptext');
|
|
||||||
popupText.classList.remove('show');
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
112
app/pages/share/index.js
Normal file
112
app/pages/share/index.js
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
/* 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">
|
||||||
|
${expireInfo(file, state.translate, emit)}
|
||||||
|
<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 class="title">${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 });
|
||||||
|
el.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,52 +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/?utm_campaign=send-acquisition&utm_medium=referral&utm_source=send.firefox.com">
|
|
||||||
<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;
|
|
||||||
};
|
|
||||||
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;
|
||||||
|
}
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
const progress = require('../templates/progress');
|
|
||||||
const { bytes } = require('../utils');
|
|
||||||
|
|
||||||
module.exports = function(state, emit) {
|
|
||||||
const transfer = state.transfer;
|
|
||||||
|
|
||||||
const div = html`
|
|
||||||
<div id="download">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
function cancel() {
|
|
||||||
const btn = document.getElementById('cancel-upload');
|
|
||||||
btn.disabled = true;
|
|
||||||
btn.textContent = state.translate('uploadCancelNotification');
|
|
||||||
emit('cancel');
|
|
||||||
}
|
|
||||||
return div;
|
|
||||||
};
|
|
||||||
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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
/* global MAXFILESIZE */
|
/* global MAXFILESIZE */
|
||||||
const html = require('choo/html');
|
const html = require('choo/html');
|
||||||
const assets = require('../../common/assets');
|
const assets = require('../../../common/assets');
|
||||||
const fileList = require('../templates/fileList');
|
const fileList = require('../../templates/fileList');
|
||||||
const { bytes, fadeOut } = require('../utils');
|
const { bytes, fadeOut } = require('../../utils');
|
||||||
|
|
||||||
module.exports = function(state, emit) {
|
module.exports = function(state, emit) {
|
||||||
const div = html`
|
// the page flickers if both the server and browser set 'effect--fadeIn'
|
||||||
<div id="page-one" class="fadeIn">
|
const fade = state.layout ? '' : 'effect--fadeIn';
|
||||||
|
return html`
|
||||||
|
<div id="page-one" class="${fade}">
|
||||||
<div class="title">${state.translate('uploadPageHeader')}</div>
|
<div class="title">${state.translate('uploadPageHeader')}</div>
|
||||||
<div class="description">
|
<div class="description">
|
||||||
<div>${state.translate('uploadPageExplainer')}</div>
|
<div>${state.translate('uploadPageExplainer')}</div>
|
||||||
@@ -16,27 +18,27 @@ module.exports = function(state, emit) {
|
|||||||
${state.translate('uploadPageLearnMore')}
|
${state.translate('uploadPageLearnMore')}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="upload-window"
|
<div class="uploadArea"
|
||||||
ondragover=${dragover}
|
ondragover=${dragover}
|
||||||
ondragleave=${dragleave}>
|
ondragleave=${dragleave}>
|
||||||
<div id="upload-img">
|
<img
|
||||||
<img
|
src="${assets.get('upload.svg')}"
|
||||||
src="${assets.get('upload.svg')}"
|
title="${state.translate('uploadSvgAlt')}"/>
|
||||||
title="${state.translate('uploadSvgAlt')}"/>
|
<div class="uploadArea__msg">
|
||||||
|
${state.translate('uploadPageDropMessage')}
|
||||||
</div>
|
</div>
|
||||||
<div id="upload-text">${state.translate('uploadPageDropMessage')}</div>
|
<span class="uploadArea__sizeMsg">
|
||||||
<span id="file-size-msg">
|
${state.translate('uploadPageSizeMessage')}
|
||||||
<em>${state.translate('uploadPageSizeMessage')}</em>
|
|
||||||
</span>
|
</span>
|
||||||
<input id="file-upload"
|
<input id="file-upload"
|
||||||
|
class="inputFile"
|
||||||
type="file"
|
type="file"
|
||||||
name="fileUploaded"
|
name="fileUploaded"
|
||||||
onfocus=${onfocus}
|
onfocus=${onfocus}
|
||||||
onblur=${onblur}
|
onblur=${onblur}
|
||||||
onchange=${upload} />
|
onchange=${upload} />
|
||||||
<label for="file-upload"
|
<label for="file-upload"
|
||||||
id="browse"
|
class="btn btn--file"
|
||||||
class="btn browse"
|
|
||||||
title="${state.translate('uploadPageBrowseButton1')}">
|
title="${state.translate('uploadPageBrowseButton1')}">
|
||||||
${state.translate('uploadPageBrowseButton1')}
|
${state.translate('uploadPageBrowseButton1')}
|
||||||
</label>
|
</label>
|
||||||
@@ -46,21 +48,21 @@ module.exports = function(state, emit) {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
function dragover(event) {
|
function dragover(event) {
|
||||||
const div = document.querySelector('.upload-window');
|
const div = document.querySelector('.uploadArea');
|
||||||
div.classList.add('ondrag');
|
div.classList.add('uploadArea--dragging');
|
||||||
}
|
}
|
||||||
|
|
||||||
function dragleave(event) {
|
function dragleave(event) {
|
||||||
const div = document.querySelector('.upload-window');
|
const div = document.querySelector('.uploadArea');
|
||||||
div.classList.remove('ondrag');
|
div.classList.remove('uploadArea--dragging');
|
||||||
}
|
}
|
||||||
|
|
||||||
function onfocus(event) {
|
function onfocus(event) {
|
||||||
event.target.classList.add('has-focus');
|
event.target.classList.add('inputFile--focused');
|
||||||
}
|
}
|
||||||
|
|
||||||
function onblur(event) {
|
function onblur(event) {
|
||||||
event.target.classList.remove('has-focus');
|
event.target.classList.remove('inputFile--focused');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function upload(event) {
|
async function upload(event) {
|
||||||
@@ -71,12 +73,12 @@ module.exports = function(state, emit) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (file.size > MAXFILESIZE) {
|
if (file.size > MAXFILESIZE) {
|
||||||
window.alert(state.translate('fileTooBig', { size: bytes(MAXFILESIZE) }));
|
// eslint-disable-next-line no-alert
|
||||||
|
alert(state.translate('fileTooBig', { size: bytes(MAXFILESIZE) }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await fadeOut('page-one');
|
await fadeOut('#page-one');
|
||||||
emit('upload', { file, type: 'click' });
|
emit('upload', { file, type: 'click' });
|
||||||
}
|
}
|
||||||
return div;
|
|
||||||
};
|
};
|
||||||
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;
|
||||||
|
}
|
||||||
25
app/pasteManager.js
Normal file
25
app/pasteManager.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/* global MAXFILESIZE */
|
||||||
|
import { bytes } from './utils';
|
||||||
|
|
||||||
|
export default function(state, emitter) {
|
||||||
|
window.addEventListener('paste', event => {
|
||||||
|
if (state.route !== '/' || state.uploading) return;
|
||||||
|
|
||||||
|
for (const item of event.clipboardData.items) {
|
||||||
|
if (!item.type.includes('image')) continue;
|
||||||
|
|
||||||
|
const file = item.getAsFile();
|
||||||
|
|
||||||
|
if (!file) continue; // Sometimes null
|
||||||
|
|
||||||
|
if (file.size > MAXFILESIZE) {
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
|
alert(state.translate('fileTooBig', { size: bytes(MAXFILESIZE) }));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
emitter.emit('upload', { file, type: 'paste' });
|
||||||
|
return; // return here since only one file is allowed to be uploaded at a time
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
9
app/readme.md
Normal file
9
app/readme.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Application Code
|
||||||
|
|
||||||
|
`app/` contains the browser code that gets bundled into `app.[hash].js`. It's got all the logic, crypto, and UI. All of it gets used in the browser, and some of it by the server for server side rendering.
|
||||||
|
|
||||||
|
The main entrypoint for the browser is [main.js](./main.js) and on the server [routes/index.js](./routes/index.js) gets imported by [/server/routes/pages.js](../server/routes/pages.js)
|
||||||
|
|
||||||
|
- `pages` contains display logic an markup for pages
|
||||||
|
- `routes` contains route definitions and logic
|
||||||
|
- `templates` contains ui elements smaller than pages
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
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) {
|
function banner(state, emit) {
|
||||||
@@ -18,18 +20,20 @@ function body(template) {
|
|||||||
const b = html`<body>
|
const b = html`<body>
|
||||||
${banner(state, emit)}
|
${banner(state, emit)}
|
||||||
${header(state)}
|
${header(state)}
|
||||||
<div class="all">
|
<main class="main">
|
||||||
<noscript>
|
<noscript>
|
||||||
<h2>${state.translate('javascriptRequired')}</h2>
|
<div class="noscript">
|
||||||
<p>
|
<h2>${state.translate('javascriptRequired')}</h2>
|
||||||
<a href="https://github.com/mozilla/send/blob/master/docs/faq.md#why-does-firefox-send-require-javascript">
|
<p>
|
||||||
${state.translate('whyJavascript')}
|
<a class="link" href="https://github.com/mozilla/send/blob/master/docs/faq.md#why-does-firefox-send-require-javascript">
|
||||||
</a>
|
${state.translate('whyJavascript')}
|
||||||
</p>
|
</a>
|
||||||
<p>${state.translate('enableJavascript')}</p>
|
</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) {
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
@@ -1,16 +1,13 @@
|
|||||||
const html = require('choo/html');
|
const html = require('choo/html');
|
||||||
|
|
||||||
module.exports = function(state, emit) {
|
module.exports = function(state, emit) {
|
||||||
|
return html`
|
||||||
|
<button class="btn btn--download"
|
||||||
|
onclick=${download}>${state.translate('downloadButtonLabel')}
|
||||||
|
</button>`;
|
||||||
|
|
||||||
function download(event) {
|
function download(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
emit('download', state.fileInfo);
|
emit('download', state.fileInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
|
||||||
<div>
|
|
||||||
<button id="download-btn"
|
|
||||||
class="btn"
|
|
||||||
onclick=${download}>${state.translate('downloadButtonLabel')}
|
|
||||||
</button>
|
|
||||||
</div>`;
|
|
||||||
};
|
};
|
||||||
@@ -1,59 +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"
|
|
||||||
autofocus />
|
|
||||||
<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('getMetadata');
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
||||||
|
};
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,22 +1,7 @@
|
|||||||
const html = require('choo/html');
|
const html = require('choo/html');
|
||||||
const assets = require('../../common/assets');
|
const assets = require('../../../common/assets');
|
||||||
|
const number = require('../../utils').number;
|
||||||
function timeLeft(milliseconds, state) {
|
const deletePopup = require('../popup');
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = function(file, state, emit) {
|
module.exports = function(file, state, emit) {
|
||||||
const ttl = file.expiresAt - Date.now();
|
const ttl = file.expiresAt - Date.now();
|
||||||
@@ -24,42 +9,39 @@ module.exports = function(file, state, emit) {
|
|||||||
timeLeft(ttl, state) || state.translate('linkExpiredAlt');
|
timeLeft(ttl, state) || state.translate('linkExpiredAlt');
|
||||||
const downloadLimit = file.dlimit || 1;
|
const downloadLimit = file.dlimit || 1;
|
||||||
const totalDownloads = file.dtotal || 0;
|
const totalDownloads = file.dtotal || 0;
|
||||||
const row = html`
|
return html`
|
||||||
<tr id="${file.id}">
|
<tr id="${file.id}">
|
||||||
<td class="overflow-col" title="${file.name}">
|
<td class="fileData fileData--overflow" title="${file.name}">
|
||||||
<a class="link" href="/share/${file.id}">${file.name}</a>
|
<a class="link" href="/share/${file.id}">${file.name}</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="center-col">
|
<td class="fileData fileData--center">
|
||||||
<img
|
<img
|
||||||
onclick=${copyClick}
|
onclick=${copyClick}
|
||||||
src="${assets.get('copy-16.svg')}"
|
src="${assets.get('copy-16.svg')}"
|
||||||
class="icon-copy"
|
class="cursor--pointer"
|
||||||
title="${state.translate('copyUrlHover')}">
|
title="${state.translate('copyUrlHover')}"
|
||||||
<span class="text-copied" hidden="true">
|
tabindex="0">
|
||||||
|
<span hidden="true">
|
||||||
${state.translate('copiedUrl')}
|
${state.translate('copiedUrl')}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>${remainingTime}</td>
|
<td class="fileData fileData--overflow">${remainingTime}</td>
|
||||||
<td class="center-col">${totalDownloads} / ${downloadLimit}</td>
|
<td class="fileData fileData--center">${number(totalDownloads)} / ${number(
|
||||||
<td class="center-col">
|
downloadLimit
|
||||||
|
)}</td>
|
||||||
|
<td class="fileData fileData--center">
|
||||||
<img
|
<img
|
||||||
onclick=${showPopup}
|
onclick=${showPopup}
|
||||||
src="${assets.get('close-16.svg')}"
|
src="${assets.get('close-16.svg')}"
|
||||||
class="icon-delete"
|
class="cursor--pointer"
|
||||||
title="${state.translate('deleteButtonHover')}">
|
title="${state.translate('deleteButtonHover')}"
|
||||||
<div class="popup">
|
tabindex="0">
|
||||||
<div class="popuptext" onblur=${cancel} tabindex="-1">
|
${deletePopup(
|
||||||
<div class="popup-message">${state.translate('deletePopupText')}</div>
|
state.translate('deletePopupText'),
|
||||||
<div class="popup-action">
|
state.translate('deletePopupYes'),
|
||||||
<span class="popup-no" onclick=${cancel}>
|
state.translate('deletePopupCancel'),
|
||||||
${state.translate('deletePopupCancel')}
|
deleteFile
|
||||||
</span>
|
)}
|
||||||
<span class="popup-yes" onclick=${deleteFile}>
|
|
||||||
${state.translate('deletePopupYes')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
@@ -78,22 +60,30 @@ module.exports = function(file, state, emit) {
|
|||||||
|
|
||||||
function showPopup() {
|
function showPopup() {
|
||||||
const tr = document.getElementById(file.id);
|
const tr = document.getElementById(file.id);
|
||||||
const popup = tr.querySelector('.popuptext');
|
const popup = tr.querySelector('.popup');
|
||||||
popup.classList.add('show');
|
popup.classList.add('popup--show');
|
||||||
popup.focus();
|
popup.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancel(e) {
|
|
||||||
e.stopPropagation();
|
|
||||||
const tr = document.getElementById(file.id);
|
|
||||||
const popup = tr.querySelector('.popuptext');
|
|
||||||
popup.classList.remove('show');
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteFile() {
|
function deleteFile() {
|
||||||
emit('delete', { file, location: 'upload-list' });
|
emit('delete', { file, location: 'upload-list' });
|
||||||
emit('render');
|
emit('render');
|
||||||
}
|
}
|
||||||
|
|
||||||
return row;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,37 +1,33 @@
|
|||||||
const html = require('choo/html');
|
const html = require('choo/html');
|
||||||
const file = require('./file');
|
const file = require('../file');
|
||||||
|
|
||||||
module.exports = function(state, emit) {
|
module.exports = function(state, emit) {
|
||||||
let table = '';
|
|
||||||
if (state.storage.files.length) {
|
if (state.storage.files.length) {
|
||||||
table = html`
|
return html`
|
||||||
<table id="uploaded-files">
|
<table class="fileList">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th id="uploaded-file">${state.translate('uploadedFile')}</th>
|
<th class="fileList__header fileList__nameCol">
|
||||||
<th id="copy-file-list" class="center-col">
|
${state.translate('uploadedFile')}
|
||||||
|
</th>
|
||||||
|
<th class="fileList__header fileList__copyCol">
|
||||||
${state.translate('copyFileList')}
|
${state.translate('copyFileList')}
|
||||||
</th>
|
</th>
|
||||||
<th id="expiry-time-file-list" >
|
<th class="fileList__header fileList__expireCol" >
|
||||||
${state.translate('timeFileList')}
|
${state.translate('timeFileList')}
|
||||||
</th>
|
</th>
|
||||||
<th id="expiry-downloads-file-list" >
|
<th class="fileList__header fileList__dlCol" >
|
||||||
${state.translate('downloadsFileList')}
|
${state.translate('downloadsFileList')}
|
||||||
</th>
|
</th>
|
||||||
<th id="delete-file-list" class="center-col">
|
<th class="fileList__header fileList__delCol">
|
||||||
${state.translate('deleteFileList')}
|
${state.translate('deleteFileList')}
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody class="fileList__body">
|
||||||
${state.storage.files.map(f => file(f, state, emit))}
|
${state.storage.files.map(f => file(f, state, emit))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
return html`
|
|
||||||
<div id="file-list">
|
|
||||||
${table}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
};
|
};
|
||||||
@@ -1,43 +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>
|
|
||||||
<a href="https://www.mozilla.org/about/legal/report-infringement/">
|
|
||||||
${state.translate('reportIPInfringement')}
|
|
||||||
</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;
|
||||||
|
}
|
||||||
|
}
|
||||||
68
app/templates/footer/index.js
Normal file
68
app/templates/footer/index.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const assets = require('../../../common/assets');
|
||||||
|
|
||||||
|
module.exports = function(state) {
|
||||||
|
const footer = html`<footer class="footer">
|
||||||
|
<div class="legalSection">
|
||||||
|
<a
|
||||||
|
href="https://www.mozilla.org"
|
||||||
|
class="legalSection__link">
|
||||||
|
<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">
|
||||||
|
<img
|
||||||
|
class="socialSection__icon"
|
||||||
|
src="${assets.get('github-icon.svg')}"
|
||||||
|
alt="github"/>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://twitter.com/FxTestPilot"
|
||||||
|
class="socialSection__link">
|
||||||
|
<img
|
||||||
|
class="socialSection__icon"
|
||||||
|
src="${assets.get('twitter-icon.svg')}"
|
||||||
|
alt="twitter"/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</footer>`;
|
||||||
|
// HACK
|
||||||
|
// We only want to render this once because we
|
||||||
|
// toggle the targets of the links with utils/openLinksInNewTab
|
||||||
|
footer.isSameNode = function(target) {
|
||||||
|
return target && target.nodeName && target.nodeName === 'FOOTER';
|
||||||
|
};
|
||||||
|
return footer;
|
||||||
|
};
|
||||||
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;
|
||||||
|
}
|
||||||
@@ -1,17 +1,14 @@
|
|||||||
const html = require('choo/html');
|
const html = require('choo/html');
|
||||||
const assets = require('../../common/assets');
|
const assets = require('../../../common/assets');
|
||||||
|
|
||||||
module.exports = function(state, emit) {
|
module.exports = function(state, emit) {
|
||||||
function clicked() {
|
let classes = 'fxPromo';
|
||||||
emit('experiment', { cd3: 'promo' });
|
|
||||||
}
|
|
||||||
let classes = 'banner';
|
|
||||||
switch (state.promo) {
|
switch (state.promo) {
|
||||||
case 'blue':
|
case 'blue':
|
||||||
classes = 'banner banner-blue';
|
classes = 'fxPromo fxPromo--blue';
|
||||||
break;
|
break;
|
||||||
case 'pink':
|
case 'pink':
|
||||||
classes = 'banner banner-pink';
|
classes = 'fxPromo fxPromo--pink';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,7 +17,7 @@ module.exports = function(state, emit) {
|
|||||||
<div>
|
<div>
|
||||||
<img
|
<img
|
||||||
src="${assets.get('firefox_logo-only.svg')}"
|
src="${assets.get('firefox_logo-only.svg')}"
|
||||||
class="firefox-logo-small"
|
class="fxPromo__logo"
|
||||||
alt="Firefox"/>
|
alt="Firefox"/>
|
||||||
<span>Send is brought to you by the all-new Firefox.
|
<span>Send is brought to you by the all-new Firefox.
|
||||||
<a
|
<a
|
||||||
@@ -30,4 +27,8 @@ module.exports = function(state, emit) {
|
|||||||
>Download Firefox now ≫</a></span>
|
>Download Firefox now ≫</a></span>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
|
function clicked() {
|
||||||
|
emit('experiment', { cd3: 'promo' });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
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';
|
|
||||||
|
|
||||||
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';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const browser = browserName();
|
|
||||||
|
|
||||||
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?ver=${version}&browser=${browser}"
|
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
67
app/templates/header/index.js
Normal file
67
app/templates/header/index.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
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}`;
|
||||||
|
const header = 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>`;
|
||||||
|
// HACK
|
||||||
|
// We only want to render this once because we
|
||||||
|
// toggle the targets of the links with utils/openLinksInNewTab
|
||||||
|
header.isSameNode = function(target) {
|
||||||
|
return target && target.nodeName && target.nodeName === 'HEADER';
|
||||||
|
};
|
||||||
|
return 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';
|
||||||
|
}
|
||||||
|
}
|
||||||
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');
|
||||||
|
}
|
||||||
41
app/templates/passwordInput/passwordInput.css
Normal file
41
app/templates/passwordInput/passwordInput.css
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
.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__form {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
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,41 +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;
|
|
||||||
};
|
|
||||||
56
app/templates/progress/index.js
Normal file
56
app/templates/progress/index.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
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) {
|
||||||
|
// HACK - never indefinite for MS Edge
|
||||||
|
if (/edge/i.test(navigator.userAgent)) {
|
||||||
|
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="progress__indefinite ${indefinite ? '' : 'progress--invisible'}"
|
||||||
|
r="${radius}"
|
||||||
|
cx="${oRadius}"
|
||||||
|
cy="${oRadius}"
|
||||||
|
fill="transparent"
|
||||||
|
transform="rotate(-90 ${oRadius} ${oRadius})"
|
||||||
|
stroke-dasharray="${circumference}"
|
||||||
|
stroke-dashoffset="${dashOffset}"/>
|
||||||
|
<circle
|
||||||
|
class="progress__bar ${indefinite ? 'progress--invisible' : ''}"
|
||||||
|
r="${radius}"
|
||||||
|
cx="${oRadius}"
|
||||||
|
cy="${oRadius}"
|
||||||
|
fill="transparent"
|
||||||
|
transform="rotate(-90 ${oRadius} ${oRadius})"
|
||||||
|
stroke-dasharray="${circumference}"
|
||||||
|
stroke-dashoffset="${dashOffset}"/>
|
||||||
|
${indefinite ? '' : progressPercent}
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
43
app/templates/progress/progress.css
Normal file
43
app/templates/progress/progress.css
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress--invisible {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
|
|
||||||
module.exports = function(selected, options, translate, changed) {
|
|
||||||
const id = `select-${Math.random()}`;
|
|
||||||
let x = selected;
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
const ul = document.getElementById(id);
|
|
||||||
const body = document.querySelector('body');
|
|
||||||
ul.classList.remove('active');
|
|
||||||
body.removeEventListener('click', close);
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle(event) {
|
|
||||||
event.stopPropagation();
|
|
||||||
const ul = document.getElementById(id);
|
|
||||||
if (ul.classList.contains('active')) {
|
|
||||||
close();
|
|
||||||
} else {
|
|
||||||
ul.classList.add('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();
|
|
||||||
}
|
|
||||||
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="selectOptions">
|
|
||||||
${options.map(
|
|
||||||
i =>
|
|
||||||
html`<li class="selectOption" onclick=${choose} data-value="${i}">${i}</li>`
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
</div>`;
|
|
||||||
};
|
|
||||||
28
app/templates/selectbox/index.js
Normal file
28
app/templates/selectbox/index.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
|
||||||
|
module.exports = function(selected, options, translate, changed) {
|
||||||
|
const id = `select-${Math.random()}`;
|
||||||
|
let x = selected;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="select">
|
||||||
|
<select id="${id}" onchange=${choose}>
|
||||||
|
${options.map(
|
||||||
|
i =>
|
||||||
|
html`<option value="${i}" ${
|
||||||
|
i === selected ? 'selected' : ''
|
||||||
|
}>${translate(i)}</option>`
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
function choose(event) {
|
||||||
|
const target = event.target;
|
||||||
|
const value = +target.value;
|
||||||
|
|
||||||
|
if (x !== value) {
|
||||||
|
x = value;
|
||||||
|
changed(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
46
app/templates/selectbox/selectbox.css
Normal file
46
app/templates/selectbox/selectbox.css
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
.select {
|
||||||
|
background-color: var(--pageBGColor);
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 4px 2px 4px 2px;
|
||||||
|
border: 1px dotted #0094fb88;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: inline;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select::after {
|
||||||
|
color: #0094fb;
|
||||||
|
content: '\25BC';
|
||||||
|
pointer-events: none;
|
||||||
|
font-size: 20px;
|
||||||
|
margin-left: -30px;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
option {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
appearance: none;
|
||||||
|
outline: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
border: 0;
|
||||||
|
background: #fff;
|
||||||
|
background-image: none;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 200;
|
||||||
|
margin: 0;
|
||||||
|
color: #0094fb;
|
||||||
|
cursor: pointer;
|
||||||
|
padding-right: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
select:active {
|
||||||
|
background-color: var(--pageBGColor);
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#arrow {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
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 = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
69
app/templates/setPasswordSection/setPasswordSection.css
Normal file
69
app/templates/setPasswordSection/setPasswordSection.css
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
.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);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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,80 +0,0 @@
|
|||||||
const html = require('choo/html');
|
|
||||||
|
|
||||||
module.exports = function(state, emit) {
|
|
||||||
const file = state.storage.getFileById(state.params.id);
|
|
||||||
|
|
||||||
return html`<div class="selectPassword">
|
|
||||||
${passwordSpan(file.password)}
|
|
||||||
<button
|
|
||||||
id="resetButton"
|
|
||||||
onclick=${toggleResetInput}
|
|
||||||
>${state.translate('changePasswordButton')}</button>
|
|
||||||
<form
|
|
||||||
id='reset-form'
|
|
||||||
class="setPassword hidden"
|
|
||||||
onsubmit=${resetPassword}
|
|
||||||
data-no-csrf>
|
|
||||||
<input id="unlock-reset-input"
|
|
||||||
class="unlock-input input-no-btn"
|
|
||||||
maxlength="32"
|
|
||||||
autocomplete="off"
|
|
||||||
type="password"
|
|
||||||
oninput=${inputChanged}
|
|
||||||
placeholder="${state.translate('unlockInputPlaceholder')}">
|
|
||||||
<input type="submit"
|
|
||||||
id="unlock-reset-btn"
|
|
||||||
class="btn btn-hidden"
|
|
||||||
value="${state.translate('changePasswordButton')}"/>
|
|
||||||
</form>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
function passwordSpan(password) {
|
|
||||||
password = password || '●●●●●';
|
|
||||||
const span = html([
|
|
||||||
`<span>${state.translate('passwordResult', {
|
|
||||||
password:
|
|
||||||
'<pre class="passwordOriginal"></pre><pre class="passwordMask"></pre>'
|
|
||||||
})}</span>`
|
|
||||||
]);
|
|
||||||
const og = span.querySelector('.passwordOriginal');
|
|
||||||
const masked = span.querySelector('.passwordMask');
|
|
||||||
og.textContent = password;
|
|
||||||
masked.textContent = password.replace(/./g, '●');
|
|
||||||
return span;
|
|
||||||
}
|
|
||||||
|
|
||||||
function inputChanged() {
|
|
||||||
const resetInput = document.getElementById('unlock-reset-input');
|
|
||||||
const resetBtn = document.getElementById('unlock-reset-btn');
|
|
||||||
if (resetInput.value.length > 0) {
|
|
||||||
resetBtn.classList.remove('btn-hidden');
|
|
||||||
resetInput.classList.remove('input-no-btn');
|
|
||||||
} else {
|
|
||||||
resetBtn.classList.add('btn-hidden');
|
|
||||||
resetInput.classList.add('input-no-btn');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetPassword(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const password = document.querySelector('#unlock-reset-input').value;
|
|
||||||
if (password.length > 0) {
|
|
||||||
document.getElementById('copy').classList.remove('wait-password');
|
|
||||||
document.getElementById('copy-btn').disabled = false;
|
|
||||||
emit('password', { password, file });
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleResetInput(event) {
|
|
||||||
const form = event.target.parentElement.querySelector('form');
|
|
||||||
const input = document.getElementById('unlock-reset-input');
|
|
||||||
if (form.style.visibility === 'hidden' || form.style.visibility === '') {
|
|
||||||
form.style.visibility = 'visible';
|
|
||||||
input.focus();
|
|
||||||
} else {
|
|
||||||
form.style.visibility = 'hidden';
|
|
||||||
}
|
|
||||||
inputChanged();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,70 +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="32"
|
|
||||||
autocomplete="off"
|
|
||||||
placeholder="${state.translate('unlockInputPlaceholder')}"
|
|
||||||
type="password"
|
|
||||||
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 false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return div;
|
|
||||||
};
|
|
||||||
60
app/utils.js
60
app/utils.js
@@ -9,10 +9,7 @@ function arrayToB64(array) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function b64ToArray(str) {
|
function b64ToArray(str) {
|
||||||
str = (str + '==='.slice((str.length + 3) % 4))
|
return b64.toByteArray(str + '==='.slice((str.length + 3) % 4));
|
||||||
.replace(/-/g, '+')
|
|
||||||
.replace(/_/g, '/');
|
|
||||||
return b64.toByteArray(str);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadShim(polyfill) {
|
function loadShim(polyfill) {
|
||||||
@@ -25,7 +22,7 @@ function loadShim(polyfill) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function canHasSend(polyfill) {
|
async function canHasSend() {
|
||||||
try {
|
try {
|
||||||
const key = await window.crypto.subtle.generateKey(
|
const key = await window.crypto.subtle.generateKey(
|
||||||
{
|
{
|
||||||
@@ -35,7 +32,6 @@ async function canHasSend(polyfill) {
|
|||||||
true,
|
true,
|
||||||
['encrypt', 'decrypt']
|
['encrypt', 'decrypt']
|
||||||
);
|
);
|
||||||
|
|
||||||
await window.crypto.subtle.encrypt(
|
await window.crypto.subtle.encrypt(
|
||||||
{
|
{
|
||||||
name: 'AES-GCM',
|
name: 'AES-GCM',
|
||||||
@@ -45,9 +41,24 @@ async function canHasSend(polyfill) {
|
|||||||
key,
|
key,
|
||||||
new ArrayBuffer(8)
|
new ArrayBuffer(8)
|
||||||
);
|
);
|
||||||
|
await window.crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
window.crypto.getRandomValues(new Uint8Array(16)),
|
||||||
|
'PBKDF2',
|
||||||
|
false,
|
||||||
|
['deriveKey']
|
||||||
|
);
|
||||||
|
await window.crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
window.crypto.getRandomValues(new Uint8Array(16)),
|
||||||
|
'HKDF',
|
||||||
|
false,
|
||||||
|
['deriveKey']
|
||||||
|
);
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return loadShim(polyfill);
|
console.error(err);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,6 +128,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;
|
||||||
@@ -126,29 +145,13 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
function openLinksInNewTab(links, should = true) {
|
||||||
links = links || Array.from(document.querySelectorAll('a:not([target])'));
|
links = links || Array.from(document.querySelectorAll('a:not([target])'));
|
||||||
if (should) {
|
if (should) {
|
||||||
@@ -171,11 +174,12 @@ module.exports = {
|
|||||||
allowedCopy,
|
allowedCopy,
|
||||||
bytes,
|
bytes,
|
||||||
percent,
|
percent,
|
||||||
|
number,
|
||||||
copyToClipboard,
|
copyToClipboard,
|
||||||
arrayToB64,
|
arrayToB64,
|
||||||
b64ToArray,
|
b64ToArray,
|
||||||
|
loadShim,
|
||||||
canHasSend,
|
canHasSend,
|
||||||
isFile,
|
isFile,
|
||||||
saveFile,
|
|
||||||
openLinksInNewTab
|
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 |
File diff suppressed because one or more lines are too long
1236
assets/main.css
1236
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,5 @@
|
|||||||
last 2 chrome versions
|
last 2 chrome versions
|
||||||
last 2 firefox versions
|
last 2 firefox versions
|
||||||
|
last 2 safari versions
|
||||||
|
last 2 edge versions
|
||||||
firefox esr
|
firefox esr
|
||||||
ie >= 9
|
|
||||||
safari >= 9
|
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
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');
|
const fs = require('fs');
|
||||||
|
|
||||||
function toJSON(map) {
|
function toJSON(map) {
|
||||||
@@ -36,22 +39,25 @@ module.exports = function(source) {
|
|||||||
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});
|
(function () {
|
||||||
ctx._messages = new Map(${toJSON(merged)});
|
var ctx = new fluent.MessageContext('${locale}', {useIsolating: false});
|
||||||
function translate(id, data) {
|
ctx._messages = new Map(${toJSON(merged)});
|
||||||
var msg = ctx.getMessage(id);
|
function translate(id, data) {
|
||||||
if (typeof(msg) !== 'string' && !msg.val && msg.attrs) {
|
var msg = ctx.getMessage(id);
|
||||||
msg = msg.attrs.title || msg.attrs.alt
|
if (typeof(msg) !== 'string' && !msg.val && msg.attrs) {
|
||||||
|
msg = msg.attrs.title || msg.attrs.alt
|
||||||
|
}
|
||||||
|
return ctx.format(msg, data);
|
||||||
}
|
}
|
||||||
return ctx.format(msg, data);
|
if (typeof window === 'undefined') {
|
||||||
}
|
module.exports = translate;
|
||||||
if (typeof window === 'undefined') {
|
}
|
||||||
module.exports = translate;
|
else {
|
||||||
}
|
window.translate = translate;
|
||||||
else {
|
}
|
||||||
window.translate = translate;
|
})();
|
||||||
}
|
|
||||||
\``;
|
\``;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,14 @@
|
|||||||
|
/*
|
||||||
|
This code is included by both the server and frontend via
|
||||||
|
common/assets.js
|
||||||
|
|
||||||
|
When included from the server the export will be the function.
|
||||||
|
|
||||||
|
When included from the frontend (via webpack) the export will
|
||||||
|
be an object mapping file names to hashed file names. Example:
|
||||||
|
"send_logo.svg": "send_logo.5fcfdf0e.svg"
|
||||||
|
*/
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
@@ -14,6 +25,6 @@ module.exports = function() {
|
|||||||
return {
|
return {
|
||||||
code,
|
code,
|
||||||
dependencies: files.map(f => require.resolve('../assets/' + f)),
|
dependencies: files.map(f => require.resolve('../assets/' + f)),
|
||||||
cacheable: false
|
cacheable: true
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,14 @@
|
|||||||
|
/*
|
||||||
|
This code is included by both the server and frontend via
|
||||||
|
common/locales.js
|
||||||
|
|
||||||
|
When included from the server the export will be the function.
|
||||||
|
|
||||||
|
When included from the frontend (via webpack) the export will
|
||||||
|
be an object mapping ftl files to js files. Example:
|
||||||
|
"public/locales/en-US/send.ftl":"public/locales/en-US/send.6b4f8354.js"
|
||||||
|
*/
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
@@ -17,6 +28,6 @@ module.exports = function() {
|
|||||||
dependencies: dirs.map(d =>
|
dependencies: dirs.map(d =>
|
||||||
require.resolve(`../public/locales/${d}/send.ftl`)
|
require.resolve(`../public/locales/${d}/send.ftl`)
|
||||||
),
|
),
|
||||||
cacheable: false
|
cacheable: true
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
26
build/readme.md
Normal file
26
build/readme.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Custom Loaders
|
||||||
|
|
||||||
|
## Fluent Loader
|
||||||
|
|
||||||
|
The fluent loader "compiles" `.ftl` files into `.js` files directly usable by both the frontend and server for localization.
|
||||||
|
|
||||||
|
## Generate Asset Map
|
||||||
|
|
||||||
|
This loader enumerates all the files in `assets/` so that `common/assets.js` can provide mappings from the source filename to the hashed filename used on the site.
|
||||||
|
|
||||||
|
## Generate L10N Map
|
||||||
|
|
||||||
|
This loader enumerates all the ftl files in `public/locales` so that the fluent loader can create it's js files.
|
||||||
|
|
||||||
|
## Package.json Loader
|
||||||
|
|
||||||
|
This loader creates a `version.json` file that gets exposed by the `/__version__` route from the `package.json` file and current git commit hash.
|
||||||
|
|
||||||
|
## Version Loader
|
||||||
|
|
||||||
|
This loader substitutes the string "VERSION" for the version string specified in `package.json`. This is a workaround because `package.json` already uses the `package_json_loader`. See [app/templates/header/index.js](../app/templates/header/index.js) for more info.
|
||||||
|
|
||||||
|
# See Also
|
||||||
|
|
||||||
|
- [docs/build.md](../docs/build.md)
|
||||||
|
- [webpack.config.js](../webpack.config.js)
|
||||||
171
circle.yml
171
circle.yml
@@ -1,35 +1,136 @@
|
|||||||
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
|
- ./dist
|
||||||
- docker build -t mozilla/send:latest .
|
test:
|
||||||
- docker push mozilla/send:latest
|
docker:
|
||||||
tags:
|
- image: circleci/node:8-browsers
|
||||||
tag: /.*/
|
steps:
|
||||||
owner: mozilla
|
- checkout
|
||||||
commands:
|
- restore_cache:
|
||||||
- docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
|
key: send-{{ checksum "package-lock.json" }}
|
||||||
- docker build -t mozilla/send:$CIRCLE_TAG .
|
- run: npm install
|
||||||
- docker push mozilla/send:$CIRCLE_TAG
|
- save_cache:
|
||||||
|
key: send-{{ checksum "package-lock.json" }}
|
||||||
test:
|
paths:
|
||||||
override:
|
- node_modules
|
||||||
- npm run build
|
- run: npm run check
|
||||||
- npm run lint
|
- run: npm run lint
|
||||||
- npm test
|
- run: npm run test
|
||||||
- nsp check
|
- store_artifacts:
|
||||||
|
path: coverage
|
||||||
|
integration_tests:
|
||||||
|
machine: true
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- attach_workspace:
|
||||||
|
at: .
|
||||||
|
- run:
|
||||||
|
name: Install Docker Compose
|
||||||
|
command: |
|
||||||
|
set -x
|
||||||
|
pip install docker-compose>=1.18
|
||||||
|
docker-compose --version
|
||||||
|
- run:
|
||||||
|
command: npm run test-integration
|
||||||
|
- store_artifacts:
|
||||||
|
path: coverage/send-test.html
|
||||||
|
deploy_dev:
|
||||||
|
machine: true
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- 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:
|
||||||
|
- checkout
|
||||||
|
- 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:
|
||||||
|
- build:
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
ignore: master
|
||||||
|
- test:
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
ignore: master
|
||||||
|
- integration_tests:
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
ignore: master
|
||||||
|
requires:
|
||||||
|
- build
|
||||||
|
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.*/
|
||||||
|
- integration_tests:
|
||||||
|
requires:
|
||||||
|
- build
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
ignore: /.*/
|
||||||
|
tags:
|
||||||
|
only: /^v.*/
|
||||||
|
- deploy_stage:
|
||||||
|
requires:
|
||||||
|
- build
|
||||||
|
- test
|
||||||
|
- integration_tests
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
ignore: /.*/
|
||||||
|
tags:
|
||||||
|
only: /^v.*/
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const isServer = typeof gen === 'function';
|
|||||||
const prefix = isServer ? '/' : '';
|
const prefix = isServer ? '/' : '';
|
||||||
let manifest = {};
|
let manifest = {};
|
||||||
try {
|
try {
|
||||||
//eslint-disable-next-line node/no-missing-require
|
// eslint-disable-next-line node/no-missing-require
|
||||||
manifest = require('../dist/manifest.json');
|
manifest = require('../dist/manifest.json');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// use middleware
|
// use middleware
|
||||||
@@ -17,6 +17,7 @@ function getLocale(name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function serverTranslator(name) {
|
function serverTranslator(name) {
|
||||||
|
// eslint-disable-next-line security/detect-non-literal-require
|
||||||
return require(`../dist/${locales[`public/locales/${name}/send.ftl`]}`);
|
return require(`../dist/${locales[`public/locales/${name}/send.ftl`]}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
3
common/readme.md
Normal file
3
common/readme.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Common Code
|
||||||
|
|
||||||
|
This directory contains code loaded by both the frontend `app` and backend `server`. The code here can be challenging to understand at first because the contexts for the two (three counting the dev server) environments that include them are quite different, but the purpose of these modules are quite simple, to provide mappings from the source assets (`copy-16.png`) to the concrete production assets (`copy-16.db66e0bf.svg`), similarly for localizations.
|
||||||
@@ -10,3 +10,17 @@ services:
|
|||||||
- REDIS_HOST=redis
|
- REDIS_HOST=redis
|
||||||
redis:
|
redis:
|
||||||
image: redis:alpine
|
image: redis:alpine
|
||||||
|
selenium:
|
||||||
|
image: b4handjr/selenium-firefox
|
||||||
|
ports:
|
||||||
|
- "${VNC_PORT:-5900}:5900"
|
||||||
|
shm_size: 2g
|
||||||
|
integration-tests:
|
||||||
|
build: ./test/integration
|
||||||
|
environment:
|
||||||
|
- BASE_URL=${BASE_URL:-http://web:1443}
|
||||||
|
links:
|
||||||
|
- web
|
||||||
|
- selenium
|
||||||
|
volumes:
|
||||||
|
- "./coverage:/coverage"
|
||||||
|
|||||||
22
docs/build.md
Normal file
22
docs/build.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
Send has two build configurations, development and production. Both can be run via `npm` scripts, `npm start` for development and `npm run build` for production. Webpack is our only build tool and all configuration lives in [webpack.config.js](../webpack.config.js).
|
||||||
|
|
||||||
|
# Development
|
||||||
|
|
||||||
|
`npm start` launches a `webpack-dev-server` on port 8080 that compiles the assets and watches files for changes. It also serves the backend API and frontend unit tests via the `server/dev.js` entrypoint. The frontend tests can be run in the browser by navigating to http://localhost:8080/test and will rerun automatically as the watched files are saved with changes.
|
||||||
|
|
||||||
|
# Production
|
||||||
|
|
||||||
|
`npm run build` compiles the assets and writes the files to the `dist/` directory. `npm run prod` launches an Express server on port 1443 that serves the backend API and frontend static assets from `dist/` via the `server/prod.js` entrypoint.
|
||||||
|
|
||||||
|
# Notable differences
|
||||||
|
|
||||||
|
- Development compiles assets in memory, so no `dist/` directory is generated
|
||||||
|
- Development does not enable CSP headers
|
||||||
|
- Development frontend source is instrumented for code coverage
|
||||||
|
- Only development includes sourcemaps
|
||||||
|
- Only development exposes the `/test` route
|
||||||
|
- Production sets Cache-Control immutable headers on the hashed static assets
|
||||||
|
|
||||||
|
# Custom Loaders
|
||||||
|
|
||||||
|
The `build/` directory contains custom webpack loaders specific to Send. See [build/readme.md](../build/readme.md) for details on each loader.
|
||||||
46
docs/encryption.md
Normal file
46
docs/encryption.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# File Encryption
|
||||||
|
|
||||||
|
Send use 128-bit AES-GCM encryption via the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) to encrypt files in the browser before uploading them to the server. The code is in [app/keychain.js](../app/keychain.js).
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
### Uploading
|
||||||
|
|
||||||
|
1. A new secret key is generated with `crypto.getRandomValues`
|
||||||
|
2. The secret key is used to derive 3 more keys via HKDF SHA-256
|
||||||
|
- an encryption key for the file (AES-GCM)
|
||||||
|
- an encryption key for the file metadata (AES-GCM)
|
||||||
|
- a signing key for request authentication (HMAC SHA-256)
|
||||||
|
3. The file and metadata are encrypted with their corresponding keys
|
||||||
|
4. The encrypted data and signing key are uploaded to the server
|
||||||
|
5. An owner token and the share url are returned by the server and stored in local storage
|
||||||
|
6. The secret key is appended to the share url as a [#fragment](https://en.wikipedia.org/wiki/Fragment_identifier) and presented to the UI
|
||||||
|
|
||||||
|
### Downloading
|
||||||
|
|
||||||
|
1. The browser loads the share url page, which includes an authentication nonce
|
||||||
|
2. The browser imports the secret key from the url fragment
|
||||||
|
3. The same 3 keys as above are derived
|
||||||
|
4. The browser signs the nonce with it's signing key and requests the metadata
|
||||||
|
5. The encrypted metadata is decrypted and presented on the page
|
||||||
|
6. The browser makes another authenticated request to download the encrypted file
|
||||||
|
7. The browser downloads and decrypts the file
|
||||||
|
8. The file prompts the save dialog or automatically saves depending on the browser settings
|
||||||
|
|
||||||
|
### Passwords
|
||||||
|
|
||||||
|
A password may optionally be set to authenticate the download request. When a password is set the following steps occur.
|
||||||
|
|
||||||
|
#### Sender
|
||||||
|
|
||||||
|
1. The original signing key derived from the secret key is discarded
|
||||||
|
2. A new signing key is generated via PBKDF2 from the user entered password and the full share url (including secret key fragment)
|
||||||
|
3. The new key is sent to the server, authenticated by the owner token
|
||||||
|
4. The server stores the new key and marks the record as needing a password
|
||||||
|
|
||||||
|
#### Downloader
|
||||||
|
|
||||||
|
1. The browser loads the share url page, which includes an authentication nonce and indicator that the file requires a password
|
||||||
|
2. The user is prompted for the password and the signing key is derived
|
||||||
|
3. The browser requests the metadata using the key to sign the nonce
|
||||||
|
4. If the password was correct the metadata is returned, otherwise a 401
|
||||||
84
docs/experiments.md
Normal file
84
docs/experiments.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# A/B experiment testing
|
||||||
|
|
||||||
|
We're using Google Analytics Experiments for A/B testing.
|
||||||
|
|
||||||
|
## Creating an experiment
|
||||||
|
|
||||||
|
Navigate to the Behavior > Experiments section of Google Analytics and click the "Create experiment" button.
|
||||||
|
|
||||||
|
The "Objective for this experiment" is the most complicated part. See the "Promo click (Goal ID 4 / Goal Set 1)" for an example.
|
||||||
|
|
||||||
|
In step 2 add as many variants as you plan to test. The urls are not important since we aren't using their js library to choose the variants. The name will show up in the report so choose good ones. "Original page" becomes variant 0 and each variant increments by one. We'll use the numbers in our `app/experiments.js` code.
|
||||||
|
|
||||||
|
Step 3 contains some script that we'll ignore. The important thing here is the **Experiment ID**. This is the value we need to name our experiment in `app/experiments.js`. Save the changes so far and wait until the code containing the experiment has been deployed to production **before** starting the experiment.
|
||||||
|
|
||||||
|
## Experiment code
|
||||||
|
|
||||||
|
Code for experiments live in [app/experiments.js](../app/experiments.js). There's an `experiments` object that contains the logic for deciding whether an experiment should run, which variant to use, and what to do. Each object needs to have these functions:
|
||||||
|
|
||||||
|
### `eligible` function
|
||||||
|
|
||||||
|
This function returns a boolean of whether this experiment should be active for this session. Any data available to the page can be used determine the result.
|
||||||
|
|
||||||
|
### `variant` function
|
||||||
|
|
||||||
|
This function returns which experimental group this session is placed in. The variant values need to match the values set up in Google Analytics, usually 0 thru N-1. This value is usually picked at random based on what percentage of each variant is desired.
|
||||||
|
|
||||||
|
### `run` function
|
||||||
|
|
||||||
|
This function gets the `variant` value chosen by the variant function and the `state` and `emitter` objects from the app. This function can do anything needed to change the app based on the experiment. A common pattern is to set or change a value on `state` that will be picked up by other parts of the app, like ui templates, to change how it looks or behaves.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
Here's a full example of the experiment object:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const experiments = {
|
||||||
|
S9wqVl2SQ4ab2yZtqDI3Dw: { // The Experiment ID from Google Analytics
|
||||||
|
id: 'S9wqVl2SQ4ab2yZtqDI3Dw',
|
||||||
|
run: function(variant, state, emitter) {
|
||||||
|
switch (variant) {
|
||||||
|
case 1:
|
||||||
|
state.promo = 'blue';
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
state.promo = 'pink';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
state.promo = 'grey';
|
||||||
|
}
|
||||||
|
emitter.emit('render');
|
||||||
|
},
|
||||||
|
eligible: function() {
|
||||||
|
return (
|
||||||
|
!/firefox|fxios/i.test(navigator.userAgent) &&
|
||||||
|
document.querySelector('html').lang === 'en-US'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
variant: function(state) {
|
||||||
|
const n = this.luckyNumber(state);
|
||||||
|
if (n < 0.33) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return n < 0.66 ? 1 : 2;
|
||||||
|
},
|
||||||
|
luckyNumber: function(state) {
|
||||||
|
return luckyNumber(
|
||||||
|
`${this.id}:${state.storage.get('testpilot_ga__cid')}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reporting results
|
||||||
|
|
||||||
|
All metrics pings will include the variant and experiment id, but it's usually important to trigger a specific event to be counted as the experiment goal (the "Objective for this experiment" part from setup). Use an 'experiment' event to do this. For example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
emit('experiment', { cd3: 'promo' });
|
||||||
|
```
|
||||||
|
|
||||||
|
where `emit` is the app emitter function passed to the [route handler](https://github.com/choojs/choo#approuteroutename-handlerstate-emit)
|
||||||
|
|
||||||
|
The second argument can be an object with any additional parameters. It usually includes a custom dimension that we chose to filter on while creating the experiment in Google Analytics.
|
||||||
@@ -31,12 +31,11 @@ Since Send is an open source project, you can see all of the cool ways we use Ja
|
|||||||
## How long are files available for?
|
## How long are files available for?
|
||||||
|
|
||||||
Files are available to be downloaded for 24 hours, after which they are removed
|
Files are available to be downloaded for 24 hours, after which they are removed
|
||||||
from the server. They are also removed immediately after a download completes.
|
from the server. They are also removed immediately once the download limit is reached.
|
||||||
|
|
||||||
## Can a file be downloaded more than once?
|
## Can a file be downloaded more than once?
|
||||||
|
|
||||||
Not currently, but we're considering multiple download support in a future
|
Yes, once a file is submitted to Send you can select the download limit.
|
||||||
release.
|
|
||||||
|
|
||||||
|
|
||||||
*Disclaimer: Send is an experiment and under active development. The answers
|
*Disclaimer: Send is an experiment and under active development. The answers
|
||||||
|
|||||||
29
docs/localization.md
Normal file
29
docs/localization.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Localization
|
||||||
|
|
||||||
|
Send is localized in over 50 languages. We use the [fluent](http://projectfluent.org/) library and store our translations in [FTL](http://projectfluent.org/fluent/guide/) files in `public/locales/`. `en-US` is our base language, and other languages are managed by [pontoon](https://pontoon.mozilla.org/projects/test-pilot-firefox-send/).
|
||||||
|
|
||||||
|
## Process
|
||||||
|
|
||||||
|
Strings are added or removed from [public/locales/en-US/send.ftl] as needed. Strings **MUST NOT** be *changed* after they've been commited and pushed to master. Changing a string requires creating a new ID with a new name (preferably descriptive instead of incremented) and deletion of the obsolete ID. It's often useful to add a comment above the string with info about how and where the string is used.
|
||||||
|
|
||||||
|
Once new strings are commited to master they are available for translators in Pontoon. All languages other than `en-US` should be edited via Pontoon. Translations get automatically commited to the github master branch.
|
||||||
|
|
||||||
|
### Activation
|
||||||
|
|
||||||
|
The development environment includes all locales in `public/locales` via the `L10N_DEV` environment variable. Production uses `package.json` as the list of locales to use. Once a locale has enough string coverage it should be added to `package.json`.
|
||||||
|
|
||||||
|
## Code
|
||||||
|
|
||||||
|
In `app/` we use the `state.translate()` function to translate strings to the best matching language base on the user's `Accept-Language` header. It's a wrapper around fluent's [MessageContext.format](http://projectfluent.org/fluent.js/fluent/MessageContext.html). It works the same for both server and client side rendering.
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```js
|
||||||
|
// simple string
|
||||||
|
const finishedString = state.translate('downloadFinish')
|
||||||
|
// with parameters
|
||||||
|
const progressString = state.translate('downloadingPageProgress', {
|
||||||
|
filename: state.fileInfo.name,
|
||||||
|
size: bytes(state.fileInfo.size)
|
||||||
|
})
|
||||||
|
```
|
||||||
19022
package-lock.json
generated
19022
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
123
package.json
123
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "firefox-send",
|
"name": "firefox-send",
|
||||||
"description": "File Sharing Experiment",
|
"description": "File Sharing Experiment",
|
||||||
"version": "2.3.0",
|
"version": "2.6.1",
|
||||||
"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",
|
||||||
@@ -20,11 +22,14 @@
|
|||||||
"format": "prettier '**/*.js' 'assets/*.css' --single-quote --write",
|
"format": "prettier '**/*.js' 'assets/*.css' --single-quote --write",
|
||||||
"get-prod-locales": "node scripts/get-prod-locales",
|
"get-prod-locales": "node scripts/get-prod-locales",
|
||||||
"get-prod-locales:write": "npm run get-prod-locales -- --write",
|
"get-prod-locales:write": "npm run get-prod-locales -- --write",
|
||||||
"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:*",
|
||||||
"start": "cross-env NODE_ENV=development webpack-dev-server",
|
"test:backend": "nyc mocha --reporter=min test/backend",
|
||||||
|
"test:frontend": "cross-env NODE_ENV=development node test/frontend/runner.js && nyc report --reporter=html",
|
||||||
|
"test-integration": "docker-compose up --abort-on-container-exit --exit-code-from integration-tests --build --remove-orphans --quiet-pull && docker-compose down",
|
||||||
|
"test-integration-stage": "cross-env BASE_URL=https://send.stage.mozaws.net npm run test-integration",
|
||||||
|
"start": "npm run clean && cross-env NODE_ENV=development webpack-dev-server",
|
||||||
"prod": "node server/prod.js"
|
"prod": "node server/prod.js"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
@@ -39,78 +44,96 @@
|
|||||||
"git add"
|
"git add"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"nyc": {
|
||||||
|
"reporter": [
|
||||||
|
"text"
|
||||||
|
],
|
||||||
|
"cache": true
|
||||||
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.2.0"
|
"node": ">=8.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"autoprefixer": "^7.2.5",
|
"asmcrypto.js": "^0.22.0",
|
||||||
"babel-core": "^6.26.0",
|
"babel-core": "^6.26.3",
|
||||||
"babel-loader": "^7.1.2",
|
"babel-loader": "^7.1.5",
|
||||||
"babel-plugin-yo-yoify": "^1.0.2",
|
"babel-plugin-istanbul": "^4.1.6",
|
||||||
"babel-polyfill": "^6.26.0",
|
"babel-plugin-yo-yoify": "^1.0.3",
|
||||||
"babel-preset-env": "^1.6.1",
|
"babel-preset-env": "^1.7.0",
|
||||||
"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.3.1",
|
"base64-js": "^1.3.0",
|
||||||
"cross-env": "^5.1.3",
|
"copy-webpack-plugin": "^4.5.2",
|
||||||
"css-loader": "^0.28.9",
|
"cross-env": "^5.2.0",
|
||||||
|
"css-loader": "^0.28.11",
|
||||||
"css-mqpacker": "^6.0.2",
|
"css-mqpacker": "^6.0.2",
|
||||||
"cssnano": "^3.10.0",
|
"eslint": "^4.19.1",
|
||||||
"eslint": "^4.16.0",
|
"eslint-plugin-mocha": "^4.12.1",
|
||||||
"eslint-plugin-mocha": "^4.11.0",
|
"eslint-plugin-node": "^6.0.1",
|
||||||
"eslint-plugin-node": "^5.2.1",
|
|
||||||
"eslint-plugin-security": "^1.4.0",
|
"eslint-plugin-security": "^1.4.0",
|
||||||
"expose-loader": "^0.7.4",
|
"expose-loader": "^0.7.5",
|
||||||
"extract-loader": "^1.0.2",
|
"extract-loader": "^1.0.2",
|
||||||
"file-loader": "^1.1.6",
|
"extract-text-webpack-plugin": "^3.0.2",
|
||||||
|
"fast-text-encoding": "^1.0.0",
|
||||||
|
"file-loader": "^1.1.11",
|
||||||
"fluent-intl-polyfill": "^0.1.0",
|
"fluent-intl-polyfill": "^0.1.0",
|
||||||
"git-rev-sync": "^1.9.1",
|
"git-rev-sync": "^1.12.0",
|
||||||
"github-changes": "^1.1.2",
|
|
||||||
"html-loader": "^0.5.5",
|
"html-loader": "^0.5.5",
|
||||||
"husky": "^0.14.3",
|
"husky": "^0.14.3",
|
||||||
"lint-staged": "^4.3.0",
|
"lint-staged": "^7.3.0",
|
||||||
"mocha": "^3.5.3",
|
"mocha": "^5.2.0",
|
||||||
"nanobus": "^4.3.2",
|
"nanobus": "^4.3.4",
|
||||||
"npm-run-all": "^4.1.2",
|
"nanotiming": "^7.3.1",
|
||||||
"postcss-loader": "^2.0.10",
|
"npm-run-all": "^4.1.3",
|
||||||
"prettier": "^1.10.2",
|
"nsp": "^3.2.1",
|
||||||
|
"nyc": "^11.9.0",
|
||||||
|
"postcss-cssnext": "^3.1.0",
|
||||||
|
"postcss-import": "^11.1.0",
|
||||||
|
"postcss-loader": "^2.1.6",
|
||||||
|
"prettier": "^1.14.3",
|
||||||
"proxyquire": "^1.8.0",
|
"proxyquire": "^1.8.0",
|
||||||
"raven-js": "^3.22.1",
|
"puppeteer": "^1.9.0",
|
||||||
"redis-mock": "^0.20.0",
|
"raven-js": "^3.27.0",
|
||||||
"require-from-string": "^2.0.1",
|
"redis-mock": "^0.21.0",
|
||||||
|
"require-from-string": "^2.0.2",
|
||||||
"rimraf": "^2.6.2",
|
"rimraf": "^2.6.2",
|
||||||
"selenium-webdriver": "^3.6.0",
|
"sinon": "^4.5.0",
|
||||||
"sinon": "^4.2.2",
|
|
||||||
"string-hash": "^1.1.3",
|
"string-hash": "^1.1.3",
|
||||||
"stylelint-config-standard": "^17.0.0",
|
"stylelint": "^9.6.0",
|
||||||
"stylelint-no-unsupported-browser-features": "^1.0.1",
|
"stylelint-config-standard": "^18.2.0",
|
||||||
"supertest": "^3.0.0",
|
"stylelint-no-unsupported-browser-features": "^2.0.0",
|
||||||
|
"svgo": "^1.1.1",
|
||||||
|
"svgo-loader": "^2.2.0",
|
||||||
"testpilot-ga": "^0.3.0",
|
"testpilot-ga": "^0.3.0",
|
||||||
"val-loader": "^1.1.0",
|
"val-loader": "^1.1.1",
|
||||||
"webpack": "^3.10.0",
|
"webpack": "^3.12.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.188.0",
|
"@google-cloud/storage": "^2.3.0",
|
||||||
"body-parser": "^1.18.2",
|
"aws-sdk": "^2.349.0",
|
||||||
"choo": "^6.7.0",
|
"babel-polyfill": "^6.26.0",
|
||||||
|
"choo": "^6.13.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.4.0",
|
||||||
"express": "^4.16.2",
|
"express": "^4.16.3",
|
||||||
"fluent": "^0.4.1",
|
"fast-crc32c": "^1.0.4",
|
||||||
|
"fluent": "^0.6.4",
|
||||||
"fluent-langneg": "^0.1.0",
|
"fluent-langneg": "^0.1.0",
|
||||||
"helmet": "^3.10.0",
|
"helmet": "^3.13.0",
|
||||||
"mkdirp": "^0.5.1",
|
"mkdirp": "^0.5.1",
|
||||||
"mozlog": "^2.2.0",
|
"mozlog": "^2.2.0",
|
||||||
"raven": "^2.4.0",
|
"raven": "^2.6.4",
|
||||||
"redis": "^2.8.0"
|
"redis": "^2.8.0"
|
||||||
},
|
},
|
||||||
"availableLanguages": [
|
"availableLanguages": [
|
||||||
"en-US",
|
"en-US",
|
||||||
|
"ar",
|
||||||
"ast",
|
"ast",
|
||||||
"az",
|
"az",
|
||||||
"bs",
|
"bs",
|
||||||
@@ -118,9 +141,11 @@
|
|||||||
"cak",
|
"cak",
|
||||||
"cs",
|
"cs",
|
||||||
"cy",
|
"cy",
|
||||||
|
"da",
|
||||||
"de",
|
"de",
|
||||||
"dsb",
|
"dsb",
|
||||||
"el",
|
"el",
|
||||||
|
"en-CA",
|
||||||
"es-AR",
|
"es-AR",
|
||||||
"es-CL",
|
"es-CL",
|
||||||
"es-ES",
|
"es-ES",
|
||||||
@@ -131,6 +156,7 @@
|
|||||||
"fy-NL",
|
"fy-NL",
|
||||||
"hsb",
|
"hsb",
|
||||||
"hu",
|
"hu",
|
||||||
|
"ia",
|
||||||
"id",
|
"id",
|
||||||
"it",
|
"it",
|
||||||
"ja",
|
"ja",
|
||||||
@@ -143,11 +169,14 @@
|
|||||||
"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",
|
||||||
|
"te",
|
||||||
"tl",
|
"tl",
|
||||||
"tr",
|
"tr",
|
||||||
"uk",
|
"uk",
|
||||||
|
|||||||
@@ -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 = الانطباعات
|
||||||
@@ -7,8 +7,7 @@ uploadPageExplainer = أرسل الملفات عبر رابط آمن خاص وم
|
|||||||
uploadPageLearnMore = اطّلع على المزيد
|
uploadPageLearnMore = اطّلع على المزيد
|
||||||
uploadPageDropMessage = أسقِط ملفّك هنا لبدء الرفع
|
uploadPageDropMessage = أسقِط ملفّك هنا لبدء الرفع
|
||||||
uploadPageSizeMessage = لتتحصل على أفضل تجربة، من المستحسن أن يكون الملف أصغر من 1 غ.بايت
|
uploadPageSizeMessage = لتتحصل على أفضل تجربة، من المستحسن أن يكون الملف أصغر من 1 غ.بايت
|
||||||
uploadPageBrowseButton = اختر ملفّا على حاسوبك
|
uploadPageBrowseButton = اختر ملفًا على حاسوبك
|
||||||
.title = اختر ملفّا على حاسوبك
|
|
||||||
uploadPageBrowseButton1 = اختر ملفّا لرفعه
|
uploadPageBrowseButton1 = اختر ملفّا لرفعه
|
||||||
uploadPageMultipleFilesAlert = رفع عدة ملفات (أو رفع مجلد) ليس مدعوما حاليا.
|
uploadPageMultipleFilesAlert = رفع عدة ملفات (أو رفع مجلد) ليس مدعوما حاليا.
|
||||||
uploadPageBrowseButtonTitle = ارفع ملفًا
|
uploadPageBrowseButtonTitle = ارفع ملفًا
|
||||||
@@ -20,16 +19,15 @@ decryptingFile = يفك التعمية…
|
|||||||
notifyUploadDone = انتهى الرفع.
|
notifyUploadDone = انتهى الرفع.
|
||||||
uploadingPageMessage = ما إن يُرفع الملف سيُتاح ضبط خيارات انتهاء صلاحيته.
|
uploadingPageMessage = ما إن يُرفع الملف سيُتاح ضبط خيارات انتهاء صلاحيته.
|
||||||
uploadingPageCancel = ألغِ الرفع
|
uploadingPageCancel = ألغِ الرفع
|
||||||
.title = ألغِ الرفع
|
|
||||||
uploadCancelNotification = أُلغي الرفع.
|
uploadCancelNotification = أُلغي الرفع.
|
||||||
uploadingPageLargeFileMessage = هذا الملف كبير الحجم وسيأخذ رفعه وقتا. انتظر رجاءً.
|
uploadingPageLargeFileMessage = هذا الملف كبير الحجم وسيأخذ رفعه وقتا. انتظر رجاءً.
|
||||||
uploadingFileNotification = أعلِمني عندما يكتمل الرفع.
|
uploadingFileNotification = أعلِمني عندما يكتمل الرفع.
|
||||||
uploadSuccessConfirmHeader = جاهز للإرسال
|
uploadSuccessConfirmHeader = جاهز للإرسال
|
||||||
uploadSvgAlt
|
uploadSvgAlt = ارفع
|
||||||
.alt = ارفع
|
|
||||||
uploadSuccessTimingHeader = ستنتهي صلاحية الرابط الذي يشير إلى الملف في حال: نُزِّل لأول مرة، أو مرّ ٢٤ ساعة على رفعه.
|
uploadSuccessTimingHeader = ستنتهي صلاحية الرابط الذي يشير إلى الملف في حال: نُزِّل لأول مرة، أو مرّ ٢٤ ساعة على رفعه.
|
||||||
expireInfo = ستنتهي صلاحية رابط الملف بعد { $downloadCount } أو { $timespan }.
|
expireInfo = ستنتهي صلاحية رابط الملف بعد { $downloadCount } أو { $timespan }.
|
||||||
downloadCount = { $num ->
|
downloadCount =
|
||||||
|
{ $num ->
|
||||||
[zero] لا تنزيلات
|
[zero] لا تنزيلات
|
||||||
[one] تنزيل واحد
|
[one] تنزيل واحد
|
||||||
[two] تنزيلين
|
[two] تنزيلين
|
||||||
@@ -37,7 +35,8 @@ downloadCount = { $num ->
|
|||||||
[many] { $num } تنزيلًا
|
[many] { $num } تنزيلًا
|
||||||
*[other] { $num } تنزيل
|
*[other] { $num } تنزيل
|
||||||
}
|
}
|
||||||
timespanHours = { $num ->
|
timespanHours =
|
||||||
|
{ $num ->
|
||||||
[zero] أقل من ساعة
|
[zero] أقل من ساعة
|
||||||
[one] ساعة
|
[one] ساعة
|
||||||
[two] ساعتين
|
[two] ساعتين
|
||||||
@@ -47,42 +46,36 @@ timespanHours = { $num ->
|
|||||||
}
|
}
|
||||||
copyUrlFormLabelWithName = انسخ الرابط وشاركه لإرسال الملف: { $filename }
|
copyUrlFormLabelWithName = انسخ الرابط وشاركه لإرسال الملف: { $filename }
|
||||||
copyUrlFormButton = انسخ إلى الحافظة
|
copyUrlFormButton = انسخ إلى الحافظة
|
||||||
.title = انسخ إلى الحافظة
|
|
||||||
copiedUrl = نُسخ!
|
copiedUrl = نُسخ!
|
||||||
deleteFileButton = احذف الملف
|
deleteFileButton = احذف الملف
|
||||||
.title = احذف الملف
|
sendAnotherFileLink = أرسِل ملفًا آخر
|
||||||
sendAnotherFileLink = أرسل ملفّا آخر
|
# Alternative text used on the download link/button (indicates an action).
|
||||||
.title = أرسل ملفّا آخر
|
downloadAltText = نزّل
|
||||||
// Alternative text used on the download link/button (indicates an action).
|
|
||||||
downloadAltText
|
|
||||||
.alt = نزّل
|
|
||||||
downloadsFileList = التنزيلات
|
downloadsFileList = التنزيلات
|
||||||
// Used as header in a column indicating the amount of time left before a
|
# Used as header in a column indicating the amount of time left before a
|
||||||
// download link expires (e.g. "10h 5m")
|
# download link expires (e.g. "10h 5m")
|
||||||
timeFileList = الوقت
|
timeFileList = الوقت
|
||||||
// Used as header in a column indicating the number of times a file has been
|
# Used as header in a column indicating the number of times a file has been
|
||||||
// downloaded
|
# downloaded
|
||||||
downloadFileName = نزّل { $filename }
|
downloadFileName = نزّل { $filename }
|
||||||
downloadFileSize = ({ $size })
|
downloadFileSize = ({ $size })
|
||||||
unlockInputLabel = أدخل كلمة السر
|
unlockInputLabel = أدخل كلمة السر
|
||||||
unlockInputPlaceholder = كلمة السر
|
unlockInputPlaceholder = كلمة السر
|
||||||
unlockButtonLabel = افتح القفل
|
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 = نزّل
|
|
||||||
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 })
|
downloadingPageProgress = ينزّل { $filename } ({ $size })
|
||||||
downloadingPageMessage = رجاء أبقِ هذا اللسان مفتوحا حتى نجلب الملف ونفك تعميته.
|
downloadingPageMessage = رجاء أبقِ هذا اللسان مفتوحا حتى نجلب الملف ونفك تعميته.
|
||||||
errorAltText
|
errorAltText = خطأ أثناء الرفع
|
||||||
.alt = خطأ أثناء الرفع
|
|
||||||
errorPageHeader = حدث خطب ما.
|
errorPageHeader = حدث خطب ما.
|
||||||
errorPageMessage = حدث خطب ما أثناء رفع الملف.
|
errorPageMessage = حدث خطب ما أثناء رفع الملف.
|
||||||
errorPageLink = أرسل ملفا آخر
|
errorPageLink = أرسل ملفا آخر
|
||||||
@@ -90,7 +83,7 @@ fileTooBig = حجم الملف كبير للغاية لرفعه. يجب أن ي
|
|||||||
linkExpiredAlt = انتهت صلاحية الرابط
|
linkExpiredAlt = انتهت صلاحية الرابط
|
||||||
expiredPageHeader = انتهت صلاحية هذا الرابط أو لم يكن موجودا في المقام الأول!
|
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 = للأسف فإن إصدارة فَيَرفُكس هذه لا تدعم تقنية الوِب التي يعتمد عليها «فَيَرفُكس سِنْد». عليك تحديث متصفحك.
|
||||||
@@ -98,7 +91,7 @@ updateFirefox = حدّث فَيَرفُكس
|
|||||||
downloadFirefoxButtonSub = تنزيل مجاني
|
downloadFirefoxButtonSub = تنزيل مجاني
|
||||||
uploadedFile = ملف
|
uploadedFile = ملف
|
||||||
copyFileList = انسخ الرابط
|
copyFileList = انسخ الرابط
|
||||||
// expiryFileList is used as a column header
|
# expiryFileList is used as a column header
|
||||||
expiryFileList = ينتهي في
|
expiryFileList = ينتهي في
|
||||||
deleteFileList = احذف
|
deleteFileList = احذف
|
||||||
nevermindButton = لا بأس
|
nevermindButton = لا بأس
|
||||||
@@ -108,12 +101,10 @@ legalNoticeMozilla = يخضع استخدام موقع «فَيَرفُكس سِ
|
|||||||
deletePopupText = أأحذف هذا الملف؟
|
deletePopupText = أأحذف هذا الملف؟
|
||||||
deletePopupYes = نعم
|
deletePopupYes = نعم
|
||||||
deletePopupCancel = ألغِ
|
deletePopupCancel = ألغِ
|
||||||
deleteButtonHover
|
deleteButtonHover = احذف
|
||||||
.title = احذف
|
copyUrlHover = انسخ الرابط
|
||||||
copyUrlHover
|
|
||||||
.title = انسخ الرابط
|
|
||||||
footerLinkLegal = القانونية
|
footerLinkLegal = القانونية
|
||||||
// Test Pilot is a proper name and should not be localized.
|
# Test Pilot is a proper name and should not be localized.
|
||||||
footerLinkAbout = حول الاختبار التجريبي
|
footerLinkAbout = حول الاختبار التجريبي
|
||||||
footerLinkPrivacy = الخصوصية
|
footerLinkPrivacy = الخصوصية
|
||||||
footerLinkTerms = الشروط
|
footerLinkTerms = الشروط
|
||||||
@@ -122,6 +113,17 @@ requirePasswordCheckbox = اطلب كلمة سر لتنزيل هذا الملف
|
|||||||
addPasswordButton = أضِف كلمة سر
|
addPasswordButton = أضِف كلمة سر
|
||||||
changePasswordButton = غيّر
|
changePasswordButton = غيّر
|
||||||
passwordTryAgain = كلمة السر خاطئة. أعِد المحاولة.
|
passwordTryAgain = كلمة السر خاطئة. أعِد المحاولة.
|
||||||
// This label is followed by the password needed to download a file
|
|
||||||
passwordResult = كلمة السر: { $password }
|
|
||||||
reportIPInfringement = أبلغ عن انتهاك للملكية الفكرية
|
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 = ضُبطت كلمة السر
|
||||||
|
# A short status message shown when the user enters a long password
|
||||||
|
maxPasswordLength = أقصر طول لكلمة السر: { $length }
|
||||||
|
# A short status message shown when there was an error setting the password
|
||||||
|
passwordSetError = يجب ألا تُضبط كلمة السر هذه
|
||||||
|
|||||||
@@ -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 = esperimentu web
|
siteSubtitle = esperimentu web
|
||||||
siteFeedback = Feedback
|
siteFeedback = Feedback
|
||||||
@@ -25,28 +25,45 @@ uploadingFileNotification = Avísame cuando se complete la xuba.
|
|||||||
uploadSuccessConfirmHeader = Preparáu pa unviar
|
uploadSuccessConfirmHeader = Preparáu pa unviar
|
||||||
uploadSvgAlt = Xubir
|
uploadSvgAlt = Xubir
|
||||||
uploadSuccessTimingHeader = L'enllaz del to ficheru caducará dempués d'una descarga o en 24 hores.
|
uploadSuccessTimingHeader = L'enllaz del to ficheru caducará dempués d'una descarga o en 24 hores.
|
||||||
|
expireInfo = L'enllaz al ficheru va caducar tres { $downloadCount } o { $timespan }.
|
||||||
|
downloadCount =
|
||||||
|
{ $num ->
|
||||||
|
[one] 1 descarga
|
||||||
|
*[other] { $num } descargues
|
||||||
|
}
|
||||||
|
timespanHours =
|
||||||
|
{ $num ->
|
||||||
|
[one] 1 hora
|
||||||
|
*[other] { $num } hores
|
||||||
|
}
|
||||||
copyUrlFormLabelWithName = Copia y comparti l'enllaz pa unviar el to ficheru: { $filename }
|
copyUrlFormLabelWithName = Copia y comparti l'enllaz pa unviar el to ficheru: { $filename }
|
||||||
copyUrlFormButton = Copiar al cartafueyu
|
copyUrlFormButton = Copiar al cartafueyu
|
||||||
copiedUrl = ¡Copióse!
|
copiedUrl = ¡Copióse!
|
||||||
deleteFileButton = Desaniciar ficheru
|
deleteFileButton = Desaniciar ficheru
|
||||||
sendAnotherFileLink = Unviar otru ficheru
|
sendAnotherFileLink = Unviar otru ficheru
|
||||||
// Alternative text used on the download link/button (indicates an action).
|
# Alternative text used on the download link/button (indicates an action).
|
||||||
downloadAltText = Baxar
|
downloadAltText = Baxar
|
||||||
|
downloadsFileList = Descargues
|
||||||
|
# Used as header in a column indicating the amount of time left before a
|
||||||
|
# download link expires (e.g. "10h 5m")
|
||||||
|
timeFileList = Tiempu
|
||||||
|
# Used as header in a column indicating the number of times a file has been
|
||||||
|
# downloaded
|
||||||
downloadFileName = Baxar { $filename }
|
downloadFileName = Baxar { $filename }
|
||||||
downloadFileSize = ({ $size })
|
downloadFileSize = ({ $size })
|
||||||
unlockInputLabel = Introducir contraseña
|
unlockInputLabel = Introducir contraseña
|
||||||
unlockInputPlaceholder = Contraseña
|
unlockInputPlaceholder = Contraseña
|
||||||
unlockButtonLabel = Desbloquiar
|
unlockButtonLabel = Desbloquiar
|
||||||
downloadFileTitle = Baxar ficheru cifráu
|
downloadFileTitle = Baxar ficheru cifráu
|
||||||
// Firefox Send is a brand name and should not be localized.
|
# Firefox Send is a brand name and should not be localized.
|
||||||
downloadMessage = El to collaciu unvióte un ficheru usando Firefox Send, un serviciu que te permite compartir ficheros con un enllaz seguru, priváu y cifráu que caduca automáticamente p'asegurar que les to coses nun queden siempres na rede.
|
downloadMessage = El to collaciu unvióte un ficheru usando Firefox Send, un serviciu que te permite compartir ficheros con un enllaz seguru, priváu y cifráu que caduca automáticamente p'asegurar que les to coses nun queden siempres na rede.
|
||||||
// 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 = Baxar
|
downloadButtonLabel = Baxar
|
||||||
downloadNotification = Completóse la to descarga.
|
downloadNotification = Completóse la to descarga.
|
||||||
downloadFinish = Descarga completada
|
downloadFinish = Descarga completada
|
||||||
// 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 = Prueba Firefox Send
|
sendYourFilesLink = Prueba Firefox Send
|
||||||
downloadingPageProgress = Baxando { $filename } ({ $size })
|
downloadingPageProgress = Baxando { $filename } ({ $size })
|
||||||
downloadingPageMessage = Dexa esta llingüeta abierta entrín vamos en cata del to ficheru y lu desciframos, por favor.
|
downloadingPageMessage = Dexa esta llingüeta abierta entrín vamos en cata del to ficheru y lu desciframos, por favor.
|
||||||
@@ -58,7 +75,7 @@ fileTooBig = Esti ficheru ye mui grande como pa xubilu. Debería tener menos de
|
|||||||
linkExpiredAlt = Enllaz caducáu
|
linkExpiredAlt = Enllaz caducáu
|
||||||
expiredPageHeader = ¡Esti enllaz caducó o enxamás nun esistó!
|
expiredPageHeader = ¡Esti enllaz caducó o enxamás nun esistó!
|
||||||
notSupportedHeader = El to restolador nun ta sofitáu.
|
notSupportedHeader = El to restolador nun ta sofitáu.
|
||||||
// Firefox Send is a brand name and should not be localized.
|
# Firefox Send is a brand name and should not be localized.
|
||||||
notSupportedDetail = Desafortunadamente esti restolador nun sofita la teunoloxía web qu'usa Firefox Send. Precisarás d'usar otru restolador. ¡Aconseyámoste Firefox!
|
notSupportedDetail = Desafortunadamente esti restolador nun sofita la teunoloxía web qu'usa Firefox Send. Precisarás d'usar otru restolador. ¡Aconseyámoste Firefox!
|
||||||
notSupportedLink = ¿Por qué'l mio restolador nun ta sofitáu?
|
notSupportedLink = ¿Por qué'l mio restolador nun ta sofitáu?
|
||||||
notSupportedOutdatedDetail = Desafortunadamente esta versión de Firefox nun sofita la teunoloxía web qu'usa Firefox Send. Precisarás d'anovar Firefox.
|
notSupportedOutdatedDetail = Desafortunadamente esta versión de Firefox nun sofita la teunoloxía web qu'usa Firefox Send. Precisarás d'anovar Firefox.
|
||||||
@@ -66,7 +83,7 @@ updateFirefox = Anovar Firefox
|
|||||||
downloadFirefoxButtonSub = Descarga de baldre
|
downloadFirefoxButtonSub = Descarga de baldre
|
||||||
uploadedFile = Ficheru
|
uploadedFile = Ficheru
|
||||||
copyFileList = Copiar URL
|
copyFileList = Copiar URL
|
||||||
// expiryFileList is used as a column header
|
# expiryFileList is used as a column header
|
||||||
expiryFileList = Caduca en
|
expiryFileList = Caduca en
|
||||||
deleteFileList = Desaniciar
|
deleteFileList = Desaniciar
|
||||||
nevermindButton = Nun m'importa
|
nevermindButton = Nun m'importa
|
||||||
@@ -79,13 +96,25 @@ deletePopupCancel = Encaboxar
|
|||||||
deleteButtonHover = Desaniciar
|
deleteButtonHover = Desaniciar
|
||||||
copyUrlHover = Copiar URL
|
copyUrlHover = Copiar URL
|
||||||
footerLinkLegal = Llegal
|
footerLinkLegal = Llegal
|
||||||
// Test Pilot is a proper name and should not be localized.
|
# Test Pilot is a proper name and should not be localized.
|
||||||
footerLinkAbout = Tocante a Test Pilot
|
footerLinkAbout = Tocante a Test Pilot
|
||||||
footerLinkPrivacy = Privacidá
|
footerLinkPrivacy = Privacidá
|
||||||
footerLinkTerms = Términos
|
footerLinkTerms = Términos
|
||||||
footerLinkCookies = Cookies
|
footerLinkCookies = Cookies
|
||||||
requirePasswordCheckbox = Riquir una contraseña pa baxar esti ficheru
|
requirePasswordCheckbox = Riquir una contraseña pa baxar esti ficheru
|
||||||
addPasswordButton = Amestar contraseña
|
addPasswordButton = Amestar contraseña
|
||||||
|
changePasswordButton = Camudar
|
||||||
passwordTryAgain = Contraseña incorreuta. Volvi tentalo.
|
passwordTryAgain = Contraseña incorreuta. Volvi tentalo.
|
||||||
// This label is followed by the password needed to download a file
|
javascriptRequired = Firefox Send rique JavaScript
|
||||||
passwordResult = Contraseña: { $password }
|
whyJavascript = ¿Por qué Firefox Send rique JavaScript?
|
||||||
|
enableJavascript = Activa JavaScript y volvi tentalo, por favor.
|
||||||
|
# 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 = Afitóse la contraseña
|
||||||
|
# A short status message shown when the user enters a long password
|
||||||
|
maxPasswordLength = Llargor máximu de la contraseña: { $length }
|
||||||
|
# A short status message shown when there was an error setting the password
|
||||||
|
passwordSetError = Nun pudo afitase esta contraseña
|
||||||
|
|||||||
@@ -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 = Geri dönüş
|
siteFeedback = Geri dönüş
|
||||||
@@ -26,11 +26,13 @@ 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.
|
expireInfo = Faylınız üçün keçidin vaxtı { $downloadCount } sonra və ya { $timespan } tarixində keçəcək.
|
||||||
downloadCount = { $num ->
|
downloadCount =
|
||||||
|
{ $num ->
|
||||||
[one] 1 endirmə
|
[one] 1 endirmə
|
||||||
*[other] { $num } endirmə
|
*[other] { $num } endirmə
|
||||||
}
|
}
|
||||||
timespanHours = { $num ->
|
timespanHours =
|
||||||
|
{ $num ->
|
||||||
[one] 1 saat
|
[one] 1 saat
|
||||||
*[other] { $num } saat
|
*[other] { $num } saat
|
||||||
}
|
}
|
||||||
@@ -39,29 +41,29 @@ copyUrlFormButton = Buferə köçür
|
|||||||
copiedUrl = Köçürüldü!
|
copiedUrl = Köçürüldü!
|
||||||
deleteFileButton = Faylı sil
|
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
|
downloadsFileList = Endirmələr
|
||||||
// Used as header in a column indicating the amount of time left before a
|
# Used as header in a column indicating the amount of time left before a
|
||||||
// download link expires (e.g. "10h 5m")
|
# download link expires (e.g. "10h 5m")
|
||||||
timeFileList = Vaxt
|
timeFileList = Vaxt
|
||||||
// Used as header in a column indicating the number of times a file has been
|
# Used as header in a column indicating the number of times a file has been
|
||||||
// downloaded
|
# downloaded
|
||||||
downloadFileName = { $filename } faylını endir
|
downloadFileName = { $filename } faylını endir
|
||||||
downloadFileSize = ({ $size })
|
downloadFileSize = ({ $size })
|
||||||
unlockInputLabel = Parol daxil edin
|
unlockInputLabel = Parol daxil edin
|
||||||
unlockInputPlaceholder = Parol
|
unlockInputPlaceholder = Parol
|
||||||
unlockButtonLabel = Aç
|
unlockButtonLabel = Aç
|
||||||
downloadFileTitle = Şifrələnmiş Faylı Endir
|
downloadFileTitle = Şifrələnmiş Faylı Endir
|
||||||
// Firefox Send is a brand name and should not be localized.
|
# Firefox Send is a brand name and should not be localized.
|
||||||
downloadMessage = Yoldaşınız Firefox Send ilə sizə fayl göndərir, fayllarınızı təhlükəsiz, məxfi, şifrələnmiş və daima onlayn qalmaması üçün avtomatik silən fayl göndərmə xidməti.
|
downloadMessage = Yoldaşınız Firefox Send ilə sizə fayl göndərir, fayllarınızı təhlükəsiz, məxfi, şifrələnmiş və daima onlayn qalmaması üçün avtomatik silən fayl göndərmə xidməti.
|
||||||
// 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 = Endir
|
downloadButtonLabel = Endir
|
||||||
downloadNotification = Endirməniz tamamlandı.
|
downloadNotification = Endirməniz tamamlandı.
|
||||||
downloadFinish = Endirmə Tamamlandı
|
downloadFinish = Endirmə Tamamlandı
|
||||||
// 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 = Firefox Send Yoxla
|
sendYourFilesLink = Firefox Send Yoxla
|
||||||
downloadingPageProgress = { $filename } faylı ({ $size }) endirilir
|
downloadingPageProgress = { $filename } faylı ({ $size }) endirilir
|
||||||
downloadingPageMessage = Lütfən faylı endirib şifrəsini açarkən vərəqi açıq buraxın.
|
downloadingPageMessage = Lütfən faylı endirib şifrəsini açarkən vərəqi açıq buraxın.
|
||||||
@@ -73,7 +75,7 @@ fileTooBig = Fayl yükləmək üçün çox böyükdür. Fayl { $size }-dan az ol
|
|||||||
linkExpiredAlt = Keçidin vaxtı çıxıb
|
linkExpiredAlt = Keçidin vaxtı çıxıb
|
||||||
expiredPageHeader = Keçidin vaxtı çıxıb və ya heç vaxt olmayıb!
|
expiredPageHeader = Keçidin vaxtı çıxıb və ya heç vaxt olmayıb!
|
||||||
notSupportedHeader = Səyyahınız dəstəklənmir.
|
notSupportedHeader = Səyyahınız dəstəklənmir.
|
||||||
// Firefox Send is a brand name and should not be localized.
|
# Firefox Send is a brand name and should not be localized.
|
||||||
notSupportedDetail = Heyf ki, bu səyyah Firefox Send-ə güc verən web texnologiyalarını dəstəkləmir. Fərqli bir səyyah yoxlamalısınız. Biz Firefox məsləhət görürük!
|
notSupportedDetail = Heyf ki, bu səyyah Firefox Send-ə güc verən web texnologiyalarını dəstəkləmir. Fərqli bir səyyah yoxlamalısınız. Biz Firefox məsləhət görürük!
|
||||||
notSupportedLink = Səyyahım niyə dəstəklənmir?
|
notSupportedLink = Səyyahım niyə dəstəklənmir?
|
||||||
notSupportedOutdatedDetail = Heyf ki, Firefox səyyahının bu versiyası Firefox Send-ə güc verən web texnologiyalarını dəstəkləmir. Səyyahınızı yeniləməlisiniz.
|
notSupportedOutdatedDetail = Heyf ki, Firefox səyyahının bu versiyası Firefox Send-ə güc verən web texnologiyalarını dəstəkləmir. Səyyahınızı yeniləməlisiniz.
|
||||||
@@ -81,7 +83,7 @@ updateFirefox = Firefox-u Yenilə
|
|||||||
downloadFirefoxButtonSub = Pulsuz Endir
|
downloadFirefoxButtonSub = Pulsuz Endir
|
||||||
uploadedFile = Fayl
|
uploadedFile = Fayl
|
||||||
copyFileList = Keçidi Köçürt
|
copyFileList = Keçidi Köçürt
|
||||||
// expiryFileList is used as a column header
|
# expiryFileList is used as a column header
|
||||||
expiryFileList = Vaxtı çıxma tarixi
|
expiryFileList = Vaxtı çıxma tarixi
|
||||||
deleteFileList = Sil
|
deleteFileList = Sil
|
||||||
nevermindButton = Vacib deyil
|
nevermindButton = Vacib deyil
|
||||||
@@ -94,7 +96,7 @@ deletePopupCancel = Ləğv et
|
|||||||
deleteButtonHover = Sil
|
deleteButtonHover = Sil
|
||||||
copyUrlHover = Keçidi Köçürt
|
copyUrlHover = Keçidi Köçürt
|
||||||
footerLinkLegal = Hüquqi
|
footerLinkLegal = Hüquqi
|
||||||
// Test Pilot is a proper name and should not be localized.
|
# Test Pilot is a proper name and should not be localized.
|
||||||
footerLinkAbout = Test Pilot Haqqında
|
footerLinkAbout = Test Pilot Haqqında
|
||||||
footerLinkPrivacy = Məxfilik
|
footerLinkPrivacy = Məxfilik
|
||||||
footerLinkTerms = Şərtlər
|
footerLinkTerms = Şərtlər
|
||||||
@@ -103,13 +105,17 @@ requirePasswordCheckbox = Bu faylı endirmək üçün parol tələb et
|
|||||||
addPasswordButton = Parol əlavə et
|
addPasswordButton = Parol əlavə et
|
||||||
changePasswordButton = Dəyişdir
|
changePasswordButton = Dəyişdir
|
||||||
passwordTryAgain = Səhv parol. Təkrar yoxlayın.
|
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
|
reportIPInfringement = Əqli-mülkiyyət pozuntusu bildir
|
||||||
javascriptRequired = Firefox Send üçün JavaScript lazımdır
|
javascriptRequired = Firefox Send üçün JavaScript lazımdır
|
||||||
whyJavascript = Firefox Send niyə JavaScript tələb edir?
|
whyJavascript = Firefox Send niyə JavaScript tələb edir?
|
||||||
enableJavascript = Lütfən JavaScript-i aktiv edib təkrar yoxlayın.
|
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"
|
# 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
|
expiresHoursMinutes = { $hours } saat { $minutes } dəq
|
||||||
// A short representation of a countdown timer containing the number of minutes remaining as digits, example "56m"
|
# A short representation of a countdown timer containing the number of minutes remaining as digits, example "56m"
|
||||||
expiresMinutes = { $minutes } dəq
|
expiresMinutes = { $minutes } dəq
|
||||||
|
# A short status message shown when a password is successfully set
|
||||||
|
passwordIsSet = Parol quruldu
|
||||||
|
# A short status message shown when the user enters a long password
|
||||||
|
maxPasswordLength = Maksimum parol uzunluğu: { $length }
|
||||||
|
# A short status message shown when there was an error setting the password
|
||||||
|
passwordSetError = Parol qurula bilmədi
|
||||||
|
|||||||
@@ -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 = ওয়েব গবেষণা
|
siteSubtitle = ওয়েব গবেষণা
|
||||||
siteFeedback = প্রতিক্রিয়া
|
siteFeedback = প্রতিক্রিয়া
|
||||||
@@ -15,11 +15,13 @@ uploadingPageCancel = আপলোড বাতিল করুন
|
|||||||
uploadCancelNotification = আপনার অাপলোড বাতিল করা হয়েছে।
|
uploadCancelNotification = আপনার অাপলোড বাতিল করা হয়েছে।
|
||||||
uploadSuccessConfirmHeader = পাঠানোর জন্য প্রস্তুত
|
uploadSuccessConfirmHeader = পাঠানোর জন্য প্রস্তুত
|
||||||
uploadSvgAlt = আপলোড
|
uploadSvgAlt = আপলোড
|
||||||
downloadCount = { $num ->
|
downloadCount =
|
||||||
|
{ $num ->
|
||||||
[one] 1 ডাউনলোড
|
[one] 1 ডাউনলোড
|
||||||
*[other] { $num } ডাউনলোডগুলো
|
*[other] { $num } ডাউনলোডগুলো
|
||||||
}
|
}
|
||||||
timespanHours = { $num ->
|
timespanHours =
|
||||||
|
{ $num ->
|
||||||
[one] 1 ঘন্টা
|
[one] 1 ঘন্টা
|
||||||
*[other] { $num } ঘন্টা
|
*[other] { $num } ঘন্টা
|
||||||
}
|
}
|
||||||
@@ -27,20 +29,20 @@ 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 = ডাউনলোডগুলো
|
downloadsFileList = ডাউনলোডগুলো
|
||||||
// Used as header in a column indicating the amount of time left before a
|
# Used as header in a column indicating the amount of time left before a
|
||||||
// download link expires (e.g. "10h 5m")
|
# download link expires (e.g. "10h 5m")
|
||||||
timeFileList = সময়
|
timeFileList = সময়
|
||||||
// Used as header in a column indicating the number of times a file has been
|
# Used as header in a column indicating the number of times a file has been
|
||||||
// downloaded
|
# downloaded
|
||||||
downloadFileName = ডাউনলোড { $filename }
|
downloadFileName = ডাউনলোড { $filename }
|
||||||
downloadFileSize = ({ $size })
|
downloadFileSize = ({ $size })
|
||||||
unlockInputLabel = পাসওয়ার্ড লিখুন
|
unlockInputLabel = পাসওয়ার্ড লিখুন
|
||||||
unlockInputPlaceholder = পাসওয়ার্ড
|
unlockInputPlaceholder = পাসওয়ার্ড
|
||||||
unlockButtonLabel = আনলক করুন
|
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 = আপনার ডাউনলোড সম্পন্ন হয়েছে।
|
||||||
downloadFinish = ডাউনলোড সম্পন্ন
|
downloadFinish = ডাউনলোড সম্পন্ন
|
||||||
@@ -53,7 +55,7 @@ updateFirefox = Firefox হালনাগাদ করুন
|
|||||||
downloadFirefoxButtonSub = বিনামূল্যে ডাউনলোড
|
downloadFirefoxButtonSub = বিনামূল্যে ডাউনলোড
|
||||||
uploadedFile = ফাইল
|
uploadedFile = ফাইল
|
||||||
copyFileList = URL অনুলিপি করুন
|
copyFileList = URL অনুলিপি করুন
|
||||||
// expiryFileList is used as a column header
|
# expiryFileList is used as a column header
|
||||||
expiryFileList = মেয়াদোত্তীর্ণ তারিখ
|
expiryFileList = মেয়াদোত্তীর্ণ তারিখ
|
||||||
deleteFileList = মুছে ফেলুন
|
deleteFileList = মুছে ফেলুন
|
||||||
nevermindButton = কিছু মনে করবেন না
|
nevermindButton = কিছু মনে করবেন না
|
||||||
@@ -64,7 +66,7 @@ deletePopupCancel = বাতিল
|
|||||||
deleteButtonHover = মুছে ফেলুন
|
deleteButtonHover = মুছে ফেলুন
|
||||||
copyUrlHover = URL অনুলিপি করুন
|
copyUrlHover = URL অনুলিপি করুন
|
||||||
footerLinkLegal = আইনগত
|
footerLinkLegal = আইনগত
|
||||||
// Test Pilot is a proper name and should not be localized.
|
# Test Pilot is a proper name and should not be localized.
|
||||||
footerLinkAbout = Test Pilot পরিচিতি
|
footerLinkAbout = Test Pilot পরিচিতি
|
||||||
footerLinkPrivacy = গোপনীয়তা
|
footerLinkPrivacy = গোপনীয়তা
|
||||||
footerLinkTerms = শর্তাবলী
|
footerLinkTerms = শর্তাবলী
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user