Compare commits
257 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cfdef23365 | ||
|
|
7c4b6a9de4 | ||
|
|
bb8866f73f | ||
|
|
33c42648ef | ||
|
|
7feddd2eee | ||
|
|
1344b84cf5 | ||
|
|
ef03b750c5 | ||
|
|
0c07d78a37 | ||
|
|
b5885e446c | ||
|
|
0fa3d4481a | ||
|
|
49e7c2e05b | ||
|
|
2dc7d046ef | ||
|
|
3e0bd41efd | ||
|
|
929eaca2d8 | ||
|
|
60517c5ab6 | ||
|
|
97a8a2b305 | ||
|
|
8fc54bdbe2 | ||
|
|
6ff251b24a | ||
|
|
7237800a91 | ||
|
|
b4cc8e92c7 | ||
|
|
2e233da16d | ||
|
|
b703f78db9 | ||
|
|
13e792cf4d | ||
|
|
552c2d74f3 | ||
|
|
593f23b021 | ||
|
|
1baf83bac3 | ||
|
|
d43ca3190e | ||
|
|
c4c7860876 | ||
|
|
0decdeb37c | ||
|
|
47505dcc31 | ||
|
|
e71d6e792b | ||
|
|
29796cfec8 | ||
|
|
4bdf255c3a | ||
|
|
11efe8b8d1 | ||
|
|
3dd2d09584 | ||
|
|
ed4e8e8f25 | ||
|
|
181a74df88 | ||
|
|
166b2f3a52 | ||
|
|
bd5cdc52f9 | ||
|
|
5d0318c102 | ||
|
|
17adc644fb | ||
|
|
f48159dc0b | ||
|
|
360697c034 | ||
|
|
45dd833b27 | ||
|
|
98491eed01 | ||
|
|
fdafc1c59e | ||
|
|
a32638ed4c | ||
|
|
39080a6046 | ||
|
|
a124d36714 | ||
|
|
7f9e619643 | ||
|
|
db12f2f5c8 | ||
|
|
415e0b70f3 | ||
|
|
0e4b1e5ec7 | ||
|
|
054a97371c | ||
|
|
2b25b6a6ea | ||
|
|
780ed3120e | ||
|
|
bdd3cbd4c7 | ||
|
|
0c8013038f | ||
|
|
c020d59c56 | ||
|
|
a5c336494b | ||
|
|
aaa4655f45 | ||
|
|
ed18ec0bc5 | ||
|
|
1de43f31b6 | ||
|
|
1a04c86edd | ||
|
|
2bbdcae82e | ||
|
|
9b0aa5d601 | ||
|
|
cdc261e30a | ||
|
|
09133a66b0 | ||
|
|
64f1a31533 | ||
|
|
9a92a50a5f | ||
|
|
2afd93e82f | ||
|
|
cb62cc1e9d | ||
|
|
2cd3fc5af9 | ||
|
|
bac710a17f | ||
|
|
fb8b0f78ca | ||
|
|
e18ce15753 | ||
|
|
ed8ce9e3ca | ||
|
|
af5ef04115 | ||
|
|
9530a3df52 | ||
|
|
2794ac653f | ||
|
|
43eb758d73 | ||
|
|
d0364cd101 | ||
|
|
6a008bf312 | ||
|
|
dfb271410c | ||
|
|
789d67209c | ||
|
|
a31f6b75d9 | ||
|
|
f814427a7d | ||
|
|
b0307e92d4 | ||
|
|
7fa3d69aa1 | ||
|
|
58555a6b85 | ||
|
|
52113395db | ||
|
|
8dd1309c21 | ||
|
|
202e428412 | ||
|
|
6e7ed3cea3 | ||
|
|
41cb49141b | ||
|
|
82a8283b6e | ||
|
|
a5d28adc44 | ||
|
|
82e206bccf | ||
|
|
1e4d6646c6 | ||
|
|
acbf9fc32f | ||
|
|
046f227003 | ||
|
|
50ac9e32be | ||
|
|
3459dcaa15 | ||
|
|
9410defab6 | ||
|
|
b5a26e11f8 | ||
|
|
c51481628d | ||
|
|
24fa51a12c | ||
|
|
7328520d05 | ||
|
|
409d206f1e | ||
|
|
b0b393f3d9 | ||
|
|
c4499088c8 | ||
|
|
4cccd6ac5c | ||
|
|
188b28fce3 | ||
|
|
24adda6c7d | ||
|
|
4b49302fbe | ||
|
|
402ab350de | ||
|
|
47b68770af | ||
|
|
60aa16a327 | ||
|
|
5702a4806b | ||
|
|
74c4bdb660 | ||
|
|
203a2cf7fb | ||
|
|
1faa2733b3 | ||
|
|
ffa432a876 | ||
|
|
009fd29265 | ||
|
|
8f05c2324e | ||
|
|
e1ab515883 | ||
|
|
a68a7a60a7 | ||
|
|
b4dc274646 | ||
|
|
b31892bdc6 | ||
|
|
f388a1348d | ||
|
|
52dacbddf9 | ||
|
|
cc52f60aa1 | ||
|
|
1af818b691 | ||
|
|
d76f7758e7 | ||
|
|
48ab2f2400 | ||
|
|
0aa844eebc | ||
|
|
481b02ccf2 | ||
|
|
67e6ef6fda | ||
|
|
7d19f86d7a | ||
|
|
2d27d8a47c | ||
|
|
10fac130ef | ||
|
|
c6b632543d | ||
|
|
32eb8157eb | ||
|
|
3628f22114 | ||
|
|
177fa37041 | ||
|
|
717f6576ea | ||
|
|
f1b2ffa0fa | ||
|
|
ac73c23c73 | ||
|
|
ac40308b1c | ||
|
|
6fb81aa78c | ||
|
|
92430c78c2 | ||
|
|
cb7ddaa295 | ||
|
|
786d079632 | ||
|
|
4af25a505a | ||
|
|
3218803aae | ||
|
|
2311d5bcef | ||
|
|
e56d92334f | ||
|
|
bc24a069da | ||
|
|
837747f8f7 | ||
|
|
a8c32ae49c | ||
|
|
32c5b414de | ||
|
|
12c81a22e8 | ||
|
|
0c5d0d4bb2 | ||
|
|
234f9c624d | ||
|
|
da669b44ff | ||
|
|
3c39f5f085 | ||
|
|
6de91b5872 | ||
|
|
ff9a0979f6 | ||
|
|
e1e8af2489 | ||
|
|
1eb000f615 | ||
|
|
e20fd97e59 | ||
|
|
d10ceacd67 | ||
|
|
208c28ee01 | ||
|
|
cdd1bb3c29 | ||
|
|
3d9c4fa320 | ||
|
|
9c4d18ef3b | ||
|
|
ce9ff3959f | ||
|
|
51aef4e1e5 | ||
|
|
90247059d0 | ||
|
|
c97abb46ed | ||
|
|
b8f5e371c7 | ||
|
|
401311a05f | ||
|
|
652b8e4e15 | ||
|
|
99b7e7c0f1 | ||
|
|
81442bb6f2 | ||
|
|
137f474b69 | ||
|
|
8e14d3f8f7 | ||
|
|
07b7bc003a | ||
|
|
df691c1516 | ||
|
|
17e61bb09d | ||
|
|
14e21988b2 | ||
|
|
205de5a633 | ||
|
|
eabdc903b9 | ||
|
|
0628e71ec9 | ||
|
|
ebbcb38c7a | ||
|
|
5c1f535291 | ||
|
|
2c8e488611 | ||
|
|
6f3eac659c | ||
|
|
86bca790bc | ||
|
|
895d196876 | ||
|
|
3d8b38ffe4 | ||
|
|
fddc415c86 | ||
|
|
7a8e9b5de1 | ||
|
|
bbaeb44b26 | ||
|
|
a95f659474 | ||
|
|
4d311b134f | ||
|
|
5f90de577f | ||
|
|
3c32ce946a | ||
|
|
5ca89d0e0d | ||
|
|
781d1f5b0a | ||
|
|
ef9bfae319 | ||
|
|
7abbc60e17 | ||
|
|
15244e1a64 | ||
|
|
4b39d61ff4 | ||
|
|
74718d6361 | ||
|
|
ced640c24a | ||
|
|
cdaa92c86d | ||
|
|
57012f0660 | ||
|
|
6fde6e0a79 | ||
|
|
182bde30fa | ||
|
|
59e2267513 | ||
|
|
01a064ef7f | ||
|
|
9759338e6a | ||
|
|
5ac4560157 | ||
|
|
8e60ca1ac9 | ||
|
|
131a8b5564 | ||
|
|
663023a204 | ||
|
|
2b5c9dfb35 | ||
|
|
a9a34fdd0a | ||
|
|
1655094ce3 | ||
|
|
9ae7e3df11 | ||
|
|
0a31e2d521 | ||
|
|
b6849661a6 | ||
|
|
53e822964e | ||
|
|
b2f76d2df9 | ||
|
|
574a3ce894 | ||
|
|
c68f796891 | ||
|
|
c592f84d7d | ||
|
|
31faaf147e | ||
|
|
09c20f5933 | ||
|
|
fa4ab7bd5c | ||
|
|
de4a24a7f8 | ||
|
|
8f1c404724 | ||
|
|
1b975e7ba7 | ||
|
|
1d7473c489 | ||
|
|
a7d3992ba1 | ||
|
|
d81e9a76db | ||
|
|
0624e59776 | ||
|
|
546064a7ee | ||
|
|
9d49cc876c | ||
|
|
0bf8481fd0 | ||
|
|
ae0758ac14 | ||
|
|
e9405f49ee | ||
|
|
a1d0eef8a5 | ||
|
|
254b806fb4 | ||
|
|
a7aee1450f | ||
|
|
fa5573a5ff |
@@ -1,12 +1,8 @@
|
||||
node_modules
|
||||
.git
|
||||
.DS_Store
|
||||
static
|
||||
test
|
||||
scripts
|
||||
docs
|
||||
firefox
|
||||
assets
|
||||
docs
|
||||
public
|
||||
views
|
||||
webpack
|
||||
frontend
|
||||
test
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
public
|
||||
test/frontend/bundle.js
|
||||
dist
|
||||
assets
|
||||
firefox
|
||||
|
||||
6
.gitignore
vendored
@@ -1,6 +1,2 @@
|
||||
.DS_Store
|
||||
dist
|
||||
node_modules
|
||||
static/*
|
||||
!static/info.txt
|
||||
test/frontend/bundle.js
|
||||
dist
|
||||
|
||||
3
.nsprc
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"exceptions": ["https://nodesecurity.io/advisories/534"]
|
||||
}
|
||||
2
.prettierignore
Normal file
@@ -0,0 +1,2 @@
|
||||
dist
|
||||
assets/*.js
|
||||
218
CHANGELOG.md
Normal file
@@ -0,0 +1,218 @@
|
||||
## Change Log
|
||||
|
||||
### v1.1.1 (2017/08/17 01:29 +00:00)
|
||||
- [#516](https://github.com/mozilla/send/pull/516) cache assets (@dannycoates)
|
||||
- [#520](https://github.com/mozilla/send/pull/520) fix drag & drop (@dannycoates)
|
||||
- [#515](https://github.com/mozilla/send/pull/515) removed jquery from upload.js (@dannycoates)
|
||||
- [#514](https://github.com/mozilla/send/pull/514) use async and removed jquery from download.js (@dannycoates)
|
||||
- [#513](https://github.com/mozilla/send/pull/513) use svg for progress (@dannycoates)
|
||||
- [#510](https://github.com/mozilla/send/pull/510) added precommit hook for format (@dannycoates)
|
||||
- [#502](https://github.com/mozilla/send/pull/502) extracted filelist into its own file (@dannycoates)
|
||||
- [#428](https://github.com/mozilla/send/pull/428) add twitter and open graph cards (@dannycoates, @johngruen)
|
||||
- [#506](https://github.com/mozilla/send/pull/506) 404 page (@varghesethomase)
|
||||
- [#508](https://github.com/mozilla/send/pull/508) fixes 478 (@abhinadduri)
|
||||
- [#504](https://github.com/mozilla/send/pull/504) fix japanese browse button (@johngruen)
|
||||
- [#503](https://github.com/mozilla/send/pull/503) Added editorconfig (@skystar-p)
|
||||
- [#499](https://github.com/mozilla/send/pull/499) use import/export in the frontend code (@dannycoates)
|
||||
- [#500](https://github.com/mozilla/send/pull/500) fixed build:css on windows (@dannycoates)
|
||||
- [#481](https://github.com/mozilla/send/pull/481) Cater for mobile and desktop (@pdehaan, @hubdotcom)
|
||||
- [#493](https://github.com/mozilla/send/pull/493) added webpack-dev-middleware (@dannycoates)
|
||||
- [#491](https://github.com/mozilla/send/pull/491) added missing exit event cases (@dannycoates)
|
||||
- [#492](https://github.com/mozilla/send/pull/492) make the site mostly work when cookies (localStorage) are disabled (@dannycoates)
|
||||
- [#490](https://github.com/mozilla/send/pull/490) set the mime type in the download blob (@dannycoates)
|
||||
- [#485](https://github.com/mozilla/send/pull/485) added progress to tab title when not in focus (@dannycoates)
|
||||
- [#474](https://github.com/mozilla/send/pull/474) Fixing bug #438 by adding role attribute to anchor tags and alt attribute images (@varghesethomase)
|
||||
- [#480](https://github.com/mozilla/send/pull/480) Increase font weight to 500 on <button>s and <label>s (@pdehaan)
|
||||
- [#419](https://github.com/mozilla/send/pull/419) Add autoprefixer and cssnano support (@pdehaan)
|
||||
|
||||
### v1.1.0 (2017/08/08 03:59 +00:00)
|
||||
- [#473](https://github.com/mozilla/send/pull/473) Sort contributors alphabetically to prevent churn (@pdehaan)
|
||||
- [#472](https://github.com/mozilla/send/pull/472) removed references to checksums in frontend tests (@abhinadduri)
|
||||
- [#470](https://github.com/mozilla/send/pull/470) removed the file sha256 hash (@dannycoates)
|
||||
- [#469](https://github.com/mozilla/send/pull/469) Increase mimimum node version to 8.2.0 (@ehuggett)
|
||||
- [#468](https://github.com/mozilla/send/pull/468) attach delete-file handler only after upload (@dannycoates)
|
||||
- [#466](https://github.com/mozilla/send/pull/466) added webpack (@dannycoates)
|
||||
- [#427](https://github.com/mozilla/send/pull/427) Extended system font list fixes:#408 (@gautamkrishnar)
|
||||
- [#448](https://github.com/mozilla/send/pull/448) Migrate width attribute to CSS (Fixes #436) (@nskins)
|
||||
- [#457](https://github.com/mozilla/send/pull/457) factored out progress into progress.js (@dannycoates)
|
||||
- [#452](https://github.com/mozilla/send/pull/452) refactored metrics (@dannycoates)
|
||||
- [#455](https://github.com/mozilla/send/pull/455) Add a few missing strings from es-CL and tr locales (@pdehaan)
|
||||
- [#444](https://github.com/mozilla/send/pull/444) Chain jQuery calls, do not use events alias and store selectors (@Johann-S)
|
||||
- [#416](https://github.com/mozilla/send/pull/416) WIP: use webcrypto-liner to support Safari 10 (@dannycoates)
|
||||
- [#451](https://github.com/mozilla/send/pull/451) Add rel noopener noreferrer to target='_blank' anchor elements (Fixes #439) (@boopeshmahendran)
|
||||
- [#449](https://github.com/mozilla/send/pull/449) Add X-UA-Compatible meta tag (@kenrick95)
|
||||
- [#433](https://github.com/mozilla/send/pull/433) Prevent download button from being clicked multiple times (@pdehaan)
|
||||
- [#432](https://github.com/mozilla/send/pull/432) Add contributors script (@pdehaan)
|
||||
- [#409](https://github.com/mozilla/send/pull/409) Handle copy clipboard disabled (@Johann-S)
|
||||
|
||||
### v1.0.4 (2017/08/03 23:05 +00:00)
|
||||
- [#418](https://github.com/mozilla/send/pull/418) _blank all footer links (@dannycoates)
|
||||
- [#386](https://github.com/mozilla/send/pull/386) fix percentage view on mobile layout (@ariestiyansyah)
|
||||
- [#414](https://github.com/mozilla/send/pull/414) Add link to FAQ in unsupported view (@pdehaan)
|
||||
- [#415](https://github.com/mozilla/send/pull/415) Only include Fira CSS on /unsupported/* route (@pdehaan)
|
||||
- [#412](https://github.com/mozilla/send/pull/412) throw key errors before download begins (@dannycoates)
|
||||
- [#404](https://github.com/mozilla/send/pull/404) Use async function instead of promise (#325) (@weihanglo)
|
||||
- [#406](https://github.com/mozilla/send/pull/406) Add noscript tag (@pdehaan)
|
||||
- [#325](https://github.com/mozilla/send/pull/325) Use async function instead of promise (#325) (@weihanglo)
|
||||
- [#325](https://github.com/mozilla/send/pull/325) Use async function instead of promise (#325) (@weihanglo)
|
||||
|
||||
### v1.0.3 (2017/08/02 23:59 +00:00)
|
||||
- [#402](https://github.com/mozilla/send/pull/402) filter the hash from error reports (@dannycoates)
|
||||
- [#400](https://github.com/mozilla/send/pull/400) fix link that breaks download by opening in new tab (@johngruen)
|
||||
- [#369](https://github.com/mozilla/send/pull/369) Add ESLint no-alert shame rule (@pdehaan)
|
||||
- [#396](https://github.com/mozilla/send/pull/396) add babel-polyfill (@dannycoates)
|
||||
- [#394](https://github.com/mozilla/send/pull/394) catch JSON.parse errors of storage metadata (@dannycoates)
|
||||
- [#367](https://github.com/mozilla/send/pull/367) Generate production locales using 'compare-locales' (@pdehaan)
|
||||
- [#392](https://github.com/mozilla/send/pull/392) Adjust hover behavior on send-logo (#382)
|
||||
Fixes: #382. (@weihanglo)
|
||||
- [#382](https://github.com/mozilla/send/pull/382) Adjust hover behavior on send-logo (#382) (@weihanglo)
|
||||
- [#382](https://github.com/mozilla/send/pull/382) Adjust hover behavior on send-logo (#382) (@weihanglo)
|
||||
- [#380](https://github.com/mozilla/send/pull/380) Add Pontoon URL to README (@pdehaan)
|
||||
|
||||
### v1.0.2 (2017/07/31 18:58 +00:00)
|
||||
- [#365](https://github.com/mozilla/send/pull/365) revert the IE fix to fix footer on chrome (@dannycoates)
|
||||
|
||||
### v1.0.1 (2017/07/31 17:28 +00:00)
|
||||
- [#353](https://github.com/mozilla/send/pull/353) redirect ie to /unsupported (@abhinadduri, @dannycoates)
|
||||
- [#360](https://github.com/mozilla/send/pull/360) Fix some linting nits (@pdehaan)
|
||||
- [#362](https://github.com/mozilla/send/pull/362) Adjusts category of unsupported event (fixes #350). (@chuckharmston)
|
||||
- [#355](https://github.com/mozilla/send/pull/355) Make order of uploaded files in list consistent (@pdehaan)
|
||||
- [#356](https://github.com/mozilla/send/pull/356) Get rid of console.log statements (@pdehaan)
|
||||
- [#358](https://github.com/mozilla/send/pull/358) Fix some missing .title attributes in dev-only locales (@pdehaan)
|
||||
- [#354](https://github.com/mozilla/send/pull/354) Remove /en-US/ from cookies link in footer (@pdehaan)
|
||||
- [#339](https://github.com/mozilla/send/pull/339) Show error page on firefox v49 and below (@ericawright, @abhinadduri)
|
||||
- [#346](https://github.com/mozilla/send/pull/346) Add docs/CODEOWNERS file (@pdehaan)
|
||||
- [#345](https://github.com/mozilla/send/pull/345) wrap long file names (@dnarcese)
|
||||
- [#344](https://github.com/mozilla/send/pull/344) don't wrap file list headers (@dnarcese)
|
||||
- [#327](https://github.com/mozilla/send/pull/327) Modify popup delete dialog (@youwenliang)
|
||||
- [#341](https://github.com/mozilla/send/pull/341) center percentage text on all browser versions (@dnarcese)
|
||||
- [#340](https://github.com/mozilla/send/pull/340) Remove duplicate entities in localized FTL files (@flodolo)
|
||||
- [#337](https://github.com/mozilla/send/pull/337) support v 50 and 51 by not allowing const in loops (@ericawright)
|
||||
- [#338](https://github.com/mozilla/send/pull/338) Remove duplicated strings in en-US, fix nn-NO file (@flodolo)
|
||||
- [#336](https://github.com/mozilla/send/pull/336) German(de): Fixed missing value for deleteFileButton (#336) (@flodolo)
|
||||
- [#334](https://github.com/mozilla/send/pull/334) fix functionality on firefox 50 and 51 (@dnarcese)
|
||||
|
||||
### v1.0.0 (2017/07/26 19:08 +00:00)
|
||||
- [#323](https://github.com/mozilla/send/pull/323) disable upload/download notifications (@dannycoates)
|
||||
- [#322](https://github.com/mozilla/send/pull/322) fix feedback button jump (@dnarcese)
|
||||
- [#320](https://github.com/mozilla/send/pull/320) fix German footer (@dnarcese)
|
||||
|
||||
### v0.2.2 (2017/07/26 04:50 +00:00)
|
||||
- [#314](https://github.com/mozilla/send/pull/314) added L10N_DEV environment variable for making all languages available (@dannycoates)
|
||||
- [#313](https://github.com/mozilla/send/pull/313) removing timeout limit for front end tests (@abhinadduri)
|
||||
- [#311](https://github.com/mozilla/send/pull/311) expired ids should reject instead of returning null (@dannycoates)
|
||||
- [#302](https://github.com/mozilla/send/pull/302) UX Refine WIP (@youwenliang)
|
||||
- [#310](https://github.com/mozilla/send/pull/310) if the download card is pressed, the expired card shows up properly (@abhinadduri)
|
||||
- [#269](https://github.com/mozilla/send/pull/269) refactored ftl file (@abhinadduri)
|
||||
- [#291](https://github.com/mozilla/send/pull/291) added legal page (@dannycoates)
|
||||
- [#307](https://github.com/mozilla/send/pull/307) don't show error page on upload cancel (@dnarcese)
|
||||
- [#299](https://github.com/mozilla/send/pull/299) use CIRCLE_TAG as version.json version if present (@dannycoates)
|
||||
|
||||
### v0.2.1 (2017/07/24 23:34 +00:00)
|
||||
- [#296](https://github.com/mozilla/send/pull/296) restyle delete popup (@dnarcese)
|
||||
- [#295](https://github.com/mozilla/send/pull/295) renamed environment variables to remove P2P_ prefix (@dannycoates)
|
||||
- [#294](https://github.com/mozilla/send/pull/294) dealing with invalid drag and drops (@abhinadduri)
|
||||
- [#297](https://github.com/mozilla/send/pull/297) added environment variable for expire time (@dannycoates)
|
||||
- [#292](https://github.com/mozilla/send/pull/292) Fixes289 (@abhinadduri)
|
||||
- [#288](https://github.com/mozilla/send/pull/288) fix: Don`t allow upload when not on the upload page. (@ericawright)
|
||||
- [#285](https://github.com/mozilla/send/pull/285) added messages for processing phases (@dannycoates)
|
||||
- [#267](https://github.com/mozilla/send/pull/267) make site responsive and add feedback link (@johngruen)
|
||||
- [#286](https://github.com/mozilla/send/pull/286) Update download progress bar color (@pdehaan)
|
||||
- [#281](https://github.com/mozilla/send/pull/281) Stop ESLint from linting the /public/ directory (@pdehaan)
|
||||
- [#280](https://github.com/mozilla/send/pull/280) created /unsupported page and added gcmCompliant to /download page (@dannycoates)
|
||||
- [#279](https://github.com/mozilla/send/pull/279) create separate js bundles for upload/download pages (@dannycoates)
|
||||
- [#268](https://github.com/mozilla/send/pull/268) Testpilot ga (@abhinadduri)
|
||||
|
||||
### v0.2.0 (2017/07/21 19:27 +00:00)
|
||||
- [#266](https://github.com/mozilla/send/pull/266) abort uploads over maxfilesize (@dannycoates)
|
||||
- [#264](https://github.com/mozilla/send/pull/264) Remove duplicate custom metric. (@chuckharmston)
|
||||
- [#259](https://github.com/mozilla/send/pull/259) add alert when uploading multiple files (@dnarcese)
|
||||
- [#262](https://github.com/mozilla/send/pull/262) sync download progress bar with percentage (@dnarcese)
|
||||
- [#258](https://github.com/mozilla/send/pull/258) better sync percent with progress bar (@dnarcese)
|
||||
- [#257](https://github.com/mozilla/send/pull/257) add a dynamic js script for page config (@dannycoates)
|
||||
- [#256](https://github.com/mozilla/send/pull/256) add file size limit message (@dnarcese)
|
||||
- [#253](https://github.com/mozilla/send/pull/253) Add favicon.ico version of the Send logo (@pdehaan)
|
||||
- [#254](https://github.com/mozilla/send/pull/254) Add nsp check to circle ci (@pdehaan)
|
||||
- [#245](https://github.com/mozilla/send/pull/245) Localization (@abhinadduri)
|
||||
- [#252](https://github.com/mozilla/send/pull/252) only allow drag and drop on upload page (@dnarcese)
|
||||
- [#250](https://github.com/mozilla/send/pull/250) make footer not overlap (@dnarcese)
|
||||
- [#251](https://github.com/mozilla/send/pull/251) minify all images (@ericawright)
|
||||
- [#249](https://github.com/mozilla/send/pull/249) change how the file upload box expands (@dnarcese)
|
||||
- [#246](https://github.com/mozilla/send/pull/246) remove P2P references. Fixes #224 (@clouserw)
|
||||
- [#242](https://github.com/mozilla/send/pull/242) Make only icons clickable in file list (@dnarcese)
|
||||
- [#236](https://github.com/mozilla/send/pull/236) add FAQ. Fixes #186 (@clouserw)
|
||||
- [#235](https://github.com/mozilla/send/pull/235) allow send another file link to open in new tab (@dnarcese)
|
||||
- [#234](https://github.com/mozilla/send/pull/234) fix download svg (@dnarcese)
|
||||
- [#232](https://github.com/mozilla/send/pull/232) escape filename in the ui (@dannycoates)
|
||||
- [#226](https://github.com/mozilla/send/pull/226) added functionality to cancel uploads (@abhinadduri)
|
||||
- [#231](https://github.com/mozilla/send/pull/231) move head and html tags to main template (@dnarcese)
|
||||
- [#228](https://github.com/mozilla/send/pull/228) add send logo (@dnarcese)
|
||||
- [#229](https://github.com/mozilla/send/pull/229) change learn more and github links (@dnarcese)
|
||||
- [#201](https://github.com/mozilla/send/pull/201) Adds metrics documentation (closes #5). (@chuckharmston)
|
||||
- [#223](https://github.com/mozilla/send/pull/223) change size of send another file links (@dnarcese)
|
||||
- [#222](https://github.com/mozilla/send/pull/222) add footer (@dnarcese)
|
||||
- [#197](https://github.com/mozilla/send/pull/197) fixes issues 195 and 192 (@abhinadduri)
|
||||
- [#204](https://github.com/mozilla/send/pull/204) added HSTS header (@dannycoates)
|
||||
- [#193](https://github.com/mozilla/send/pull/193) Frontend tests (@abhinadduri)
|
||||
- [#191](https://github.com/mozilla/send/pull/191) New ui! (@dnarcese)
|
||||
|
||||
### v0.1.4 (2017/07/12 18:21 +00:00)
|
||||
- [#189](https://github.com/mozilla/send/pull/189) Add CSP directives (@dannycoates)
|
||||
- [#188](https://github.com/mozilla/send/pull/188) fixes delete button error (@abhinadduri)
|
||||
- [#185](https://github.com/mozilla/send/pull/185) added loading, hashing, and encrypting events for uploader; decryptin… (@abhinadduri)
|
||||
- [#183](https://github.com/mozilla/send/pull/183) rename to 'Send' (@dannycoates)
|
||||
- [#184](https://github.com/mozilla/send/pull/184) Server tests (@abhinadduri)
|
||||
- [#178](https://github.com/mozilla/send/pull/178) fixed issues in branch title (@abhinadduri)
|
||||
- [#177](https://github.com/mozilla/send/pull/177) Gcm compliance (@abhinadduri)
|
||||
- [#106](https://github.com/mozilla/send/pull/106) Gcm (@abhinadduri, @dannycoates)
|
||||
- [#168](https://github.com/mozilla/send/pull/168) Show error page if upload fails (@dnarcese)
|
||||
- [#148](https://github.com/mozilla/send/pull/148) WIP: Add basic contribute.json (@pdehaan)
|
||||
- [#162](https://github.com/mozilla/send/pull/162) Fix dev server URL in README.md file (@pdehaan)
|
||||
- [#167](https://github.com/mozilla/send/pull/167) build docker image with new name (@relud)
|
||||
- [#164](https://github.com/mozilla/send/pull/164) Add word wraps to table (@dnarcese)
|
||||
- [#149](https://github.com/mozilla/send/pull/149) Add robots.txt (@pdehaan)
|
||||
- [#161](https://github.com/mozilla/send/pull/161) Hide table header on empty list (@dnarcese)
|
||||
- [#154](https://github.com/mozilla/send/pull/154) Remove expired uploads (@dnarcese)
|
||||
- [#146](https://github.com/mozilla/send/pull/146) Update README with some more details (@pdehaan)
|
||||
|
||||
### v0.1.2 (2017/06/24 03:38 +00:00)
|
||||
- [#138](https://github.com/mozilla/send/pull/138) remove notLocalHost (@dannycoates)
|
||||
|
||||
### v0.1.0 (2017/06/24 01:24 +00:00)
|
||||
- [#137](https://github.com/mozilla/send/pull/137) refactored docker build (@dannycoates)
|
||||
- [#132](https://github.com/mozilla/send/pull/132) Add /__version__ route (@pdehaan)
|
||||
- [#135](https://github.com/mozilla/send/pull/135) make dockerfile more dockerflowy (@dannycoates)
|
||||
- [#134](https://github.com/mozilla/send/pull/134) Load previous uploads (@dannycoates, @dnarcese)
|
||||
- [#131](https://github.com/mozilla/send/pull/131) added __heartbeat__ (@dannycoates)
|
||||
- [#133](https://github.com/mozilla/send/pull/133) Add LICENSE file (@pdehaan)
|
||||
- [#130](https://github.com/mozilla/send/pull/130) added sentry to server code (@abhinadduri)
|
||||
- [#124](https://github.com/mozilla/send/pull/124) Remove unused [dev]dependencies (@pdehaan)
|
||||
- [#119](https://github.com/mozilla/send/pull/119) Move cross-env to a dep (@pdehaan)
|
||||
- [#123](https://github.com/mozilla/send/pull/123) removed bitly integration (@abhinadduri)
|
||||
- [#122](https://github.com/mozilla/send/pull/122) fix docker build (@dannycoates)
|
||||
- [#121](https://github.com/mozilla/send/pull/121) added docker service to circle.yml (@dannycoates)
|
||||
- [#120](https://github.com/mozilla/send/pull/120) added sentry (@abhinadduri)
|
||||
- [#118](https://github.com/mozilla/send/pull/118) change docker image name and add builds for tags (@relud)
|
||||
- [#116](https://github.com/mozilla/send/pull/116) add /__lbheartbeat__ endpoint (@relud)
|
||||
- [#79](https://github.com/mozilla/send/pull/79) Optimize/minimize bundle.js for production (@pdehaan)
|
||||
- [#104](https://github.com/mozilla/send/pull/104) Fix a bunch of ESLint and HTMLLint errors (@pdehaan)
|
||||
- [#105](https://github.com/mozilla/send/pull/105) Progress bars (@dnarcese)
|
||||
- [#111](https://github.com/mozilla/send/pull/111) added in anonmyized ip google analytics (@abhinadduri)
|
||||
- [#110](https://github.com/mozilla/send/pull/110) added notifications (@abhinadduri)
|
||||
- [#103](https://github.com/mozilla/send/pull/103) added Dockerfile (@dannycoates)
|
||||
- [#100](https://github.com/mozilla/send/pull/100) Added Helmet Middleware (@abhinadduri)
|
||||
- [#99](https://github.com/mozilla/send/pull/99) Testing (@abhinadduri)
|
||||
- [#77](https://github.com/mozilla/send/pull/77) Fix the linter errors (@pdehaan)
|
||||
- [#54](https://github.com/mozilla/send/pull/54) Adding basic ESLint config (@pdehaan)
|
||||
- [#71](https://github.com/mozilla/send/pull/71) Drag & drop (@dnarcese)
|
||||
- [#72](https://github.com/mozilla/send/pull/72) Logging (@abhinadduri, @dannycoates)
|
||||
- [#45](https://github.com/mozilla/send/pull/45) S3 integration (@abhinadduri, @dannycoates)
|
||||
- [#46](https://github.com/mozilla/send/pull/46) Download page and share link UI (@dnarcese)
|
||||
- [#41](https://github.com/mozilla/send/pull/41) Added upload page and file list UI (@dnarcese)
|
||||
- [#40](https://github.com/mozilla/send/pull/40) Tweak the package.json file (@pdehaan)
|
||||
- [#43](https://github.com/mozilla/send/pull/43) added return (@abhinadduri)
|
||||
- [#42](https://github.com/mozilla/send/pull/42) changed to handle 404 during download, also removing progress listene… (@abhinadduri)
|
||||
- [#39](https://github.com/mozilla/send/pull/39) Refactor riff (@abhinadduri, @dannycoates)
|
||||
- [#36](https://github.com/mozilla/send/pull/36) added prettier for js formatting (@dannycoates)
|
||||
- [#28](https://github.com/mozilla/send/pull/28) Added a UI for the uploader end, made stylistic changes, implemented deleting (@abhinadduri)
|
||||
- [#25](https://github.com/mozilla/send/pull/25) Changed naming for some pages, no longer stores files by name on server (@abhinadduri)
|
||||
14
CONTRIBUTORS
@@ -2,15 +2,19 @@ Abhinav Adduri
|
||||
Alexander Slovesnik
|
||||
Amin Mahmudian
|
||||
Andreas Pettersson
|
||||
Arash Mousavi
|
||||
Balázs Meskó
|
||||
Belayet Hossain
|
||||
Bjørn I
|
||||
Boopesh Mahendran
|
||||
Chuck Harmston
|
||||
Cláudio Esperança
|
||||
Cynthia Pereira
|
||||
Daniel Thorn
|
||||
Daniela Arcese
|
||||
Danny Coates
|
||||
Emin Mastizada
|
||||
Enol
|
||||
Erica
|
||||
Erica Wright
|
||||
Fjoerfoks
|
||||
@@ -18,9 +22,13 @@ Francesco Lodolo
|
||||
Francesco Lodolo [:flod]
|
||||
Gautam krishna.R
|
||||
Håvar Henriksen
|
||||
Jae Hyeon Park
|
||||
Jakub Rychlý
|
||||
Jamie
|
||||
Jim Spentzos
|
||||
Johann-S
|
||||
John Gruen
|
||||
Jon Vadillo
|
||||
Jordi Serratosa
|
||||
Juraj Cigáň
|
||||
Kohei Yoshino
|
||||
@@ -48,12 +56,14 @@ Rok Žerdin
|
||||
Sahithi
|
||||
Sairam Raavi
|
||||
Sandro
|
||||
Schieck :)
|
||||
Selim Şumlu
|
||||
Slimane Amiri
|
||||
Théo Chevalier
|
||||
Tomáš Zelina
|
||||
Ton
|
||||
Tymur Faradzhev
|
||||
Varghese Thomas
|
||||
Victor Bychek
|
||||
Weihang Lo
|
||||
Wil Clouser
|
||||
@@ -69,10 +79,14 @@ erdem cobanoglu
|
||||
gautamkrishnar
|
||||
goofy
|
||||
hi
|
||||
jesferman1993
|
||||
josotrix
|
||||
kenrick95
|
||||
manxmensch
|
||||
ravmn
|
||||
reza.habibi2008
|
||||
siparon
|
||||
skystar-p
|
||||
xcffl
|
||||
Μιχάλης
|
||||
Марко Костић (Marko Kostić)
|
||||
|
||||
@@ -12,4 +12,4 @@ RUN npm install --production && npm cache clean --force
|
||||
ENV PORT=1443
|
||||
EXPOSE $PORT
|
||||
|
||||
CMD ["npm", "start"]
|
||||
CMD ["npm", "run", "prod"]
|
||||
|
||||
59
README.md
@@ -5,39 +5,80 @@
|
||||
|
||||
**Docs:** [Docker](docs/docker.md), [Metrics](docs/metrics.md)
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
* [What it does](#what-it-does)
|
||||
* [Requirements](#requirements)
|
||||
* [Development](#development)
|
||||
* [Commands](#commands)
|
||||
* [Configuration](#configuration)
|
||||
* [Localization](#localization)
|
||||
* [Contributing](#contributing)
|
||||
* [Testing](#testing)
|
||||
* [License](#license)
|
||||
|
||||
---
|
||||
|
||||
## What it does
|
||||
|
||||
A file sharing experiment which allows you to send encrypted files to other users.
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
- [Node.js 8+](https://nodejs.org/)
|
||||
- [Redis server](https://redis.io/)
|
||||
- [Node.js 8.2+](https://nodejs.org/)
|
||||
- [Redis server](https://redis.io/) (optional for development)
|
||||
- [AWS S3](https://aws.amazon.com/s3/) or compatible service. (optional)
|
||||
|
||||
**NOTE:** To run the project, make sure you have a Redis server running locally:
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
To start an ephemeral development server run:
|
||||
|
||||
```sh
|
||||
$ redis-server /usr/local/etc/redis.conf
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
## How to use it
|
||||
Then browse to http://localhost:8080
|
||||
|
||||
---
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
|------------------|-------------|
|
||||
| `npm run dev` | Builds and starts the web server locally for development.
|
||||
| `npm run format` | Formats the frontend and server code using **prettier**.
|
||||
| `npm run lint` | Lints the CSS and JavaScript code.
|
||||
| `npm start` | Starts the Express web server.
|
||||
| `npm test` | Runs the suite of mocha tests.
|
||||
| `npm start` | Runs the server in development configuration.
|
||||
| `npm run build` | Builds the production assets.
|
||||
| `npm run prod` | Runs the server in production configuration.
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
The server is configured with environment variables. See [server/config.js](server/config.js) for all options and [docs/docker.md](docs/docker.md) for examples.
|
||||
|
||||
---
|
||||
|
||||
## Localization
|
||||
|
||||
Firefox Send localization is managed via [Pontoon](https://pontoon.mozilla.org/projects/test-pilot-firefox-send/), not direct pull requests to the repository. If you want to fix a typo, add a new language, or simply know more about localization, please get in touch with the [existing localization team](https://pontoon.mozilla.org/teams/) for your language, or Mozilla’s [l10n-drivers](https://wiki.mozilla.org/L10n:Mozilla_Team#Mozilla_Corporation) for guidance.
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
Pull requests are always welcome! Feel free to check out the list of ["good first bugs"](https://github.com/mozilla/send/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+bug%22).
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
| ENVIRONMENT | URL
|
||||
@@ -46,6 +87,10 @@ Pull requests are always welcome! Feel free to check out the list of ["good firs
|
||||
| Stage | <https://send.stage.mozaws.net/>
|
||||
| Development | <https://send.dev.mozaws.net/>
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
[Mozilla Public License Version 2.0](LICENSE)
|
||||
|
||||
---
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
env:
|
||||
browser: true
|
||||
node: false
|
||||
node: true
|
||||
|
||||
parserOptions:
|
||||
sourceType: module
|
||||
24
app/dragManager.js
Normal file
@@ -0,0 +1,24 @@
|
||||
export default function(state, emitter) {
|
||||
emitter.on('DOMContentLoaded', () => {
|
||||
document.body.addEventListener('dragover', event => {
|
||||
if (state.route === '/') {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
document.body.addEventListener('drop', event => {
|
||||
if (state.route === '/' && !state.transfer) {
|
||||
event.preventDefault();
|
||||
document.querySelector('.upload-window').classList.remove('ondrag');
|
||||
const target = event.dataTransfer;
|
||||
if (target.files.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (target.files.length > 1 || target.files[0].size === 0) {
|
||||
return alert(state.translate('uploadPageMultipleFilesAlert'));
|
||||
}
|
||||
const file = target.files[0];
|
||||
emitter.emit('upload', { file, type: 'drop' });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
47
app/experiments.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import hash from 'string-hash';
|
||||
|
||||
const experiments = {};
|
||||
|
||||
//Returns a number between 0 and 1
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function luckyNumber(str) {
|
||||
return hash(str) / 0xffffffff;
|
||||
}
|
||||
|
||||
function checkExperiments(state, emitter) {
|
||||
const all = Object.keys(experiments);
|
||||
const id = all.find(id => experiments[id].eligible(state));
|
||||
if (id) {
|
||||
const variant = experiments[id].variant(state);
|
||||
state.storage.enroll(id, variant);
|
||||
experiments[id].run(variant, state, emitter);
|
||||
}
|
||||
}
|
||||
|
||||
export default function initialize(state, emitter) {
|
||||
emitter.on('DOMContentLoaded', () => {
|
||||
const xp = experiments[state.query.x];
|
||||
if (xp) {
|
||||
xp.run(+state.query.v, state, emitter);
|
||||
}
|
||||
});
|
||||
|
||||
if (!state.storage.get('testpilot_ga__cid')) {
|
||||
// first ever visit. check again after cid is assigned.
|
||||
emitter.on('DOMContentLoaded', () => {
|
||||
checkExperiments(state, emitter);
|
||||
});
|
||||
} else {
|
||||
const enrolled = state.storage.enrolled;
|
||||
enrolled.forEach(([id, variant]) => {
|
||||
const xp = experiments[id];
|
||||
if (xp) {
|
||||
xp.run(variant, state, emitter);
|
||||
}
|
||||
});
|
||||
// single experiment per session for now
|
||||
if (enrolled.length === 0) {
|
||||
checkExperiments(state, emitter);
|
||||
}
|
||||
}
|
||||
}
|
||||
249
app/fileManager.js
Normal file
@@ -0,0 +1,249 @@
|
||||
/* global EXPIRE_SECONDS */
|
||||
import FileSender from './fileSender';
|
||||
import FileReceiver from './fileReceiver';
|
||||
import { copyToClipboard, delay, fadeOut, percent } from './utils';
|
||||
import * as metrics from './metrics';
|
||||
|
||||
function saveFile(file) {
|
||||
const dataView = new DataView(file.plaintext);
|
||||
const blob = new Blob([dataView], { type: file.type });
|
||||
const downloadUrl = URL.createObjectURL(blob);
|
||||
|
||||
if (window.navigator.msSaveBlob) {
|
||||
return window.navigator.msSaveBlob(blob, file.name);
|
||||
}
|
||||
const a = document.createElement('a');
|
||||
a.href = downloadUrl;
|
||||
a.download = file.name;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
URL.revokeObjectURL(downloadUrl);
|
||||
}
|
||||
|
||||
function openLinksInNewTab(links, should = true) {
|
||||
links = links || Array.from(document.querySelectorAll('a:not([target])'));
|
||||
if (should) {
|
||||
links.forEach(l => {
|
||||
l.setAttribute('target', '_blank');
|
||||
l.setAttribute('rel', 'noopener noreferrer');
|
||||
});
|
||||
} else {
|
||||
links.forEach(l => {
|
||||
l.removeAttribute('target');
|
||||
l.removeAttribute('rel');
|
||||
});
|
||||
}
|
||||
return links;
|
||||
}
|
||||
|
||||
function exists(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
|
||||
resolve(xhr.status === 200);
|
||||
}
|
||||
};
|
||||
xhr.onerror = () => resolve(false);
|
||||
xhr.ontimeout = () => resolve(false);
|
||||
xhr.open('get', '/api/exists/' + id);
|
||||
xhr.timeout = 2000;
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
|
||||
export default function(state, emitter) {
|
||||
let lastRender = 0;
|
||||
let updateTitle = false;
|
||||
|
||||
function render() {
|
||||
emitter.emit('render');
|
||||
}
|
||||
|
||||
async function checkFiles() {
|
||||
const files = state.storage.files;
|
||||
let rerender = false;
|
||||
for (const file of files) {
|
||||
const ok = await exists(file.id);
|
||||
if (!ok) {
|
||||
state.storage.remove(file.id);
|
||||
rerender = true;
|
||||
}
|
||||
}
|
||||
if (rerender) {
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
function updateProgress() {
|
||||
if (updateTitle) {
|
||||
emitter.emit('DOMTitleChange', percent(state.transfer.progressRatio));
|
||||
}
|
||||
render();
|
||||
}
|
||||
|
||||
emitter.on('DOMContentLoaded', () => {
|
||||
document.addEventListener('blur', () => (updateTitle = true));
|
||||
document.addEventListener('focus', () => {
|
||||
updateTitle = false;
|
||||
emitter.emit('DOMTitleChange', 'Firefox Send');
|
||||
});
|
||||
checkFiles();
|
||||
});
|
||||
|
||||
emitter.on('navigate', checkFiles);
|
||||
|
||||
emitter.on('render', () => {
|
||||
lastRender = Date.now();
|
||||
});
|
||||
|
||||
emitter.on('delete', async ({ file, location }) => {
|
||||
try {
|
||||
metrics.deletedUpload({
|
||||
size: file.size,
|
||||
time: file.time,
|
||||
speed: file.speed,
|
||||
type: file.type,
|
||||
ttl: file.expiresAt - Date.now(),
|
||||
location
|
||||
});
|
||||
state.storage.remove(file.id);
|
||||
await FileSender.delete(file.id, file.deleteToken);
|
||||
} catch (e) {
|
||||
state.raven.captureException(e);
|
||||
}
|
||||
state.fileInfo = null;
|
||||
});
|
||||
|
||||
emitter.on('cancel', () => {
|
||||
state.transfer.cancel();
|
||||
});
|
||||
|
||||
emitter.on('upload', async ({ file, type }) => {
|
||||
const size = file.size;
|
||||
const sender = new FileSender(file);
|
||||
sender.on('progress', updateProgress);
|
||||
sender.on('encrypting', render);
|
||||
state.transfer = sender;
|
||||
render();
|
||||
const links = openLinksInNewTab();
|
||||
await delay(200);
|
||||
try {
|
||||
const start = Date.now();
|
||||
metrics.startedUpload({ size, type });
|
||||
const info = await sender.upload();
|
||||
const time = Date.now() - start;
|
||||
const speed = size / (time / 1000);
|
||||
metrics.completedUpload({ size, time, speed, type });
|
||||
document.getElementById('cancel-upload').hidden = 'hidden';
|
||||
await delay(1000);
|
||||
await fadeOut('upload-progress');
|
||||
info.name = file.name;
|
||||
info.size = size;
|
||||
info.type = type;
|
||||
info.time = time;
|
||||
info.speed = speed;
|
||||
info.createdAt = Date.now();
|
||||
info.url = `${info.url}#${info.secretKey}`;
|
||||
info.expiresAt = Date.now() + EXPIRE_SECONDS * 1000;
|
||||
state.fileInfo = info;
|
||||
state.storage.addFile(state.fileInfo);
|
||||
openLinksInNewTab(links, false);
|
||||
state.transfer = null;
|
||||
state.storage.totalUploads += 1;
|
||||
emitter.emit('pushState', `/share/${info.id}`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
state.transfer = null;
|
||||
if (err.message === '0') {
|
||||
//cancelled. do nothing
|
||||
metrics.cancelledUpload({ size, type });
|
||||
return render();
|
||||
}
|
||||
state.raven.captureException(err);
|
||||
metrics.stoppedUpload({ size, type, err });
|
||||
emitter.emit('pushState', '/error');
|
||||
}
|
||||
});
|
||||
|
||||
emitter.on('password', async ({ password, file }) => {
|
||||
try {
|
||||
await FileSender.setPassword(password, file);
|
||||
metrics.addedPassword({ size: file.size });
|
||||
file.password = password;
|
||||
state.storage.writeFiles();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
render();
|
||||
});
|
||||
|
||||
emitter.on('preview', async () => {
|
||||
const file = state.fileInfo;
|
||||
const url = `/api/download/${file.id}`;
|
||||
const receiver = new FileReceiver(url, file);
|
||||
receiver.on('progress', updateProgress);
|
||||
receiver.on('decrypting', render);
|
||||
state.transfer = receiver;
|
||||
try {
|
||||
await receiver.getMetadata(file.nonce);
|
||||
} catch (e) {
|
||||
if (e.message === '401') {
|
||||
file.password = null;
|
||||
if (!file.pwd) {
|
||||
return emitter.emit('pushState', '/404');
|
||||
}
|
||||
}
|
||||
}
|
||||
render();
|
||||
});
|
||||
|
||||
emitter.on('download', async file => {
|
||||
state.transfer.on('progress', render);
|
||||
state.transfer.on('decrypting', render);
|
||||
const links = openLinksInNewTab();
|
||||
const size = file.size;
|
||||
try {
|
||||
const start = Date.now();
|
||||
metrics.startedDownload({ size: file.size, ttl: file.ttl });
|
||||
const f = await state.transfer.download(file.nonce);
|
||||
const time = Date.now() - start;
|
||||
const speed = size / (time / 1000);
|
||||
await delay(1000);
|
||||
await fadeOut('download-progress');
|
||||
saveFile(f);
|
||||
state.storage.totalDownloads += 1;
|
||||
state.transfer = null;
|
||||
metrics.completedDownload({ size, time, speed });
|
||||
emitter.emit('pushState', '/completed');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
// TODO cancelled download
|
||||
const location = err.message === 'notfound' ? '/404' : '/error';
|
||||
if (location === '/error') {
|
||||
state.raven.captureException(err);
|
||||
metrics.stoppedDownload({ size, err });
|
||||
}
|
||||
emitter.emit('pushState', location);
|
||||
} finally {
|
||||
state.transfer = null;
|
||||
openLinksInNewTab(links, false);
|
||||
}
|
||||
});
|
||||
|
||||
emitter.on('copy', ({ url, location }) => {
|
||||
copyToClipboard(url);
|
||||
metrics.copiedLink({ location });
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
// poll for rerendering the file list countdown timers
|
||||
if (
|
||||
state.route === '/' &&
|
||||
state.storage.files.length > 0 &&
|
||||
Date.now() - lastRender > 30000
|
||||
) {
|
||||
render();
|
||||
}
|
||||
}, 60000);
|
||||
}
|
||||
252
app/fileReceiver.js
Normal file
@@ -0,0 +1,252 @@
|
||||
import Nanobus from 'nanobus';
|
||||
import { arrayToB64, b64ToArray, bytes } from './utils';
|
||||
|
||||
export default class FileReceiver extends Nanobus {
|
||||
constructor(url, file) {
|
||||
super('FileReceiver');
|
||||
this.secretKeyPromise = window.crypto.subtle.importKey(
|
||||
'raw',
|
||||
b64ToArray(file.key),
|
||||
'HKDF',
|
||||
false,
|
||||
['deriveKey']
|
||||
);
|
||||
this.encryptKeyPromise = this.secretKeyPromise.then(sk => {
|
||||
const encoder = new TextEncoder();
|
||||
return window.crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'HKDF',
|
||||
salt: new Uint8Array(),
|
||||
info: encoder.encode('encryption'),
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
sk,
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
length: 128
|
||||
},
|
||||
false,
|
||||
['decrypt']
|
||||
);
|
||||
});
|
||||
if (file.pwd) {
|
||||
const encoder = new TextEncoder();
|
||||
console.log(file.password + file.url);
|
||||
this.authKeyPromise = window.crypto.subtle
|
||||
.importKey(
|
||||
'raw',
|
||||
encoder.encode(file.password),
|
||||
{ name: 'PBKDF2' },
|
||||
false,
|
||||
['deriveKey']
|
||||
)
|
||||
.then(pwdKey =>
|
||||
window.crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'PBKDF2',
|
||||
salt: encoder.encode(file.url),
|
||||
iterations: 100,
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
pwdKey,
|
||||
{
|
||||
name: 'HMAC',
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
true,
|
||||
['sign']
|
||||
)
|
||||
);
|
||||
} else {
|
||||
this.authKeyPromise = this.secretKeyPromise.then(sk => {
|
||||
const encoder = new TextEncoder();
|
||||
return window.crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'HKDF',
|
||||
salt: new Uint8Array(),
|
||||
info: encoder.encode('authentication'),
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
sk,
|
||||
{
|
||||
name: 'HMAC',
|
||||
hash: { name: 'SHA-256' }
|
||||
},
|
||||
false,
|
||||
['sign']
|
||||
);
|
||||
});
|
||||
}
|
||||
this.metaKeyPromise = this.secretKeyPromise.then(sk => {
|
||||
const encoder = new TextEncoder();
|
||||
return window.crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'HKDF',
|
||||
salt: new Uint8Array(),
|
||||
info: encoder.encode('metadata'),
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
sk,
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
length: 128
|
||||
},
|
||||
false,
|
||||
['decrypt']
|
||||
);
|
||||
});
|
||||
this.file = file;
|
||||
this.url = url;
|
||||
this.msg = 'fileSizeProgress';
|
||||
this.state = 'initialized';
|
||||
this.progress = [0, 1];
|
||||
}
|
||||
|
||||
get progressRatio() {
|
||||
return this.progress[0] / this.progress[1];
|
||||
}
|
||||
|
||||
get sizes() {
|
||||
return {
|
||||
partialSize: bytes(this.progress[0]),
|
||||
totalSize: bytes(this.progress[1])
|
||||
};
|
||||
}
|
||||
|
||||
cancel() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
fetchMetadata(sig) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
const nonce = xhr.getResponseHeader('WWW-Authenticate').split(' ')[1];
|
||||
this.file.nonce = nonce;
|
||||
if (xhr.status === 200) {
|
||||
return resolve(xhr.response);
|
||||
}
|
||||
reject(new Error(xhr.status));
|
||||
}
|
||||
};
|
||||
xhr.onerror = () => reject(new Error(0));
|
||||
xhr.ontimeout = () => reject(new Error(0));
|
||||
xhr.open('get', `/api/metadata/${this.file.id}`);
|
||||
xhr.setRequestHeader('Authorization', `send-v1 ${arrayToB64(sig)}`);
|
||||
xhr.responseType = 'json';
|
||||
xhr.timeout = 2000;
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
|
||||
async getMetadata(nonce) {
|
||||
try {
|
||||
const authKey = await this.authKeyPromise;
|
||||
const sig = await window.crypto.subtle.sign(
|
||||
{
|
||||
name: 'HMAC'
|
||||
},
|
||||
authKey,
|
||||
b64ToArray(nonce)
|
||||
);
|
||||
const data = await this.fetchMetadata(new Uint8Array(sig));
|
||||
const metaKey = await this.metaKeyPromise;
|
||||
const json = await window.crypto.subtle.decrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: new Uint8Array(12),
|
||||
tagLength: 128
|
||||
},
|
||||
metaKey,
|
||||
b64ToArray(data.metadata)
|
||||
);
|
||||
const decoder = new TextDecoder();
|
||||
const meta = JSON.parse(decoder.decode(json));
|
||||
this.file.name = meta.name;
|
||||
this.file.type = meta.type;
|
||||
this.file.iv = meta.iv;
|
||||
this.file.size = data.size;
|
||||
this.file.ttl = data.ttl;
|
||||
this.state = 'ready';
|
||||
} catch (e) {
|
||||
this.state = 'invalid';
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
downloadFile(sig) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.onprogress = event => {
|
||||
if (event.lengthComputable && event.target.status !== 404) {
|
||||
this.progress = [event.loaded, event.total];
|
||||
this.emit('progress', this.progress);
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onload = event => {
|
||||
if (xhr.status === 404) {
|
||||
reject(new Error('notfound'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (xhr.status !== 200) {
|
||||
return reject(new Error(xhr.status));
|
||||
}
|
||||
|
||||
const blob = new Blob([xhr.response]);
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = function() {
|
||||
resolve(this.result);
|
||||
};
|
||||
|
||||
fileReader.readAsArrayBuffer(blob);
|
||||
};
|
||||
|
||||
xhr.open('get', this.url);
|
||||
xhr.setRequestHeader('Authorization', `send-v1 ${arrayToB64(sig)}`);
|
||||
xhr.responseType = 'blob';
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
|
||||
async download(nonce) {
|
||||
this.state = 'downloading';
|
||||
this.emit('progress', this.progress);
|
||||
try {
|
||||
const encryptKey = await this.encryptKeyPromise;
|
||||
const authKey = await this.authKeyPromise;
|
||||
const sig = await window.crypto.subtle.sign(
|
||||
{
|
||||
name: 'HMAC'
|
||||
},
|
||||
authKey,
|
||||
b64ToArray(nonce)
|
||||
);
|
||||
const ciphertext = await this.downloadFile(new Uint8Array(sig));
|
||||
this.msg = 'decryptingFile';
|
||||
this.emit('decrypting');
|
||||
const plaintext = await window.crypto.subtle.decrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: b64ToArray(this.file.iv),
|
||||
tagLength: 128
|
||||
},
|
||||
encryptKey,
|
||||
ciphertext
|
||||
);
|
||||
this.msg = 'downloadFinish';
|
||||
this.state = 'complete';
|
||||
return {
|
||||
plaintext,
|
||||
name: decodeURIComponent(this.file.name),
|
||||
type: this.file.type
|
||||
};
|
||||
} catch (e) {
|
||||
this.state = 'invalid';
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
291
app/fileSender.js
Normal file
@@ -0,0 +1,291 @@
|
||||
import Nanobus from 'nanobus';
|
||||
import { arrayToB64, b64ToArray, bytes } from './utils';
|
||||
|
||||
export default class FileSender extends Nanobus {
|
||||
constructor(file) {
|
||||
super('FileSender');
|
||||
this.file = file;
|
||||
this.msg = 'importingFile';
|
||||
this.progress = [0, 1];
|
||||
this.cancelled = false;
|
||||
this.iv = window.crypto.getRandomValues(new Uint8Array(12));
|
||||
this.uploadXHR = new XMLHttpRequest();
|
||||
this.rawSecret = window.crypto.getRandomValues(new Uint8Array(16));
|
||||
this.secretKey = window.crypto.subtle.importKey(
|
||||
'raw',
|
||||
this.rawSecret,
|
||||
'HKDF',
|
||||
false,
|
||||
['deriveKey']
|
||||
);
|
||||
}
|
||||
|
||||
static delete(id, token) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!id || !token) {
|
||||
return reject();
|
||||
}
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', `/api/delete/${id}`);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
xhr.send(JSON.stringify({ delete_token: token }));
|
||||
});
|
||||
}
|
||||
|
||||
get progressRatio() {
|
||||
return this.progress[0] / this.progress[1];
|
||||
}
|
||||
|
||||
get sizes() {
|
||||
return {
|
||||
partialSize: bytes(this.progress[0]),
|
||||
totalSize: bytes(this.progress[1])
|
||||
};
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.cancelled = true;
|
||||
if (this.msg === 'fileSizeProgress') {
|
||||
this.uploadXHR.abort();
|
||||
}
|
||||
}
|
||||
|
||||
readFile() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsArrayBuffer(this.file);
|
||||
reader.onload = function(event) {
|
||||
const plaintext = new Uint8Array(this.result);
|
||||
resolve(plaintext);
|
||||
};
|
||||
reader.onerror = function(err) {
|
||||
reject(err);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
uploadFile(encrypted, metadata, rawAuth) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const dataView = new DataView(encrypted);
|
||||
const blob = new Blob([dataView], { type: 'application/octet-stream' });
|
||||
const fd = new FormData();
|
||||
fd.append('data', blob);
|
||||
|
||||
const xhr = this.uploadXHR;
|
||||
|
||||
xhr.upload.addEventListener('progress', e => {
|
||||
if (e.lengthComputable) {
|
||||
this.progress = [e.loaded, e.total];
|
||||
this.emit('progress', this.progress);
|
||||
}
|
||||
});
|
||||
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
if (xhr.status === 200) {
|
||||
const nonce = xhr
|
||||
.getResponseHeader('WWW-Authenticate')
|
||||
.split(' ')[1];
|
||||
this.progress = [1, 1];
|
||||
this.msg = 'notifyUploadDone';
|
||||
const responseObj = JSON.parse(xhr.responseText);
|
||||
return resolve({
|
||||
url: responseObj.url,
|
||||
id: responseObj.id,
|
||||
secretKey: arrayToB64(this.rawSecret),
|
||||
deleteToken: responseObj.delete,
|
||||
nonce
|
||||
});
|
||||
}
|
||||
this.msg = 'errorPageHeader';
|
||||
reject(new Error(xhr.status));
|
||||
}
|
||||
};
|
||||
|
||||
xhr.open('post', '/api/upload', true);
|
||||
xhr.setRequestHeader(
|
||||
'X-File-Metadata',
|
||||
arrayToB64(new Uint8Array(metadata))
|
||||
);
|
||||
xhr.setRequestHeader('Authorization', `send-v1 ${arrayToB64(rawAuth)}`);
|
||||
xhr.send(fd);
|
||||
this.msg = 'fileSizeProgress';
|
||||
});
|
||||
}
|
||||
|
||||
async upload() {
|
||||
const encoder = new TextEncoder();
|
||||
const secretKey = await this.secretKey;
|
||||
const encryptKey = await window.crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'HKDF',
|
||||
salt: new Uint8Array(),
|
||||
info: encoder.encode('encryption'),
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
secretKey,
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
length: 128
|
||||
},
|
||||
false,
|
||||
['encrypt']
|
||||
);
|
||||
const authKey = await window.crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'HKDF',
|
||||
salt: new Uint8Array(),
|
||||
info: encoder.encode('authentication'),
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
secretKey,
|
||||
{
|
||||
name: 'HMAC',
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
true,
|
||||
['sign']
|
||||
);
|
||||
const metaKey = await window.crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'HKDF',
|
||||
salt: new Uint8Array(),
|
||||
info: encoder.encode('metadata'),
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
secretKey,
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
length: 128
|
||||
},
|
||||
false,
|
||||
['encrypt']
|
||||
);
|
||||
const plaintext = await this.readFile();
|
||||
if (this.cancelled) {
|
||||
throw new Error(0);
|
||||
}
|
||||
this.msg = 'encryptingFile';
|
||||
this.emit('encrypting');
|
||||
const encrypted = await window.crypto.subtle.encrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: this.iv,
|
||||
tagLength: 128
|
||||
},
|
||||
encryptKey,
|
||||
plaintext
|
||||
);
|
||||
const metadata = await window.crypto.subtle.encrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: new Uint8Array(12),
|
||||
tagLength: 128
|
||||
},
|
||||
metaKey,
|
||||
encoder.encode(
|
||||
JSON.stringify({
|
||||
iv: arrayToB64(this.iv),
|
||||
name: this.file.name,
|
||||
type: this.file.type || 'application/octet-stream'
|
||||
})
|
||||
)
|
||||
);
|
||||
const rawAuth = await window.crypto.subtle.exportKey('raw', authKey);
|
||||
if (this.cancelled) {
|
||||
throw new Error(0);
|
||||
}
|
||||
return this.uploadFile(encrypted, metadata, new Uint8Array(rawAuth));
|
||||
}
|
||||
|
||||
static async setPassword(password, file) {
|
||||
const encoder = new TextEncoder();
|
||||
const secretKey = await window.crypto.subtle.importKey(
|
||||
'raw',
|
||||
b64ToArray(file.secretKey),
|
||||
'HKDF',
|
||||
false,
|
||||
['deriveKey']
|
||||
);
|
||||
const authKey = await window.crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'HKDF',
|
||||
salt: new Uint8Array(),
|
||||
info: encoder.encode('authentication'),
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
secretKey,
|
||||
{
|
||||
name: 'HMAC',
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
true,
|
||||
['sign']
|
||||
);
|
||||
const sig = await window.crypto.subtle.sign(
|
||||
{
|
||||
name: 'HMAC'
|
||||
},
|
||||
authKey,
|
||||
b64ToArray(file.nonce)
|
||||
);
|
||||
const pwdKey = await window.crypto.subtle.importKey(
|
||||
'raw',
|
||||
encoder.encode(password),
|
||||
{ name: 'PBKDF2' },
|
||||
false,
|
||||
['deriveKey']
|
||||
);
|
||||
const newAuthKey = await window.crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'PBKDF2',
|
||||
salt: encoder.encode(file.url),
|
||||
iterations: 100,
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
pwdKey,
|
||||
{
|
||||
name: 'HMAC',
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
true,
|
||||
['sign']
|
||||
);
|
||||
const rawAuth = await window.crypto.subtle.exportKey('raw', newAuthKey);
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
if (xhr.status === 200) {
|
||||
return resolve(xhr.response);
|
||||
}
|
||||
if (xhr.status === 401) {
|
||||
const nonce = xhr
|
||||
.getResponseHeader('WWW-Authenticate')
|
||||
.split(' ')[1];
|
||||
file.nonce = nonce;
|
||||
}
|
||||
reject(new Error(xhr.status));
|
||||
}
|
||||
};
|
||||
xhr.onerror = () => reject(new Error(0));
|
||||
xhr.ontimeout = () => reject(new Error(0));
|
||||
xhr.open('post', `/api/password/${file.id}`);
|
||||
xhr.setRequestHeader(
|
||||
'Authorization',
|
||||
`send-v1 ${arrayToB64(new Uint8Array(sig))}`
|
||||
);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
xhr.responseType = 'json';
|
||||
xhr.timeout = 2000;
|
||||
xhr.send(JSON.stringify({ auth: arrayToB64(new Uint8Array(rawAuth)) }));
|
||||
});
|
||||
}
|
||||
}
|
||||
50
app/main.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import app from './routes';
|
||||
import locale from '../common/locales';
|
||||
import fileManager from './fileManager';
|
||||
import dragManager from './dragManager';
|
||||
import { canHasSend } from './utils';
|
||||
import assets from '../common/assets';
|
||||
import storage from './storage';
|
||||
import metrics from './metrics';
|
||||
import experiments from './experiments';
|
||||
import Raven from 'raven-js';
|
||||
|
||||
if (navigator.doNotTrack !== '1' && window.RAVEN_CONFIG) {
|
||||
Raven.config(window.SENTRY_ID, window.RAVEN_CONFIG).install();
|
||||
}
|
||||
|
||||
app.use((state, emitter) => {
|
||||
// init state
|
||||
state.transfer = null;
|
||||
state.fileInfo = null;
|
||||
state.translate = locale.getTranslator();
|
||||
state.storage = storage;
|
||||
state.raven = Raven;
|
||||
emitter.on('DOMContentLoaded', async () => {
|
||||
let reason = null;
|
||||
if (
|
||||
/firefox/i.test(navigator.userAgent) &&
|
||||
parseInt(navigator.userAgent.match(/firefox\/*([^\n\r]*)\./i)[1], 10) <=
|
||||
49
|
||||
) {
|
||||
reason = 'outdated';
|
||||
}
|
||||
if (/edge\/\d+/i.test(navigator.userAgent)) {
|
||||
reason = 'edge';
|
||||
}
|
||||
const ok = await canHasSend(assets.get('cryptofill.js'));
|
||||
if (!ok) {
|
||||
reason = /firefox/i.test(navigator.userAgent) ? 'outdated' : 'gcm';
|
||||
}
|
||||
if (reason) {
|
||||
setTimeout(() => emitter.emit('replaceState', `/unsupported/${reason}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.use(metrics);
|
||||
app.use(fileManager);
|
||||
app.use(dragManager);
|
||||
app.use(experiments);
|
||||
|
||||
app.mount('#page-one');
|
||||
@@ -1,12 +1,11 @@
|
||||
import testPilotGA from 'testpilot-ga/src/TestPilotGA';
|
||||
import Storage from './storage';
|
||||
const storage = new Storage();
|
||||
import storage from './storage';
|
||||
|
||||
let hasLocalStorage = false;
|
||||
try {
|
||||
hasLocalStorage = !!localStorage;
|
||||
hasLocalStorage = typeof localStorage !== 'undefined';
|
||||
} catch (e) {
|
||||
// don't care
|
||||
// when disabled, any mention of localStorage throws an error
|
||||
}
|
||||
|
||||
const analytics = new testPilotGA({
|
||||
@@ -15,19 +14,45 @@ const analytics = new testPilotGA({
|
||||
tid: window.GOOGLE_ANALYTICS_ID
|
||||
});
|
||||
|
||||
const category = location.pathname.includes('/download')
|
||||
? 'recipient'
|
||||
: 'sender';
|
||||
let appState = null;
|
||||
let experiment = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
addExitHandlers();
|
||||
addRestartHandlers();
|
||||
});
|
||||
export default function initialize(state, emitter) {
|
||||
appState = state;
|
||||
emitter.on('DOMContentLoaded', () => {
|
||||
addExitHandlers();
|
||||
experiment = storage.enrolled[0];
|
||||
sendEvent(category(), 'visit', {
|
||||
cm5: storage.totalUploads,
|
||||
cm6: storage.files.length,
|
||||
cm7: storage.totalDownloads
|
||||
});
|
||||
//TODO restart handlers... somewhere
|
||||
});
|
||||
}
|
||||
|
||||
function category() {
|
||||
switch (appState.route) {
|
||||
case '/':
|
||||
case '/share/:id':
|
||||
return 'sender';
|
||||
case '/download/:id/:key':
|
||||
case '/download/:id':
|
||||
case '/completed':
|
||||
return 'recipient';
|
||||
default:
|
||||
return 'other';
|
||||
}
|
||||
}
|
||||
|
||||
function sendEvent() {
|
||||
const args = Array.from(arguments);
|
||||
if (experiment && args[2]) {
|
||||
args[2].xid = experiment[0];
|
||||
args[2].xvar = experiment[1];
|
||||
}
|
||||
return (
|
||||
hasLocalStorage &&
|
||||
analytics.sendEvent.apply(analytics, arguments).catch(() => 0)
|
||||
hasLocalStorage && analytics.sendEvent.apply(analytics, args).catch(() => 0)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -62,11 +87,11 @@ function urlToMetric(url) {
|
||||
}
|
||||
|
||||
function setReferrer(state) {
|
||||
if (category === 'sender') {
|
||||
if (category() === 'sender') {
|
||||
if (state) {
|
||||
storage.referrer = `${state}-upload`;
|
||||
}
|
||||
} else if (category === 'recipient') {
|
||||
} else if (category() === 'recipient') {
|
||||
if (state) {
|
||||
storage.referrer = `${state}-download`;
|
||||
}
|
||||
@@ -87,10 +112,10 @@ function takeReferrer() {
|
||||
}
|
||||
|
||||
function startedUpload(params) {
|
||||
return sendEvent(category, 'upload-started', {
|
||||
return sendEvent('sender', 'upload-started', {
|
||||
cm1: params.size,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: storage.numFiles + 1,
|
||||
cm6: storage.files.length + 1,
|
||||
cm7: storage.totalDownloads,
|
||||
cd1: params.type,
|
||||
cd5: takeReferrer()
|
||||
@@ -99,10 +124,10 @@ function startedUpload(params) {
|
||||
|
||||
function cancelledUpload(params) {
|
||||
setReferrer('cancelled');
|
||||
return sendEvent(category, 'upload-stopped', {
|
||||
return sendEvent('sender', 'upload-stopped', {
|
||||
cm1: params.size,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: storage.numFiles,
|
||||
cm6: storage.files.length,
|
||||
cm7: storage.totalDownloads,
|
||||
cd1: params.type,
|
||||
cd2: 'cancelled'
|
||||
@@ -110,33 +135,42 @@ function cancelledUpload(params) {
|
||||
}
|
||||
|
||||
function completedUpload(params) {
|
||||
return sendEvent(category, 'upload-stopped', {
|
||||
return sendEvent('sender', 'upload-stopped', {
|
||||
cm1: params.size,
|
||||
cm2: params.time,
|
||||
cm3: params.speed,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: storage.numFiles,
|
||||
cm6: storage.files.length,
|
||||
cm7: storage.totalDownloads,
|
||||
cd1: params.type,
|
||||
cd2: 'completed'
|
||||
});
|
||||
}
|
||||
|
||||
function addedPassword(params) {
|
||||
return sendEvent('sender', 'password-added', {
|
||||
cm1: params.size,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: storage.files.length,
|
||||
cm7: storage.totalDownloads
|
||||
});
|
||||
}
|
||||
|
||||
function startedDownload(params) {
|
||||
return sendEvent(category, 'download-started', {
|
||||
return sendEvent('recipient', 'download-started', {
|
||||
cm1: params.size,
|
||||
cm4: params.ttl,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: storage.numFiles,
|
||||
cm6: storage.files.length,
|
||||
cm7: storage.totalDownloads
|
||||
});
|
||||
}
|
||||
|
||||
function stoppedDownload(params) {
|
||||
return sendEvent(category, 'download-stopped', {
|
||||
return sendEvent('recipient', 'download-stopped', {
|
||||
cm1: params.size,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: storage.numFiles,
|
||||
cm6: storage.files.length,
|
||||
cm7: storage.totalDownloads,
|
||||
cd2: 'errored',
|
||||
cd6: params.err
|
||||
@@ -145,20 +179,20 @@ function stoppedDownload(params) {
|
||||
|
||||
function cancelledDownload(params) {
|
||||
setReferrer('cancelled');
|
||||
return sendEvent(category, 'download-stopped', {
|
||||
return sendEvent('recipient', 'download-stopped', {
|
||||
cm1: params.size,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: storage.numFiles,
|
||||
cm6: storage.files.length,
|
||||
cm7: storage.totalDownloads,
|
||||
cd2: 'cancelled'
|
||||
});
|
||||
}
|
||||
|
||||
function stoppedUpload(params) {
|
||||
return sendEvent(category, 'upload-stopped', {
|
||||
return sendEvent('sender', 'upload-stopped', {
|
||||
cm1: params.size,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: storage.numFiles,
|
||||
cm6: storage.files.length,
|
||||
cm7: storage.totalDownloads,
|
||||
cd1: params.type,
|
||||
cd2: 'errored',
|
||||
@@ -167,25 +201,25 @@ function stoppedUpload(params) {
|
||||
}
|
||||
|
||||
function completedDownload(params) {
|
||||
return sendEvent(category, 'download-stopped', {
|
||||
return sendEvent('recipient', 'download-stopped', {
|
||||
cm1: params.size,
|
||||
cm2: params.time,
|
||||
cm3: params.speed,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: storage.numFiles,
|
||||
cm6: storage.files.length,
|
||||
cm7: storage.totalDownloads,
|
||||
cd2: 'completed'
|
||||
});
|
||||
}
|
||||
|
||||
function deletedUpload(params) {
|
||||
return sendEvent(category, 'upload-deleted', {
|
||||
return sendEvent(category(), 'upload-deleted', {
|
||||
cm1: params.size,
|
||||
cm2: params.time,
|
||||
cm3: params.speed,
|
||||
cm4: params.ttl,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: storage.numFiles,
|
||||
cm6: storage.files.length,
|
||||
cm7: storage.totalDownloads,
|
||||
cd1: params.type,
|
||||
cd4: params.location
|
||||
@@ -193,19 +227,19 @@ function deletedUpload(params) {
|
||||
}
|
||||
|
||||
function unsupported(params) {
|
||||
return sendEvent(category, 'unsupported', {
|
||||
return sendEvent(category(), 'unsupported', {
|
||||
cd6: params.err
|
||||
});
|
||||
}
|
||||
|
||||
function copiedLink(params) {
|
||||
return sendEvent(category, 'copied', {
|
||||
return sendEvent('sender', 'copied', {
|
||||
cd4: params.location
|
||||
});
|
||||
}
|
||||
|
||||
function exitEvent(target) {
|
||||
return sendEvent(category, 'exited', {
|
||||
return sendEvent(category(), 'exited', {
|
||||
cd3: urlToMetric(target.currentTarget.href)
|
||||
});
|
||||
}
|
||||
@@ -219,21 +253,13 @@ function addExitHandlers() {
|
||||
});
|
||||
}
|
||||
|
||||
function restartEvent(state) {
|
||||
function restart(state) {
|
||||
setReferrer(state);
|
||||
return sendEvent(category, 'restarted', {
|
||||
return sendEvent(category(), 'restarted', {
|
||||
cd2: state
|
||||
});
|
||||
}
|
||||
|
||||
function addRestartHandlers() {
|
||||
const elements = Array.from(document.querySelectorAll('.send-new'));
|
||||
elements.forEach(el => {
|
||||
const state = el.getAttribute('data-state');
|
||||
el.addEventListener('click', restartEvent.bind(null, state));
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
copiedLink,
|
||||
startedUpload,
|
||||
@@ -245,5 +271,7 @@ export {
|
||||
cancelledDownload,
|
||||
stoppedDownload,
|
||||
completedDownload,
|
||||
addedPassword,
|
||||
restart,
|
||||
unsupported
|
||||
};
|
||||
12
app/routes/download.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const preview = require('../templates/preview');
|
||||
const download = require('../templates/download');
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
if (state.transfer) {
|
||||
const s = state.transfer.state;
|
||||
if (s === 'downloading' || s === 'complete') {
|
||||
return download(state, emit);
|
||||
}
|
||||
}
|
||||
return preview(state, emit);
|
||||
};
|
||||
10
app/routes/home.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const welcome = require('../templates/welcome');
|
||||
const upload = require('../templates/upload');
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
if (state.transfer && state.transfer.iv) {
|
||||
//TODO relying on 'iv' is gross
|
||||
return upload(state, emit);
|
||||
}
|
||||
return welcome(state, emit);
|
||||
};
|
||||
17
app/routes/index.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const choo = require('choo');
|
||||
const download = require('./download');
|
||||
|
||||
const app = choo();
|
||||
|
||||
app.route('/', require('./home'));
|
||||
app.route('/share/:id', require('../templates/share'));
|
||||
app.route('/download/:id', download);
|
||||
app.route('/download/:id/:key', download);
|
||||
app.route('/completed', require('../templates/completed'));
|
||||
app.route('/unsupported/:reason', require('../templates/unsupported'));
|
||||
app.route('/legal', require('../templates/legal'));
|
||||
app.route('/error', require('../templates/error'));
|
||||
app.route('/blank', require('../templates/blank'));
|
||||
app.route('*', require('../templates/notFound'));
|
||||
|
||||
module.exports = app;
|
||||
@@ -26,13 +26,34 @@ class Mem {
|
||||
}
|
||||
}
|
||||
|
||||
export default class Storage {
|
||||
class Storage {
|
||||
constructor() {
|
||||
try {
|
||||
this.engine = localStorage || new Mem();
|
||||
} catch (e) {
|
||||
this.engine = new Mem();
|
||||
}
|
||||
this._files = this.loadFiles();
|
||||
}
|
||||
|
||||
loadFiles() {
|
||||
const fs = [];
|
||||
for (let i = 0; i < this.engine.length; i++) {
|
||||
const k = this.engine.key(i);
|
||||
if (isFile(k)) {
|
||||
try {
|
||||
const f = JSON.parse(this.engine.getItem(k));
|
||||
if (!f.id) {
|
||||
f.id = f.fileId;
|
||||
}
|
||||
fs.push(f);
|
||||
} catch (err) {
|
||||
// obviously you're not a golfer
|
||||
this.engine.removeItem(k);
|
||||
}
|
||||
}
|
||||
}
|
||||
return fs.sort((a, b) => a.createdAt - b.createdAt);
|
||||
}
|
||||
|
||||
get totalDownloads() {
|
||||
@@ -53,51 +74,46 @@ export default class Storage {
|
||||
set referrer(str) {
|
||||
this.engine.setItem('referrer', str);
|
||||
}
|
||||
|
||||
get files() {
|
||||
const fs = [];
|
||||
for (let i = 0; i < this.engine.length; i++) {
|
||||
const k = this.engine.key(i);
|
||||
if (isFile(k)) {
|
||||
try {
|
||||
fs.push(JSON.parse(this.engine.getItem(k)));
|
||||
} catch (err) {
|
||||
// obviously you're not a golfer
|
||||
this.engine.removeItem(k);
|
||||
}
|
||||
}
|
||||
}
|
||||
return fs.sort((file1, file2) => {
|
||||
const creationDate1 = new Date(file1.creationDate);
|
||||
const creationDate2 = new Date(file2.creationDate);
|
||||
return creationDate1 - creationDate2;
|
||||
});
|
||||
get enrolled() {
|
||||
return JSON.parse(this.engine.getItem('experiments') || '[]');
|
||||
}
|
||||
|
||||
get numFiles() {
|
||||
let length = 0;
|
||||
for (let i = 0; i < this.engine.length; i++) {
|
||||
const k = this.engine.key(i);
|
||||
if (isFile(k)) {
|
||||
length += 1;
|
||||
}
|
||||
enroll(id, variant) {
|
||||
const enrolled = this.enrolled;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
if (!enrolled.find(([i, v]) => i === id)) {
|
||||
enrolled.push([id, variant]);
|
||||
this.engine.setItem('experiments', JSON.stringify(enrolled));
|
||||
}
|
||||
return length;
|
||||
}
|
||||
|
||||
get files() {
|
||||
return this._files;
|
||||
}
|
||||
|
||||
getFileById(id) {
|
||||
try {
|
||||
return JSON.parse(this.engine.getItem(id));
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
return this._files.find(f => f.id === id);
|
||||
}
|
||||
|
||||
get(id) {
|
||||
return this.engine.getItem(id);
|
||||
}
|
||||
|
||||
remove(property) {
|
||||
if (isFile(property)) {
|
||||
this._files.splice(this._files.findIndex(f => f.id === property), 1);
|
||||
}
|
||||
this.engine.removeItem(property);
|
||||
}
|
||||
|
||||
addFile(id, file) {
|
||||
this.engine.setItem(id, JSON.stringify(file));
|
||||
addFile(file) {
|
||||
this._files.push(file);
|
||||
this.engine.setItem(file.id, JSON.stringify(file));
|
||||
}
|
||||
|
||||
writeFiles() {
|
||||
this._files.forEach(f => this.engine.setItem(f.id, JSON.stringify(f)));
|
||||
}
|
||||
}
|
||||
|
||||
export default new Storage();
|
||||
9
app/templates/blank.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const html = require('choo/html');
|
||||
|
||||
module.exports = function(state) {
|
||||
const div = html`<div id="page-one"></div>`;
|
||||
if (state.layout) {
|
||||
return state.layout(state, div);
|
||||
}
|
||||
return div;
|
||||
};
|
||||
31
app/templates/completed.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const html = require('choo/html');
|
||||
const progress = require('./progress');
|
||||
const { fadeOut } = require('../utils');
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
const div = html`
|
||||
<div id="download" class="fadeIn">
|
||||
<div id="download-progress">
|
||||
<div id="dl-title" class="title">${state.translate(
|
||||
'downloadFinish'
|
||||
)}</div>
|
||||
<div class="description"></div>
|
||||
${progress(1)}
|
||||
<div class="upload">
|
||||
<div class="progress-text"></div>
|
||||
</div>
|
||||
</div>
|
||||
<a class="send-new" data-state="completed" href="/" onclick=${sendNew}>${state.translate(
|
||||
'sendYourFilesLink'
|
||||
)}</a>
|
||||
</div>
|
||||
`;
|
||||
|
||||
async function sendNew(e) {
|
||||
e.preventDefault();
|
||||
await fadeOut('download');
|
||||
emit('pushState', '/');
|
||||
}
|
||||
|
||||
return div;
|
||||
};
|
||||
28
app/templates/download.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const html = require('choo/html');
|
||||
const progress = require('./progress');
|
||||
const { bytes } = require('../utils');
|
||||
|
||||
module.exports = function(state) {
|
||||
const transfer = state.transfer;
|
||||
const div = html`
|
||||
<div id="download-progress" class="fadeIn">
|
||||
<div id="dl-title" class="title">${state.translate(
|
||||
'downloadingPageProgress',
|
||||
{
|
||||
filename: state.fileInfo.name,
|
||||
size: bytes(state.fileInfo.size)
|
||||
}
|
||||
)}</div>
|
||||
<div class="description">${state.translate('downloadingPageMessage')}</div>
|
||||
${progress(transfer.progressRatio)}
|
||||
<div class="upload">
|
||||
<div class="progress-text">${state.translate(
|
||||
transfer.msg,
|
||||
transfer.sizes
|
||||
)}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return div;
|
||||
};
|
||||
56
app/templates/downloadPassword.js
Normal file
@@ -0,0 +1,56 @@
|
||||
const html = require('choo/html');
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
const fileInfo = state.fileInfo;
|
||||
const label =
|
||||
fileInfo.password === null
|
||||
? html`
|
||||
<label class="red"
|
||||
for="unlock-input">${state.translate('passwordTryAgain')}</label>`
|
||||
: html`
|
||||
<label for="unlock-input">
|
||||
${state.translate('unlockInputLabel')}
|
||||
</label>`;
|
||||
const div = html`
|
||||
<div class="enterPassword">
|
||||
${label}
|
||||
<form id="unlock" onsubmit=${checkPassword} data-no-csrf>
|
||||
<input id="unlock-input"
|
||||
class="unlock-input input-no-btn"
|
||||
maxlength="64"
|
||||
autocomplete="off"
|
||||
placeholder="${state.translate('unlockInputPlaceholder')}"
|
||||
oninput=${inputChanged}
|
||||
type="password"/>
|
||||
<input type="submit"
|
||||
id="unlock-btn"
|
||||
class="btn btn-hidden"
|
||||
value="${state.translate('unlockButtonLabel')}"/>
|
||||
</form>
|
||||
</div>`;
|
||||
|
||||
function inputChanged() {
|
||||
const input = document.getElementById('unlock-input');
|
||||
const btn = document.getElementById('unlock-btn');
|
||||
if (input.value.length > 0) {
|
||||
btn.classList.remove('btn-hidden');
|
||||
input.classList.remove('input-no-btn');
|
||||
} else {
|
||||
btn.classList.add('btn-hidden');
|
||||
input.classList.add('input-no-btn');
|
||||
}
|
||||
}
|
||||
|
||||
function checkPassword(event) {
|
||||
event.preventDefault();
|
||||
const password = document.getElementById('unlock-input').value;
|
||||
if (password.length > 0) {
|
||||
document.getElementById('unlock-btn').disabled = true;
|
||||
state.fileInfo.url = window.location.href;
|
||||
state.fileInfo.password = password;
|
||||
emit('preview');
|
||||
}
|
||||
}
|
||||
|
||||
return div;
|
||||
};
|
||||
10
app/templates/error.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const html = require('choo/html');
|
||||
const assets = require('../../common/assets');
|
||||
|
||||
module.exports = function(state) {
|
||||
return html`
|
||||
<div id="upload-error">
|
||||
<div class="title">${state.translate('errorPageHeader')}</div>
|
||||
<img id="upload-error-img" src="${assets.get('illustration_error.svg')}"/>
|
||||
</div>`;
|
||||
};
|
||||
84
app/templates/file.js
Normal file
@@ -0,0 +1,84 @@
|
||||
const html = require('choo/html');
|
||||
const assets = require('../../common/assets');
|
||||
|
||||
function timeLeft(milliseconds) {
|
||||
const minutes = Math.floor(milliseconds / 1000 / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const seconds = Math.floor((milliseconds / 1000) % 60);
|
||||
if (hours >= 1) {
|
||||
return `${hours}h ${minutes % 60}m`;
|
||||
} else if (hours === 0) {
|
||||
return `${minutes}m ${seconds}s`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = function(file, state, emit) {
|
||||
const ttl = file.expiresAt - Date.now();
|
||||
const remaining = timeLeft(ttl) || state.translate('linkExpiredAlt');
|
||||
const row = html`
|
||||
<tr id="${file.id}">
|
||||
<td class="overflow-col" title="${file.name}">${file.name}</td>
|
||||
<td class="center-col">
|
||||
<img onclick=${copyClick} src="${assets.get(
|
||||
'copy-16.svg'
|
||||
)}" class="icon-copy" title="${state.translate('copyUrlHover')}">
|
||||
<span class="text-copied" hidden="true">${state.translate(
|
||||
'copiedUrl'
|
||||
)}</span>
|
||||
</td>
|
||||
<td>${remaining}</td>
|
||||
<td class="center-col">
|
||||
<img onclick=${showPopup} src="${assets.get(
|
||||
'close-16.svg'
|
||||
)}" class="icon-delete" title="${state.translate('deleteButtonHover')}">
|
||||
<div class="popup">
|
||||
<div class="popuptext" onblur=${cancel} tabindex="-1">
|
||||
<div class="popup-message">${state.translate('deletePopupText')}</div>
|
||||
<div class="popup-action">
|
||||
<span class="popup-no" onclick=${cancel}>${state.translate(
|
||||
'deletePopupCancel'
|
||||
)}</span>
|
||||
<span class="popup-yes" onclick=${deleteFile}>${state.translate(
|
||||
'deletePopupYes'
|
||||
)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
function copyClick(e) {
|
||||
emit('copy', { url: file.url, location: 'upload-list' });
|
||||
const icon = e.target;
|
||||
const text = e.target.nextSibling;
|
||||
icon.hidden = true;
|
||||
text.hidden = false;
|
||||
setTimeout(() => {
|
||||
icon.hidden = false;
|
||||
text.hidden = true;
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function showPopup() {
|
||||
const tr = document.getElementById(file.id);
|
||||
const popup = tr.querySelector('.popuptext');
|
||||
popup.classList.add('show');
|
||||
popup.focus();
|
||||
}
|
||||
|
||||
function cancel(e) {
|
||||
e.stopPropagation();
|
||||
const tr = document.getElementById(file.id);
|
||||
const popup = tr.querySelector('.popuptext');
|
||||
popup.classList.remove('show');
|
||||
}
|
||||
|
||||
function deleteFile() {
|
||||
emit('delete', { file, location: 'upload-list' });
|
||||
emit('render');
|
||||
}
|
||||
|
||||
return row;
|
||||
};
|
||||
32
app/templates/fileList.js
Normal file
@@ -0,0 +1,32 @@
|
||||
const html = require('choo/html');
|
||||
const file = require('./file');
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
let table = '';
|
||||
if (state.storage.files.length) {
|
||||
table = html`
|
||||
<table id="uploaded-files">
|
||||
<thead>
|
||||
<tr>
|
||||
<th id="uploaded-file">${state.translate('uploadedFile')}</th>
|
||||
<th id="copy-file-list" class="center-col">${state.translate(
|
||||
'copyFileList'
|
||||
)}</th>
|
||||
<th id="expiry-file-list">${state.translate('expiryFileList')}</th>
|
||||
<th id="delete-file-list" class="center-col">${state.translate(
|
||||
'deleteFileList'
|
||||
)}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${state.storage.files.map(f => file(f, state, emit))}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
}
|
||||
return html`
|
||||
<div id="file-list">
|
||||
${table}
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
38
app/templates/legal.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const html = require('choo/html');
|
||||
|
||||
function replaceLinks(str, urls) {
|
||||
let i = -1;
|
||||
const s = str.replace(/<a>([^<]+)<\/a>/g, (m, v) => {
|
||||
i++;
|
||||
return `<a href="${urls[i]}">${v}</a>`;
|
||||
});
|
||||
return [`<div class="description">${s}</div>`];
|
||||
}
|
||||
|
||||
module.exports = function(state) {
|
||||
const div = html`
|
||||
<div id="page-one">
|
||||
<div id="legal">
|
||||
<div class="title">${state.translate('legalHeader')}</div>
|
||||
${html(
|
||||
replaceLinks(state.translate('legalNoticeTestPilot'), [
|
||||
'https://testpilot.firefox.com/terms',
|
||||
'https://testpilot.firefox.com/privacy',
|
||||
'https://testpilot.firefox.com/experiments/send'
|
||||
])
|
||||
)}
|
||||
${html(
|
||||
replaceLinks(state.translate('legalNoticeMozilla'), [
|
||||
'https://www.mozilla.org/privacy/websites/',
|
||||
'https://www.mozilla.org/about/legal/terms/mozilla/'
|
||||
])
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (state.layout) {
|
||||
return state.layout(state, div);
|
||||
}
|
||||
return div;
|
||||
};
|
||||
25
app/templates/notFound.js
Normal file
@@ -0,0 +1,25 @@
|
||||
const html = require('choo/html');
|
||||
const assets = require('../../common/assets');
|
||||
|
||||
module.exports = function(state) {
|
||||
const div = html`
|
||||
<div id="page-one">
|
||||
<div id="download">
|
||||
<div class="title">${state.translate('expiredPageHeader')}</div>
|
||||
<div class="share-window">
|
||||
<img src="${assets.get('illustration_expired.svg')}" id="expired-img">
|
||||
</div>
|
||||
<div class="expired-description">${state.translate(
|
||||
'uploadPageExplainer'
|
||||
)}</div>
|
||||
<a class="send-new" href="/" data-state="notfound">${state.translate(
|
||||
'sendYourFilesLink'
|
||||
)}</a>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
if (state.layout) {
|
||||
return state.layout(state, div);
|
||||
}
|
||||
return div;
|
||||
};
|
||||
76
app/templates/preview.js
Normal file
@@ -0,0 +1,76 @@
|
||||
const html = require('choo/html');
|
||||
const assets = require('../../common/assets');
|
||||
const notFound = require('./notFound');
|
||||
const downloadPassword = require('./downloadPassword');
|
||||
const { bytes } = require('../utils');
|
||||
|
||||
function getFileFromDOM() {
|
||||
const el = document.getElementById('dl-file');
|
||||
if (!el) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
nonce: el.getAttribute('data-nonce'),
|
||||
pwd: !!+el.getAttribute('data-requires-password')
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
state.fileInfo = state.fileInfo || getFileFromDOM();
|
||||
if (!state.fileInfo) {
|
||||
return notFound(state, emit);
|
||||
}
|
||||
state.fileInfo.id = state.params.id;
|
||||
state.fileInfo.key = state.params.key;
|
||||
const fileInfo = state.fileInfo;
|
||||
const size = fileInfo.size
|
||||
? state.translate('downloadFileSize', { size: bytes(fileInfo.size) })
|
||||
: '';
|
||||
let action = html`
|
||||
<div>
|
||||
<img src="${assets.get('illustration_download.svg')}"
|
||||
id="download-img"
|
||||
alt="${state.translate('downloadAltText')}"/>
|
||||
<div>
|
||||
<button id="download-btn"
|
||||
class="btn"
|
||||
onclick=${download}>${state.translate('downloadButtonLabel')}
|
||||
</button>
|
||||
</div>
|
||||
</div>`;
|
||||
if (fileInfo.pwd && !fileInfo.password) {
|
||||
action = downloadPassword(state, emit);
|
||||
} else if (!state.transfer) {
|
||||
emit('preview');
|
||||
}
|
||||
const title = fileInfo.name
|
||||
? state.translate('downloadFileName', { filename: fileInfo.name })
|
||||
: state.translate('downloadFileTitle');
|
||||
const div = html`
|
||||
<div id="page-one">
|
||||
<div id="download">
|
||||
<div id="download-page-one">
|
||||
<div class="title">
|
||||
<span id="dl-file"
|
||||
data-nonce="${fileInfo.nonce}"
|
||||
data-requires-password="${fileInfo.pwd}">${title}</span>
|
||||
<span id="dl-filesize">${' ' + size}</span>
|
||||
</div>
|
||||
<div class="description">${state.translate('downloadMessage')}</div>
|
||||
${action}
|
||||
</div>
|
||||
<a class="send-new" href="/">${state.translate('sendYourFilesLink')}</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
function download(event) {
|
||||
event.preventDefault();
|
||||
emit('download', fileInfo);
|
||||
}
|
||||
|
||||
if (state.layout) {
|
||||
return state.layout(state, div);
|
||||
}
|
||||
return div;
|
||||
};
|
||||
21
app/templates/progress.js
Normal file
@@ -0,0 +1,21 @@
|
||||
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;
|
||||
};
|
||||
90
app/templates/share.js
Normal file
@@ -0,0 +1,90 @@
|
||||
const html = require('choo/html');
|
||||
const assets = require('../../common/assets');
|
||||
const notFound = require('./notFound');
|
||||
const uploadPassword = require('./uploadPassword');
|
||||
const { allowedCopy, delay, fadeOut } = require('../utils');
|
||||
|
||||
function passwordComplete(state, password) {
|
||||
const el = html([
|
||||
`<div class="selectPassword">${state.translate('passwordResult', {
|
||||
password: '<pre></pre>'
|
||||
})}</div>`
|
||||
]);
|
||||
el.lastElementChild.textContent = password;
|
||||
return el;
|
||||
}
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
const file = state.storage.getFileById(state.params.id);
|
||||
if (!file) {
|
||||
return notFound(state, emit);
|
||||
}
|
||||
|
||||
file.password = file.password || '';
|
||||
|
||||
const passwordSection = file.password
|
||||
? passwordComplete(state, file.password)
|
||||
: uploadPassword(state, emit);
|
||||
const div = html`
|
||||
<div id="share-link" class="fadeIn">
|
||||
<div class="title">${state.translate('uploadSuccessTimingHeader')}</div>
|
||||
<div id="share-window">
|
||||
<div id="copy-text">
|
||||
${state.translate('copyUrlFormLabelWithName', {
|
||||
filename: file.name
|
||||
})}</div>
|
||||
<div id="copy">
|
||||
<input id="link" type="url" value="${file.url}" readonly="true"/>
|
||||
<button id="copy-btn"
|
||||
class="btn"
|
||||
title="${state.translate('copyUrlFormButton')}"
|
||||
onclick=${copyLink}>${state.translate('copyUrlFormButton')}</button>
|
||||
</div>
|
||||
${passwordSection}
|
||||
<button id="delete-file"
|
||||
class="btn"
|
||||
title="${state.translate('deleteFileButton')}"
|
||||
onclick=${deleteFile}>${state.translate('deleteFileButton')}</button>
|
||||
<a class="send-new"
|
||||
data-state="completed"
|
||||
href="/"
|
||||
onclick=${sendNew}>${state.translate('sendAnotherFileLink')}</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
async function sendNew(e) {
|
||||
e.preventDefault();
|
||||
await fadeOut('share-link');
|
||||
emit('pushState', '/');
|
||||
}
|
||||
|
||||
async function copyLink() {
|
||||
if (allowedCopy()) {
|
||||
emit('copy', { url: file.url, location: 'success-screen' });
|
||||
const input = document.getElementById('link');
|
||||
input.disabled = true;
|
||||
const copyBtn = document.getElementById('copy-btn');
|
||||
copyBtn.disabled = true;
|
||||
copyBtn.classList.add('success');
|
||||
copyBtn.replaceChild(
|
||||
html`<img src="${assets.get('check-16.svg')}" class="icon-check">`,
|
||||
copyBtn.firstChild
|
||||
);
|
||||
await delay(2000);
|
||||
input.disabled = false;
|
||||
if (!copyBtn.parentNode.classList.contains('wait-password')) {
|
||||
copyBtn.disabled = false;
|
||||
}
|
||||
copyBtn.classList.remove('success');
|
||||
copyBtn.textContent = state.translate('copyUrlFormButton');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteFile() {
|
||||
emit('delete', { file, location: 'success-screen' });
|
||||
await fadeOut('share-link');
|
||||
emit('pushState', '/');
|
||||
}
|
||||
return div;
|
||||
};
|
||||
50
app/templates/unsupported.js
Normal file
@@ -0,0 +1,50 @@
|
||||
const html = require('choo/html');
|
||||
const assets = require('../../common/assets');
|
||||
|
||||
module.exports = function(state) {
|
||||
const msg =
|
||||
state.params.reason === 'outdated'
|
||||
? html`
|
||||
<div id="unsupported-browser">
|
||||
<div class="title">${state.translate('notSupportedHeader')}</div>
|
||||
<div class="description">${state.translate(
|
||||
'notSupportedOutdatedDetail'
|
||||
)}</div>
|
||||
<a id="update-firefox" href="https://support.mozilla.org/kb/update-firefox-latest-version">
|
||||
<img src="${assets.get(
|
||||
'firefox_logo-only.svg'
|
||||
)}" class="firefox-logo" alt="Firefox"/>
|
||||
<div class="unsupported-button-text">${state.translate(
|
||||
'updateFirefox'
|
||||
)}</div>
|
||||
</a>
|
||||
<div class="unsupported-description">${state.translate(
|
||||
'uploadPageExplainer'
|
||||
)}</div>
|
||||
</div>`
|
||||
: html`
|
||||
<div id="unsupported-browser">
|
||||
<div class="title">${state.translate('notSupportedHeader')}</div>
|
||||
<div class="description">${state.translate('notSupportedDetail')}</div>
|
||||
<div class="description"><a href="https://github.com/mozilla/send/blob/master/docs/faq.md#why-is-my-browser-not-supported">${state.translate(
|
||||
'notSupportedLink'
|
||||
)}</a></div>
|
||||
<a id="dl-firefox" href="https://www.mozilla.org/firefox/new/?scene=2">
|
||||
<img src="${assets.get(
|
||||
'firefox_logo-only.svg'
|
||||
)}" class="firefox-logo" alt="Firefox"/>
|
||||
<div class="unsupported-button-text">Firefox<br>
|
||||
<span>${state.translate('downloadFirefoxButtonSub')}</span>
|
||||
</div>
|
||||
</a>
|
||||
<div class="unsupported-description">${state.translate(
|
||||
'uploadPageExplainer'
|
||||
)}</div>
|
||||
</div>`;
|
||||
const div = html`<div id="page-one">${msg}</div>`;
|
||||
|
||||
if (state.layout) {
|
||||
return state.layout(state, div);
|
||||
}
|
||||
return div;
|
||||
};
|
||||
38
app/templates/upload.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const html = require('choo/html');
|
||||
const progress = require('./progress');
|
||||
const { bytes } = require('../utils');
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
const transfer = state.transfer;
|
||||
|
||||
const div = html`
|
||||
<div id="upload-progress" class="fadeIn">
|
||||
<div class="title" id="upload-filename">${state.translate(
|
||||
'uploadingPageProgress',
|
||||
{
|
||||
filename: transfer.file.name,
|
||||
size: bytes(transfer.file.size)
|
||||
}
|
||||
)}</div>
|
||||
<div class="description"></div>
|
||||
${progress(transfer.progressRatio)}
|
||||
<div class="upload">
|
||||
<div class="progress-text">${state.translate(
|
||||
transfer.msg,
|
||||
transfer.sizes
|
||||
)}</div>
|
||||
<button id="cancel-upload" title="${state.translate(
|
||||
'uploadingPageCancel'
|
||||
)}" onclick=${cancel}>${state.translate('uploadingPageCancel')}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
function cancel() {
|
||||
const btn = document.getElementById('cancel-upload');
|
||||
btn.disabled = true;
|
||||
btn.textContent = state.translate('uploadCancelNotification');
|
||||
emit('cancel');
|
||||
}
|
||||
return div;
|
||||
};
|
||||
67
app/templates/uploadPassword.js
Normal file
@@ -0,0 +1,67 @@
|
||||
const html = require('choo/html');
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
const file = state.storage.getFileById(state.params.id);
|
||||
const div = html`
|
||||
<div class="selectPassword">
|
||||
<div id="addPasswordWrapper">
|
||||
<input id="addPassword" type="checkbox" autocomplete="off" onchange=${togglePasswordInput}/>
|
||||
<label for="addPassword">
|
||||
${state.translate('requirePasswordCheckbox')}</label>
|
||||
</div>
|
||||
<form class="setPassword hidden" onsubmit=${setPassword} data-no-csrf>
|
||||
<input id="unlock-input"
|
||||
class="unlock-input input-no-btn"
|
||||
maxlength="64"
|
||||
autocomplete="off"
|
||||
placeholder="${state.translate('unlockInputPlaceholder')}"
|
||||
oninput=${inputChanged}/>
|
||||
<input type="submit"
|
||||
id="unlock-btn"
|
||||
class="btn btn-hidden"
|
||||
value="${state.translate('addPasswordButton')}"/>
|
||||
</form>
|
||||
</div>`;
|
||||
|
||||
function inputChanged() {
|
||||
const input = document.getElementById('unlock-input');
|
||||
const btn = document.getElementById('unlock-btn');
|
||||
if (input.value.length > 0) {
|
||||
btn.classList.remove('btn-hidden');
|
||||
input.classList.remove('input-no-btn');
|
||||
} else {
|
||||
btn.classList.add('btn-hidden');
|
||||
input.classList.add('input-no-btn');
|
||||
}
|
||||
}
|
||||
|
||||
function togglePasswordInput(e) {
|
||||
const unlockInput = document.getElementById('unlock-input');
|
||||
const boxChecked = e.target.checked;
|
||||
document
|
||||
.querySelector('.setPassword')
|
||||
.classList.toggle('hidden', !boxChecked);
|
||||
document
|
||||
.getElementById('copy')
|
||||
.classList.toggle('wait-password', boxChecked);
|
||||
document.getElementById('copy-btn').disabled = boxChecked;
|
||||
if (boxChecked) {
|
||||
unlockInput.focus();
|
||||
} else {
|
||||
unlockInput.value = '';
|
||||
}
|
||||
inputChanged();
|
||||
}
|
||||
|
||||
function setPassword(event) {
|
||||
event.preventDefault();
|
||||
const password = document.getElementById('unlock-input').value;
|
||||
if (password.length > 0) {
|
||||
document.getElementById('copy').classList.remove('wait-password');
|
||||
document.getElementById('copy-btn').disabled = false;
|
||||
emit('password', { password, file });
|
||||
}
|
||||
}
|
||||
|
||||
return div;
|
||||
};
|
||||
75
app/templates/welcome.js
Normal file
@@ -0,0 +1,75 @@
|
||||
const html = require('choo/html');
|
||||
const assets = require('../../common/assets');
|
||||
const fileList = require('./fileList');
|
||||
const { fadeOut } = require('../utils');
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
const div = html`
|
||||
<div id="page-one" class="fadeIn">
|
||||
<div class="title">${state.translate('uploadPageHeader')}</div>
|
||||
<div class="description">
|
||||
<div>${state.translate('uploadPageExplainer')}</div>
|
||||
<a href="https://testpilot.firefox.com/experiments/send"
|
||||
class="link">${state.translate('uploadPageLearnMore')}</a>
|
||||
</div>
|
||||
<div class="upload-window"
|
||||
ondragover=${dragover}
|
||||
ondragleave=${dragleave}>
|
||||
<div id="upload-img">
|
||||
<img src="${assets.get('upload.svg')}"
|
||||
title="${state.translate('uploadSvgAlt')}"/>
|
||||
</div>
|
||||
<div id="upload-text">${state.translate('uploadPageDropMessage')}</div>
|
||||
<span id="file-size-msg">
|
||||
<em>${state.translate('uploadPageSizeMessage')}</em>
|
||||
</span>
|
||||
<input id="file-upload"
|
||||
type="file"
|
||||
name="fileUploaded"
|
||||
onfocus=${onfocus}
|
||||
onblur=${onblur}
|
||||
onchange=${upload} />
|
||||
<label for="file-upload"
|
||||
id="browse"
|
||||
class="btn browse"
|
||||
title="${state.translate('uploadPageBrowseButton1')}">
|
||||
${state.translate('uploadPageBrowseButton1')}</label>
|
||||
</div>
|
||||
${fileList(state, emit)}
|
||||
</div>
|
||||
`;
|
||||
|
||||
function dragover(event) {
|
||||
const div = document.querySelector('.upload-window');
|
||||
div.classList.add('ondrag');
|
||||
}
|
||||
|
||||
function dragleave(event) {
|
||||
const div = document.querySelector('.upload-window');
|
||||
div.classList.remove('ondrag');
|
||||
}
|
||||
|
||||
function onfocus(event) {
|
||||
event.target.classList.add('has-focus');
|
||||
}
|
||||
|
||||
function onblur(event) {
|
||||
event.target.classList.remove('has-focus');
|
||||
}
|
||||
|
||||
async function upload(event) {
|
||||
event.preventDefault();
|
||||
const target = event.target;
|
||||
const file = target.files[0];
|
||||
if (file.size === 0) {
|
||||
return;
|
||||
}
|
||||
await fadeOut('page-one');
|
||||
emit('upload', { file, type: 'click' });
|
||||
}
|
||||
|
||||
if (state.layout) {
|
||||
return state.layout(state, div);
|
||||
}
|
||||
return div;
|
||||
};
|
||||
@@ -1,23 +1,18 @@
|
||||
function arrayToHex(iv) {
|
||||
let hexStr = '';
|
||||
// eslint-disable-next-line prefer-const
|
||||
for (let i in iv) {
|
||||
if (iv[i] < 16) {
|
||||
hexStr += '0' + iv[i].toString(16);
|
||||
} else {
|
||||
hexStr += iv[i].toString(16);
|
||||
}
|
||||
}
|
||||
return hexStr;
|
||||
const b64 = require('base64-js');
|
||||
|
||||
function arrayToB64(array) {
|
||||
return b64
|
||||
.fromByteArray(array)
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=/g, '');
|
||||
}
|
||||
|
||||
function hexToArray(str) {
|
||||
const iv = new Uint8Array(str.length / 2);
|
||||
for (let i = 0; i < str.length; i += 2) {
|
||||
iv[i / 2] = parseInt(str.charAt(i) + str.charAt(i + 1), 16);
|
||||
}
|
||||
|
||||
return iv;
|
||||
function b64ToArray(str) {
|
||||
str = (str + '==='.slice((str.length + 3) % 4))
|
||||
.replace(/-/g, '+')
|
||||
.replace(/_/g, '/');
|
||||
return b64.toByteArray(str);
|
||||
}
|
||||
|
||||
function notify(str) {
|
||||
@@ -35,47 +30,39 @@ function notify(str) {
|
||||
*/
|
||||
}
|
||||
|
||||
function gcmCompliant() {
|
||||
function loadShim(polyfill) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const shim = document.createElement('script');
|
||||
shim.src = polyfill;
|
||||
shim.addEventListener('load', () => resolve(true));
|
||||
shim.addEventListener('error', () => resolve(false));
|
||||
document.head.appendChild(shim);
|
||||
});
|
||||
}
|
||||
|
||||
async function canHasSend(polyfill) {
|
||||
try {
|
||||
return window.crypto.subtle
|
||||
.generateKey(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
length: 128
|
||||
},
|
||||
true,
|
||||
['encrypt', 'decrypt']
|
||||
)
|
||||
.then(key => {
|
||||
return window.crypto.subtle
|
||||
.encrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: window.crypto.getRandomValues(new Uint8Array(12)),
|
||||
additionalData: window.crypto.getRandomValues(new Uint8Array(6)),
|
||||
tagLength: 128
|
||||
},
|
||||
key,
|
||||
new ArrayBuffer(8)
|
||||
)
|
||||
.then(() => {
|
||||
return Promise.resolve();
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
return loadShim();
|
||||
});
|
||||
const key = await window.crypto.subtle.generateKey(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
length: 128
|
||||
},
|
||||
true,
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
|
||||
await window.crypto.subtle.encrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: window.crypto.getRandomValues(new Uint8Array(12)),
|
||||
tagLength: 128
|
||||
},
|
||||
key,
|
||||
new ArrayBuffer(8)
|
||||
);
|
||||
return true;
|
||||
} catch (err) {
|
||||
return loadShim();
|
||||
}
|
||||
function loadShim() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const shim = document.createElement('script');
|
||||
shim.src = '/cryptofill.js';
|
||||
shim.addEventListener('load', resolve);
|
||||
shim.addEventListener('error', reject);
|
||||
document.head.appendChild(shim);
|
||||
});
|
||||
return loadShim(polyfill);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,11 +94,15 @@ function copyToClipboard(str) {
|
||||
const LOCALIZE_NUMBERS = !!(
|
||||
typeof Intl === 'object' &&
|
||||
Intl &&
|
||||
typeof Intl.NumberFormat === 'function'
|
||||
typeof Intl.NumberFormat === 'function' &&
|
||||
typeof navigator === 'object'
|
||||
);
|
||||
|
||||
const UNITS = ['B', 'kB', 'MB', 'GB'];
|
||||
function bytes(num) {
|
||||
if (num < 1) {
|
||||
return '0B';
|
||||
}
|
||||
const exponent = Math.min(Math.floor(Math.log10(num) / 3), UNITS.length - 1);
|
||||
const n = Number(num / Math.pow(1000, exponent));
|
||||
const nStr = LOCALIZE_NUMBERS
|
||||
@@ -134,17 +125,30 @@ function allowedCopy() {
|
||||
return support ? document.queryCommandSupported('copy') : false;
|
||||
}
|
||||
|
||||
function delay(delay = 100) {
|
||||
return new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
|
||||
function fadeOut(id) {
|
||||
const classes = document.getElementById(id).classList;
|
||||
classes.remove('fadeIn');
|
||||
classes.add('fadeOut');
|
||||
return delay(300);
|
||||
}
|
||||
|
||||
const ONE_DAY_IN_MS = 86400000;
|
||||
|
||||
export {
|
||||
module.exports = {
|
||||
fadeOut,
|
||||
delay,
|
||||
allowedCopy,
|
||||
bytes,
|
||||
percent,
|
||||
copyToClipboard,
|
||||
arrayToHex,
|
||||
hexToArray,
|
||||
arrayToB64,
|
||||
b64ToArray,
|
||||
notify,
|
||||
gcmCompliant,
|
||||
canHasSend,
|
||||
isFile,
|
||||
ONE_DAY_IN_MS
|
||||
};
|
||||
1
assets/check-16-blue.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path fill="#0A84FF " d="M6 14a1 1 0 0 1-.707-.293l-3-3a1 1 0 0 1 1.414-1.414l2.157 2.157 6.316-9.023a1 1 0 0 1 1.639 1.146l-7 10a1 1 0 0 1-.732.427A.863.863 0 0 1 6 14z"/></svg>
|
||||
|
After Width: | Height: | Size: 238 B |
|
Before Width: | Height: | Size: 257 B After Width: | Height: | Size: 257 B |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="#4A4A4A" d="M9.414 8l5.293-5.293a1 1 0 0 0-1.414-1.414L8 6.586 2.707 1.293a1 1 0 0 0-1.414 1.414L6.586 8l-5.293 5.293a1 1 0 1 0 1.414 1.414L8 9.414l5.293 5.293a1 1 0 0 0 1.414-1.414z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 16 16"><path fill="#4A4A4A" d="M9.414 8l5.293-5.293a1 1 0 0 0-1.414-1.414L8 6.586 2.707 1.293a1 1 0 0 0-1.414 1.414L6.586 8l-5.293 5.293a1 1 0 1 0 1.414 1.414L8 9.414l5.293 5.293a1 1 0 0 0 1.414-1.414z"/></svg>
|
||||
|
Before Width: | Height: | Size: 286 B After Width: | Height: | Size: 287 B |
|
Before Width: | Height: | Size: 416 B After Width: | Height: | Size: 416 B |
BIN
assets/favicon-120.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
assets/favicon-128.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
assets/favicon-144.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
assets/favicon-152.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
assets/favicon-167.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
assets/favicon-180.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
assets/favicon-195.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
assets/favicon-196.png
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
assets/favicon-228.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
assets/favicon-32.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
assets/favicon-96.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 649 B After Width: | Height: | Size: 649 B |
|
Before Width: | Height: | Size: 239 KiB After Width: | Height: | Size: 239 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.3 KiB |
@@ -1,6 +1,6 @@
|
||||
/*** index.html ***/
|
||||
html {
|
||||
background: url('../../public/resources/send_bg.svg');
|
||||
background: url('./send_bg.svg');
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'segoe ui',
|
||||
'helvetica neue', helvetica, ubuntu, roboto, noto, arial, sans-serif;
|
||||
font-weight: 200;
|
||||
@@ -89,7 +89,7 @@ body {
|
||||
|
||||
.feedback {
|
||||
background-color: #0297f8;
|
||||
background-image: url('../../public/resources/feedback.svg');
|
||||
background-image: url('./feedback.svg');
|
||||
background-position: 2px 4px;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 18px;
|
||||
@@ -137,11 +137,18 @@ body {
|
||||
width: 96%;
|
||||
}
|
||||
|
||||
pre,
|
||||
input,
|
||||
select,
|
||||
textarea,
|
||||
button {
|
||||
font-family: inherit;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
font-weight: 600;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
a {
|
||||
@@ -154,13 +161,43 @@ a {
|
||||
|
||||
/** page-one **/
|
||||
|
||||
.fadeOut {
|
||||
opacity: 0;
|
||||
animation: fadeout 200ms linear;
|
||||
}
|
||||
|
||||
@keyframes fadeout {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.fadeIn {
|
||||
opacity: 1;
|
||||
animation: fadein 200ms linear;
|
||||
}
|
||||
|
||||
@keyframes fadein {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 33px;
|
||||
line-height: 40px;
|
||||
margin: 20px auto;
|
||||
text-align: center;
|
||||
max-width: 520px;
|
||||
font-family: 'SF Pro Display', sans-serif;
|
||||
font-family: 'SF Pro Text', sans-serif;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
@@ -175,7 +212,7 @@ a {
|
||||
}
|
||||
|
||||
.upload-window {
|
||||
border: 1px dashed rgba(0, 148, 251, 0.5);
|
||||
border: 3px dashed rgba(0, 148, 251, 0.5);
|
||||
margin: 0 auto;
|
||||
height: 255px;
|
||||
border-radius: 4px;
|
||||
@@ -189,7 +226,7 @@ a {
|
||||
}
|
||||
|
||||
.upload-window.ondrag {
|
||||
border: 3px dashed rgba(0, 148, 251, 0.5);
|
||||
border: 5px dashed rgba(0, 148, 251, 0.5);
|
||||
margin: 0 auto;
|
||||
height: 251px;
|
||||
transform: scale(1.04);
|
||||
@@ -214,16 +251,16 @@ a {
|
||||
font-size: 22px;
|
||||
color: #737373;
|
||||
margin: 20px 0 10px;
|
||||
font-family: 'SF Pro Display', sans-serif;
|
||||
font-family: 'SF Pro Text', sans-serif;
|
||||
}
|
||||
|
||||
#browse {
|
||||
.browse {
|
||||
background: #0297f8;
|
||||
border-radius: 5px;
|
||||
font-size: 15px;
|
||||
font-size: 20px;
|
||||
color: #fff;
|
||||
min-width: 240px;
|
||||
height: 44px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -231,12 +268,22 @@ a {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
#browse:hover {
|
||||
.browse:hover {
|
||||
background-color: #0287e8;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
display: none;
|
||||
input[type='file'] {
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
input[type='file'].has-focus + #browse,
|
||||
input[type='file']:focus + #browse {
|
||||
background-color: #0287e8;
|
||||
outline: 1px dotted #000;
|
||||
outline: -webkit-focus-ring-color auto 5px;
|
||||
}
|
||||
|
||||
#file-size-msg {
|
||||
@@ -299,13 +346,24 @@ tbody {
|
||||
width: 12%;
|
||||
}
|
||||
|
||||
.overflow-col {
|
||||
text-overflow: ellipsis;
|
||||
max-width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.center-col {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.icon-delete,
|
||||
.icon-copy,
|
||||
.icon-check {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon-copy[disabled="disabled"] {
|
||||
.icon-copy[disabled='disabled'] {
|
||||
pointer-events: none;
|
||||
opacity: 0.3;
|
||||
}
|
||||
@@ -344,7 +402,7 @@ tbody {
|
||||
|
||||
/* Popup arrow */
|
||||
.popup .popuptext::after {
|
||||
content: "";
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -11px;
|
||||
left: 20px;
|
||||
@@ -431,12 +489,8 @@ tbody {
|
||||
}
|
||||
|
||||
.percentage {
|
||||
position: absolute;
|
||||
letter-spacing: -0.78px;
|
||||
font-family: 'Segoe UI', 'SF Pro Text', sans-serif;
|
||||
top: 58px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
@@ -449,7 +503,8 @@ tbody {
|
||||
|
||||
.percent-sign {
|
||||
font-size: 28.8px;
|
||||
color: rgb(104, 104, 104);
|
||||
stroke: none;
|
||||
fill: #686868;
|
||||
}
|
||||
|
||||
.upload {
|
||||
@@ -471,10 +526,18 @@ tbody {
|
||||
|
||||
#cancel-upload {
|
||||
color: #d70022;
|
||||
background: #fff;
|
||||
font-size: 15px;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#cancel-upload:disabled {
|
||||
text-decoration: none;
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
/** share-link **/
|
||||
#share-window {
|
||||
margin: 0 auto;
|
||||
@@ -482,6 +545,8 @@ tbody {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
#share-window-r > div {
|
||||
@@ -492,7 +557,12 @@ tbody {
|
||||
#copy {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
width: 640px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#copy.wait-password #link,
|
||||
#copy.wait-password #copy-btn {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
#copy-text {
|
||||
@@ -509,9 +579,9 @@ tbody {
|
||||
height: 56px;
|
||||
border: 1px solid #0297f8;
|
||||
border-radius: 6px 0 0 6px;
|
||||
font-size: 24px;
|
||||
font-size: 20px;
|
||||
color: #737373;
|
||||
font-family: 'SF Pro Display', sans-serif;
|
||||
font-family: 'SF Pro Text', sans-serif;
|
||||
letter-spacing: 0;
|
||||
line-height: 23px;
|
||||
font-weight: 300;
|
||||
@@ -532,21 +602,22 @@ tbody {
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
height: 60px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#copy-btn:hover {
|
||||
#copy-btn:not(:disabled):hover {
|
||||
background-color: #0287e8;
|
||||
}
|
||||
|
||||
#copy-btn:disabled {
|
||||
#copy-btn.success {
|
||||
background: #05a700;
|
||||
border: 1px solid #05a700;
|
||||
}
|
||||
|
||||
#copy-btn:disabled {
|
||||
cursor: auto;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
#delete-file {
|
||||
@@ -581,6 +652,25 @@ tbody {
|
||||
color: #0287e8;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.selectPassword {
|
||||
padding: 10px 0;
|
||||
align-self: left;
|
||||
max-width: 100%;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.setPassword {
|
||||
align-self: left;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
width: 80%;
|
||||
padding: 10px 5px;
|
||||
}
|
||||
|
||||
/* upload-error */
|
||||
#upload-error {
|
||||
display: flex;
|
||||
@@ -623,7 +713,7 @@ tbody {
|
||||
#update-firefox {
|
||||
margin-bottom: 181px;
|
||||
height: 80px;
|
||||
background: #12bc00;
|
||||
background: #98e02b;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
@@ -709,6 +799,62 @@ tbody {
|
||||
height: 196px;
|
||||
}
|
||||
|
||||
.enterPassword {
|
||||
text-align: left;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.red {
|
||||
color: red;
|
||||
}
|
||||
|
||||
#unlock {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
width: 100%;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.unlock-input {
|
||||
flex: 1;
|
||||
height: 46px;
|
||||
border: 1px solid #0297f8;
|
||||
border-radius: 6px 0 0 6px;
|
||||
font-size: 20px;
|
||||
color: #737373;
|
||||
font-family: 'SF Pro Text', sans-serif;
|
||||
letter-spacing: 0;
|
||||
line-height: 23px;
|
||||
font-weight: 300;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
#unlock-btn {
|
||||
flex: 0 1 165px;
|
||||
background: #0297f8;
|
||||
border-radius: 0 6px 6px 0;
|
||||
border: 1px solid #0297f8;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#unlock-btn:hover {
|
||||
background-color: #0287e8;
|
||||
}
|
||||
|
||||
.btn-hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.input-no-btn {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* footer */
|
||||
.footer {
|
||||
right: 0;
|
||||
@@ -731,7 +877,7 @@ tbody {
|
||||
}
|
||||
|
||||
.legal-links {
|
||||
width: 81vw;
|
||||
max-width: 81vw;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
@@ -773,6 +919,42 @@ tbody {
|
||||
margin-bottom: -5px;
|
||||
}
|
||||
|
||||
#addPasswordWrapper {
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
#addPassword {
|
||||
position: absolute;
|
||||
visibility: collapse;
|
||||
}
|
||||
|
||||
#addPasswordWrapper label {
|
||||
line-height: 20px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
#addPassword:checked + label {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#addPasswordWrapper 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;
|
||||
}
|
||||
|
||||
#addPassword:checked + label::before {
|
||||
background-image: url('./check-16-blue.svg');
|
||||
background-position: 2px 1px;
|
||||
}
|
||||
|
||||
@media (max-device-width: 992px), (max-width: 992px) {
|
||||
.popup .popuptext {
|
||||
left: auto;
|
||||
@@ -843,22 +1025,40 @@ tbody {
|
||||
padding: 5px 5px 5px 20px;
|
||||
}
|
||||
|
||||
#copy {
|
||||
#copy,
|
||||
.setPassword,
|
||||
#unlock {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
#link {
|
||||
.selectPassword {
|
||||
align-self: center;
|
||||
min-width: 95%;
|
||||
}
|
||||
|
||||
#addPasswordWrapper label::before {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
#link,
|
||||
#unlock-input {
|
||||
font-size: 22px;
|
||||
padding: 15px 10px;
|
||||
border-radius: 6px 6px 0 0;
|
||||
}
|
||||
|
||||
#copy-btn {
|
||||
#copy-btn,
|
||||
#unlock-btn {
|
||||
border-radius: 0 0 6px 6px;
|
||||
flex: 0 1 65px;
|
||||
}
|
||||
|
||||
#copy-text {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
th {
|
||||
font-size: 14px;
|
||||
padding: 0 5px;
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 312 KiB After Width: | Height: | Size: 312 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 8.4 KiB |
1
assets/send_logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="30" height="27" viewBox="0 0 30 27" xmlns="http://www.w3.org/2000/svg"><title>send logo</title><g stroke="#3E3D40" fill="none" fill-rule="evenodd" transform="translate(-0.231,0.11948695)"><path d="M22.364 19.989l-2.153-2.103a2.046 2.046 0 0 0-2.665-.151l3.402 3.323a.531.531 0 0 1 0 .766l-2.466 2.408a.563.563 0 0 1-.784 0l-3.398-3.32a1.932 1.932 0 0 0 .188 2.564l2.153 2.103c.788.77 2.066.77 2.855 0l2.868-2.802a1.94 1.94 0 0 0 0-2.788M8.77 14.745a.534.534 0 0 0 0 .766l3.399 3.32a2.05 2.05 0 0 1-2.625-.184l-2.153-2.102a1.94 1.94 0 0 1 0-2.79l2.869-2.801a2.052 2.052 0 0 1 2.854 0l2.153 2.103c.73.713.775 1.83.154 2.603l-3.401-3.323a.565.565 0 0 0-.784 0L8.77 14.745zm9.464 5.682a.777.777 0 0 1 0 1.118.822.822 0 0 1-1.144 0l-5.6-5.47a.777.777 0 0 1 0-1.118.822.822 0 0 1 1.144 0l5.6 5.47z" stroke-width=".618" fill="#3E3D40"/><path d="M6.065 20.606c-2.913-1.586-3.988-3.656-3.988-6.468 0-2.81 2.265-6.425 5.786-6.289.1.004.55-.006.649 0 .895-3.27 2.508-6.353 6.898-6.353 4.557 0 7.336 3.716 6.75 7.785.08-.005 1.232.17 1.31.186 3.096.644 4.915 3.275 4.915 5.18 0 1.905-.107 3.029-2.023 4.947" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 873 B After Width: | Height: | Size: 873 B |
|
Before Width: | Height: | Size: 336 B After Width: | Height: | Size: 336 B |
10
browserconfig.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square70x70logo src=“favicon-76.png”/>
|
||||
<square150x150logo src="favicon-228.png"/>
|
||||
<TileColor>#0297F8</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
||||
38
build/fluent_loader.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const { MessageContext } = require('fluent');
|
||||
|
||||
function toJSON(map) {
|
||||
return JSON.stringify(Array.from(map));
|
||||
}
|
||||
|
||||
module.exports = function(source) {
|
||||
const localeExp = this.options.locale || /([^/]+)\/[^/]+\.ftl$/;
|
||||
const result = localeExp.exec(this.resourcePath);
|
||||
const locale = result && result[1];
|
||||
// pre-parse the ftl
|
||||
const context = new MessageContext(locale);
|
||||
context.addMessages(source);
|
||||
if (!locale) {
|
||||
throw new Error(`couldn't find locale in: ${this.resourcePath}`);
|
||||
}
|
||||
return `
|
||||
module.exports = \`
|
||||
if (typeof window === 'undefined') {
|
||||
var fluent = require('fluent');
|
||||
}
|
||||
var ctx = new fluent.MessageContext('${locale}', {useIsolating: false});
|
||||
ctx._messages = new Map(${toJSON(context._messages)});
|
||||
function translate(id, data) {
|
||||
var msg = ctx.getMessage(id);
|
||||
if (typeof(msg) !== 'string' && !msg.val && msg.attrs) {
|
||||
msg = msg.attrs.title || msg.attrs.alt
|
||||
}
|
||||
return ctx.format(msg, data);
|
||||
}
|
||||
if (typeof window === 'undefined') {
|
||||
module.exports = translate;
|
||||
}
|
||||
else {
|
||||
window.translate = translate;
|
||||
}
|
||||
\``;
|
||||
};
|
||||
19
build/generate_asset_map.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function kv(f) {
|
||||
return `"${f}": require('../assets/${f}')`;
|
||||
}
|
||||
|
||||
module.exports = function() {
|
||||
const files = fs.readdirSync(path.join(__dirname, '..', 'assets'));
|
||||
const code = `module.exports = {
|
||||
"package.json": require('../package.json'),
|
||||
${files.map(kv).join(',\n')}
|
||||
};`;
|
||||
return {
|
||||
code,
|
||||
dependencies: files.map(f => require.resolve('../assets/' + f)),
|
||||
cacheable: false
|
||||
};
|
||||
};
|
||||
22
build/generate_l10n_map.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function kv(d) {
|
||||
return `"${d}": require('../public/locales/${d}/send.ftl')`;
|
||||
}
|
||||
|
||||
module.exports = function() {
|
||||
const dirs = fs.readdirSync(path.join(__dirname, '..', 'public', 'locales'));
|
||||
const code = `
|
||||
module.exports = {
|
||||
translate: function (id, data) { return window.translate(id, data) },
|
||||
${dirs.map(kv).join(',\n')}
|
||||
};`;
|
||||
return {
|
||||
code,
|
||||
dependencies: dirs.map(d =>
|
||||
require.resolve(`../public/locales/${d}/send.ftl`)
|
||||
),
|
||||
cacheable: false
|
||||
};
|
||||
};
|
||||
11
build/package_json_loader.js
Normal file
@@ -0,0 +1,11 @@
|
||||
const commit = require('git-rev-sync').short();
|
||||
|
||||
module.exports = function(source) {
|
||||
const pkg = JSON.parse(source);
|
||||
const version = {
|
||||
commit,
|
||||
source: pkg.homepage,
|
||||
version: process.env.CIRCLE_TAG || `v${pkg.version}`
|
||||
};
|
||||
return `module.exports = '${JSON.stringify(version)}'`;
|
||||
};
|
||||
32
common/assets.js
Normal file
@@ -0,0 +1,32 @@
|
||||
const genmap = require('../build/generate_asset_map');
|
||||
const isServer = typeof genmap === 'function';
|
||||
const prefix = isServer ? '/' : '';
|
||||
let manifest = {};
|
||||
try {
|
||||
//eslint-disable-next-line node/no-missing-require
|
||||
manifest = require('../dist/manifest.json');
|
||||
} catch (e) {
|
||||
// use middleware
|
||||
}
|
||||
|
||||
const assets = isServer ? manifest : genmap;
|
||||
|
||||
function getAsset(name) {
|
||||
return prefix + assets[name];
|
||||
}
|
||||
|
||||
const instance = {
|
||||
get: getAsset,
|
||||
setMiddleware: function(middleware) {
|
||||
if (middleware) {
|
||||
instance.get = function getAssetWithMiddleware(name) {
|
||||
const f = middleware.fileSystem.readFileSync(
|
||||
middleware.getFilenameFromUrl('/manifest.json')
|
||||
);
|
||||
return prefix + JSON.parse(f)[name];
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = instance;
|
||||
51
common/locales.js
Normal file
@@ -0,0 +1,51 @@
|
||||
const gen = require('../build/generate_l10n_map');
|
||||
|
||||
const isServer = typeof gen === 'function';
|
||||
const prefix = isServer ? '/' : '';
|
||||
let manifest = {};
|
||||
try {
|
||||
//eslint-disable-next-line node/no-missing-require
|
||||
manifest = require('../dist/manifest.json');
|
||||
} catch (e) {
|
||||
// use middleware
|
||||
}
|
||||
|
||||
const locales = isServer ? manifest : gen;
|
||||
|
||||
function getLocale(name) {
|
||||
return prefix + locales[`public/locales/${name}/send.ftl`];
|
||||
}
|
||||
|
||||
function serverTranslator(name) {
|
||||
return require(`../dist/${locales[`public/locales/${name}/send.ftl`]}`);
|
||||
}
|
||||
|
||||
function browserTranslator() {
|
||||
return locales.translate;
|
||||
}
|
||||
|
||||
const translator = isServer ? serverTranslator : browserTranslator;
|
||||
|
||||
const instance = {
|
||||
get: getLocale,
|
||||
getTranslator: translator,
|
||||
setMiddleware: function(middleware) {
|
||||
if (middleware) {
|
||||
const _eval = require('require-from-string');
|
||||
instance.get = function getLocaleWithMiddleware(name) {
|
||||
const f = middleware.fileSystem.readFileSync(
|
||||
middleware.getFilenameFromUrl('/manifest.json')
|
||||
);
|
||||
return prefix + JSON.parse(f)[`public/locales/${name}/send.ftl`];
|
||||
};
|
||||
instance.getTranslator = function(name) {
|
||||
const f = middleware.fileSystem.readFileSync(
|
||||
middleware.getFilenameFromUrl(instance.get(name))
|
||||
);
|
||||
return _eval(f.toString());
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = instance;
|
||||
@@ -1,3 +1,14 @@
|
||||
## Setup
|
||||
|
||||
Before building the Docker image, you must build the production assets:
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
Then you can run either `docker build` or `docker-compose up`.
|
||||
|
||||
|
||||
## Environment variables:
|
||||
|
||||
| Name | Description
|
||||
|
||||
@@ -67,6 +67,14 @@ Triggered whenever a user stops uploading a file. Includes:
|
||||
- `cd2`
|
||||
- `cd6`
|
||||
|
||||
#### `password-added`
|
||||
Triggered whenever a password is added to a file. Includes:
|
||||
|
||||
- `cm1`
|
||||
- `cm5`
|
||||
- `cm6`
|
||||
- `cm7`
|
||||
|
||||
#### `download-started`
|
||||
Triggered whenever a user begins downloading a file. Includes:
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import Raven from 'raven-js';
|
||||
import { unsupported } from './metrics';
|
||||
|
||||
if (navigator.doNotTrack !== '1' && window.RAVEN_CONFIG) {
|
||||
Raven.config(window.SENTRY_ID, window.RAVEN_CONFIG).install();
|
||||
}
|
||||
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
if (
|
||||
ua.indexOf('firefox') > -1 &&
|
||||
parseInt(ua.match(/firefox\/*([^\n\r]*)\./)[1], 10) <= 49
|
||||
) {
|
||||
unsupported({
|
||||
err: new Error('Firefox is outdated.')
|
||||
}).then(() => {
|
||||
location.replace('/unsupported/outdated');
|
||||
});
|
||||
}
|
||||
|
||||
export { Raven };
|
||||
@@ -1,119 +0,0 @@
|
||||
import { Raven } from './common';
|
||||
import FileReceiver from './fileReceiver';
|
||||
import { bytes, notify, gcmCompliant } from './utils';
|
||||
import Storage from './storage';
|
||||
import * as links from './links';
|
||||
import * as metrics from './metrics';
|
||||
import * as progress from './progress';
|
||||
|
||||
const storage = new Storage();
|
||||
function onUnload(size) {
|
||||
metrics.cancelledDownload({ size });
|
||||
}
|
||||
|
||||
async function download() {
|
||||
const downloadBtn = document.getElementById('download-btn');
|
||||
const downloadPanel = document.getElementById('download-page-one');
|
||||
const progressPanel = document.getElementById('download-progress');
|
||||
const file = document.getElementById('dl-file');
|
||||
const size = Number(file.getAttribute('data-size'));
|
||||
const ttl = Number(file.getAttribute('data-ttl'));
|
||||
const unloadHandler = onUnload.bind(null, size);
|
||||
const startTime = Date.now();
|
||||
const fileReceiver = new FileReceiver(
|
||||
'/assets' + location.pathname.slice(0, -1),
|
||||
location.hash.slice(1)
|
||||
);
|
||||
|
||||
downloadBtn.disabled = true;
|
||||
downloadPanel.hidden = true;
|
||||
progressPanel.hidden = false;
|
||||
metrics.startedDownload({ size, ttl });
|
||||
links.setOpenInNewTab(true);
|
||||
window.addEventListener('unload', unloadHandler);
|
||||
|
||||
fileReceiver.on('progress', data => {
|
||||
progress.setProgress({ complete: data[0], total: data[1] });
|
||||
});
|
||||
|
||||
let downloadEnd;
|
||||
fileReceiver.on('decrypting', () => {
|
||||
downloadEnd = Date.now();
|
||||
window.removeEventListener('unload', unloadHandler);
|
||||
fileReceiver.removeAllListeners('progress');
|
||||
document.l10n.formatValue('decryptingFile').then(progress.setText);
|
||||
});
|
||||
|
||||
try {
|
||||
const file = await fileReceiver.download();
|
||||
const endTime = Date.now();
|
||||
const time = endTime - startTime;
|
||||
const downloadTime = endTime - downloadEnd;
|
||||
const speed = size / (downloadTime / 1000);
|
||||
|
||||
links.setOpenInNewTab(false);
|
||||
storage.totalDownloads += 1;
|
||||
metrics.completedDownload({ size, time, speed });
|
||||
progress.setText(' ');
|
||||
document.l10n
|
||||
.formatValues('downloadNotification', 'downloadFinish')
|
||||
.then(translated => {
|
||||
notify(translated[0]);
|
||||
document.getElementById('dl-title').textContent = translated[1];
|
||||
document.querySelector('#download-progress .description').textContent =
|
||||
' ';
|
||||
});
|
||||
const dataView = new DataView(file.plaintext);
|
||||
const blob = new Blob([dataView], { type: file.type });
|
||||
const downloadUrl = URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = downloadUrl;
|
||||
if (window.navigator.msSaveBlob) {
|
||||
window.navigator.msSaveBlob(blob, file.name);
|
||||
return;
|
||||
}
|
||||
a.download = file.name;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
URL.revokeObjectURL(downloadUrl);
|
||||
} catch (err) {
|
||||
metrics.stoppedDownload({ size, err });
|
||||
|
||||
if (err.message === 'notfound') {
|
||||
location.reload();
|
||||
} else {
|
||||
progressPanel.hidden = true;
|
||||
downloadPanel.hidden = true;
|
||||
document.getElementById('upload-error').hidden = false;
|
||||
}
|
||||
Raven.captureException(err);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const file = document.getElementById('dl-file');
|
||||
const filename = file.getAttribute('data-filename');
|
||||
const b = Number(file.getAttribute('data-size'));
|
||||
const size = bytes(b);
|
||||
document.l10n.formatValue('downloadFileSize', { size }).then(str => {
|
||||
document.getElementById('dl-filesize').textContent = str;
|
||||
});
|
||||
document.l10n
|
||||
.formatValue('downloadingPageProgress', { filename, size })
|
||||
.then(str => {
|
||||
document.getElementById('dl-title').textContent = str;
|
||||
});
|
||||
|
||||
gcmCompliant()
|
||||
.then(() => {
|
||||
document
|
||||
.getElementById('download-btn')
|
||||
.addEventListener('click', download);
|
||||
})
|
||||
.catch(err => {
|
||||
metrics.unsupported({ err }).then(() => {
|
||||
location.replace('/unsupported/gcm');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,163 +0,0 @@
|
||||
import FileSender from './fileSender';
|
||||
import Storage from './storage';
|
||||
import * as metrics from './metrics';
|
||||
import { allowedCopy, copyToClipboard, ONE_DAY_IN_MS } from './utils';
|
||||
import bel from 'bel';
|
||||
import copyImg from '../../public/resources/copy-16.svg';
|
||||
import closeImg from '../../public/resources/close-16.svg';
|
||||
|
||||
const HOUR = 1000 * 60 * 60;
|
||||
const storage = new Storage();
|
||||
let fileList = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
fileList = document.getElementById('file-list');
|
||||
toggleHeader();
|
||||
Promise.all(
|
||||
storage.files.map(file => {
|
||||
const id = file.fileId;
|
||||
return checkExistence(id).then(exists => {
|
||||
if (exists) {
|
||||
addFile(storage.getFileById(id));
|
||||
} else {
|
||||
storage.remove(id);
|
||||
}
|
||||
});
|
||||
})
|
||||
)
|
||||
.catch(err => console.error(err))
|
||||
.then(toggleHeader);
|
||||
});
|
||||
|
||||
function toggleHeader() {
|
||||
fileList.hidden = storage.files.length === 0;
|
||||
}
|
||||
|
||||
function timeLeft(milliseconds) {
|
||||
const minutes = Math.floor(milliseconds / 1000 / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const seconds = Math.floor(milliseconds / 1000 % 60);
|
||||
if (hours >= 1) {
|
||||
return `${hours}h ${minutes % 60}m`;
|
||||
} else if (hours === 0) {
|
||||
return `${minutes}m ${seconds}s`;
|
||||
}
|
||||
return 'Expired';
|
||||
}
|
||||
|
||||
function addFile(file) {
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
file.creationDate = new Date(file.creationDate);
|
||||
const url = `${file.url}#${file.secretKey}`;
|
||||
const future = new Date();
|
||||
future.setTime(file.creationDate.getTime() + file.expiry);
|
||||
const countdown = future.getTime() - Date.now();
|
||||
|
||||
const row = bel`
|
||||
<tr>
|
||||
<td>${file.name}</td>
|
||||
<td>
|
||||
<span class="icon-docs" data-l10n-id="copyUrlHover"></span>
|
||||
<img onclick=${copyClick} src="${copyImg}" class="icon-copy" data-l10n-id="copyUrlHover">
|
||||
<span data-l10n-id="copiedUrl" class="text-copied" hidden="true"></span>
|
||||
</td>
|
||||
<td>${timeLeft(countdown)}</td>
|
||||
<td>
|
||||
<span class="icon-cancel-1" data-l10n-id="deleteButtonHover" title="Delete"></span>
|
||||
<img onclick=${showPopup} src="${closeImg}" class="icon-delete" data-l10n-id="deleteButtonHover" title="Delete">
|
||||
<div class="popup">
|
||||
<div class="popuptext" onclick=${stopProp} onblur=${cancel} tabindex="-1">
|
||||
<div class="popup-message" data-l10n-id="deletePopupText"></div>
|
||||
<div class="popup-action">
|
||||
<span class="popup-no" onclick=${cancel} data-l10n-id="deletePopupCancel"></span>
|
||||
<span class="popup-yes" onclick=${deleteFile} data-l10n-id="deletePopupYes"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
const popup = row.querySelector('.popuptext');
|
||||
const timeCol = row.querySelectorAll('td')[2];
|
||||
if (!allowedCopy()) {
|
||||
row.querySelector('.icon-copy').disabled = true;
|
||||
}
|
||||
|
||||
fileList.querySelector('tbody').appendChild(row);
|
||||
toggleHeader();
|
||||
poll();
|
||||
|
||||
function copyClick(e) {
|
||||
metrics.copiedLink({ location: 'upload-list' });
|
||||
copyToClipboard(url);
|
||||
const icon = e.target;
|
||||
const text = e.target.nextSibling;
|
||||
icon.hidden = true;
|
||||
text.hidden = false;
|
||||
setTimeout(() => {
|
||||
icon.hidden = false;
|
||||
text.hidden = true;
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function poll() {
|
||||
const countdown = future.getTime() - Date.now();
|
||||
if (countdown <= 0) {
|
||||
storage.remove(file.fileId);
|
||||
row.parentNode.removeChild(row);
|
||||
toggleHeader();
|
||||
}
|
||||
timeCol.textContent = timeLeft(countdown);
|
||||
setTimeout(poll, countdown >= HOUR ? 60000 : 1000);
|
||||
}
|
||||
|
||||
function deleteFile() {
|
||||
FileSender.delete(file.fileId, file.deleteToken);
|
||||
const ttl = ONE_DAY_IN_MS - (Date.now() - file.creationDate.getTime());
|
||||
metrics.deletedUpload({
|
||||
size: file.size,
|
||||
time: file.totalTime,
|
||||
speed: file.uploadSpeed,
|
||||
type: file.typeOfUpload,
|
||||
location: 'upload-list',
|
||||
ttl
|
||||
});
|
||||
row.parentNode.removeChild(row);
|
||||
storage.remove(file.fileId);
|
||||
toggleHeader();
|
||||
}
|
||||
|
||||
function showPopup() {
|
||||
popup.classList.add('show');
|
||||
popup.focus();
|
||||
}
|
||||
|
||||
function cancel(e) {
|
||||
e.stopPropagation();
|
||||
popup.classList.remove('show');
|
||||
}
|
||||
|
||||
function stopProp(e) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
async function checkExistence(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
|
||||
resolve(xhr.status === 200);
|
||||
}
|
||||
};
|
||||
xhr.onerror = reject;
|
||||
xhr.ontimeout = reject;
|
||||
xhr.open('get', '/exists/' + id);
|
||||
xhr.timeout = 2000;
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
|
||||
export { addFile };
|
||||
@@ -1,81 +0,0 @@
|
||||
import EventEmitter from 'events';
|
||||
import { hexToArray } from './utils';
|
||||
|
||||
export default class FileReceiver extends EventEmitter {
|
||||
constructor(url, k) {
|
||||
super();
|
||||
this.key = window.crypto.subtle.importKey(
|
||||
'jwk',
|
||||
{
|
||||
k,
|
||||
kty: 'oct',
|
||||
alg: 'A128GCM',
|
||||
ext: true
|
||||
},
|
||||
{
|
||||
name: 'AES-GCM'
|
||||
},
|
||||
false,
|
||||
['decrypt']
|
||||
);
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
downloadFile() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.onprogress = event => {
|
||||
if (event.lengthComputable && event.target.status !== 404) {
|
||||
this.emit('progress', [event.loaded, event.total]);
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onload = function(event) {
|
||||
if (xhr.status === 404) {
|
||||
reject(new Error('notfound'));
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = new Blob([this.response]);
|
||||
const type = xhr.getResponseHeader('Content-Type');
|
||||
const meta = JSON.parse(xhr.getResponseHeader('X-File-Metadata'));
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = function() {
|
||||
resolve({
|
||||
data: this.result,
|
||||
name: meta.filename,
|
||||
type,
|
||||
iv: meta.id
|
||||
});
|
||||
};
|
||||
|
||||
fileReader.readAsArrayBuffer(blob);
|
||||
};
|
||||
|
||||
xhr.open('get', this.url);
|
||||
xhr.responseType = 'blob';
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
|
||||
async download() {
|
||||
const key = await this.key;
|
||||
const file = await this.downloadFile();
|
||||
this.emit('decrypting');
|
||||
const plaintext = await window.crypto.subtle.decrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: hexToArray(file.iv),
|
||||
tagLength: 128
|
||||
},
|
||||
key,
|
||||
file.data
|
||||
);
|
||||
return {
|
||||
plaintext,
|
||||
name: decodeURIComponent(file.name),
|
||||
type: file.type
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
import EventEmitter from 'events';
|
||||
import { arrayToHex } from './utils';
|
||||
|
||||
export default class FileSender extends EventEmitter {
|
||||
constructor(file) {
|
||||
super();
|
||||
this.file = file;
|
||||
this.iv = window.crypto.getRandomValues(new Uint8Array(12));
|
||||
this.uploadXHR = new XMLHttpRequest();
|
||||
this.key = window.crypto.subtle.generateKey(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
length: 128
|
||||
},
|
||||
true,
|
||||
['encrypt']
|
||||
);
|
||||
}
|
||||
|
||||
static delete(fileId, token) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!fileId || !token) {
|
||||
return reject();
|
||||
}
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('post', '/delete/' + fileId, true);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
xhr.send(JSON.stringify({ delete_token: token }));
|
||||
});
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.uploadXHR.abort();
|
||||
}
|
||||
|
||||
readFile() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsArrayBuffer(this.file);
|
||||
reader.onload = function(event) {
|
||||
const plaintext = new Uint8Array(this.result);
|
||||
resolve(plaintext);
|
||||
};
|
||||
reader.onerror = function(err) {
|
||||
reject(err);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
uploadFile(encrypted, keydata) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const file = this.file;
|
||||
const fileId = arrayToHex(this.iv);
|
||||
const dataView = new DataView(encrypted);
|
||||
const blob = new Blob([dataView], { type: file.type });
|
||||
const fd = new FormData();
|
||||
fd.append('data', blob, file.name);
|
||||
|
||||
const xhr = this.uploadXHR;
|
||||
|
||||
xhr.upload.addEventListener('progress', e => {
|
||||
if (e.lengthComputable) {
|
||||
this.emit('progress', [e.loaded, e.total]);
|
||||
}
|
||||
});
|
||||
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
if (xhr.status === 200) {
|
||||
const responseObj = JSON.parse(xhr.responseText);
|
||||
return resolve({
|
||||
url: responseObj.url,
|
||||
fileId: responseObj.id,
|
||||
secretKey: keydata.k,
|
||||
deleteToken: responseObj.delete
|
||||
});
|
||||
}
|
||||
reject(xhr.status);
|
||||
}
|
||||
};
|
||||
|
||||
xhr.open('post', '/upload', true);
|
||||
xhr.setRequestHeader(
|
||||
'X-File-Metadata',
|
||||
JSON.stringify({
|
||||
id: fileId,
|
||||
filename: encodeURIComponent(file.name)
|
||||
})
|
||||
);
|
||||
xhr.send(fd);
|
||||
});
|
||||
}
|
||||
|
||||
async upload() {
|
||||
this.emit('loading');
|
||||
const key = await this.key;
|
||||
const plaintext = await this.readFile();
|
||||
this.emit('encrypting');
|
||||
const encrypted = await window.crypto.subtle.encrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: this.iv,
|
||||
tagLength: 128
|
||||
},
|
||||
key,
|
||||
plaintext
|
||||
);
|
||||
const keydata = await window.crypto.subtle.exportKey('jwk', key);
|
||||
return this.uploadFile(encrypted, keydata);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
let links = [];
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
links = Array.from(document.querySelectorAll('a:not([target])'));
|
||||
});
|
||||
|
||||
function setOpenInNewTab(bool) {
|
||||
if (bool === false) {
|
||||
links.forEach(l => {
|
||||
l.removeAttribute('target');
|
||||
l.removeAttribute('rel');
|
||||
});
|
||||
} else {
|
||||
links.forEach(l => {
|
||||
l.setAttribute('target', '_blank');
|
||||
l.setAttribute('rel', 'noopener noreferrer');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { setOpenInNewTab };
|
||||
@@ -1,47 +0,0 @@
|
||||
import { bytes, percent } from './utils';
|
||||
|
||||
let percentText = null;
|
||||
let text = null;
|
||||
let title = null;
|
||||
let bar = null;
|
||||
let updateTitle = false;
|
||||
|
||||
const radius = 73;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
percentText = document.querySelector('.percent-number');
|
||||
text = document.querySelector('.progress-text');
|
||||
bar = document.getElementById('bar');
|
||||
title = document.querySelector('title');
|
||||
});
|
||||
|
||||
document.addEventListener('blur', function() {
|
||||
updateTitle = true;
|
||||
});
|
||||
|
||||
document.addEventListener('focus', function() {
|
||||
updateTitle = false;
|
||||
return title && (title.textContent = 'Firefox Send');
|
||||
});
|
||||
|
||||
function setProgress(params) {
|
||||
const ratio = params.complete / params.total;
|
||||
bar.setAttribute('stroke-dashoffset', (1 - ratio) * circumference);
|
||||
percentText.textContent = Math.floor(ratio * 100);
|
||||
if (updateTitle) {
|
||||
title.textContent = percent(ratio);
|
||||
}
|
||||
document.l10n
|
||||
.formatValue('fileSizeProgress', {
|
||||
partialSize: bytes(params.complete),
|
||||
totalSize: bytes(params.total)
|
||||
})
|
||||
.then(setText);
|
||||
}
|
||||
|
||||
function setText(str) {
|
||||
text.textContent = str;
|
||||
}
|
||||
|
||||
export { setProgress, setText };
|
||||
@@ -1,250 +0,0 @@
|
||||
/* global MAXFILESIZE EXPIRE_SECONDS */
|
||||
import { Raven } from './common';
|
||||
import FileSender from './fileSender';
|
||||
import {
|
||||
allowedCopy,
|
||||
bytes,
|
||||
copyToClipboard,
|
||||
notify,
|
||||
gcmCompliant,
|
||||
ONE_DAY_IN_MS
|
||||
} from './utils';
|
||||
import Storage from './storage';
|
||||
import * as metrics from './metrics';
|
||||
import * as progress from './progress';
|
||||
import * as fileList from './fileList';
|
||||
|
||||
const storage = new Storage();
|
||||
|
||||
async function upload(event) {
|
||||
event.preventDefault();
|
||||
const pageOne = document.getElementById('page-one');
|
||||
const link = document.getElementById('link');
|
||||
const uploadWindow = document.querySelector('.upload-window');
|
||||
const uploadError = document.getElementById('upload-error');
|
||||
const uploadProgress = document.getElementById('upload-progress');
|
||||
const clickOrDrop = event.type === 'drop' ? 'drop' : 'click';
|
||||
|
||||
// don't allow upload if not on upload page
|
||||
if (pageOne.hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
storage.totalUploads += 1;
|
||||
|
||||
let file = '';
|
||||
if (clickOrDrop === 'drop') {
|
||||
if (!event.dataTransfer.files[0]) {
|
||||
uploadWindow.classList.remove('ondrag');
|
||||
return;
|
||||
}
|
||||
if (
|
||||
event.dataTransfer.files.length > 1 ||
|
||||
event.dataTransfer.files[0].size === 0
|
||||
) {
|
||||
uploadWindow.classList.remove('ondrag');
|
||||
document.l10n.formatValue('uploadPageMultipleFilesAlert').then(str => {
|
||||
alert(str);
|
||||
});
|
||||
return;
|
||||
}
|
||||
file = event.dataTransfer.files[0];
|
||||
} else {
|
||||
file = event.target.files[0];
|
||||
}
|
||||
|
||||
if (file.size > MAXFILESIZE) {
|
||||
return document.l10n
|
||||
.formatValue('fileTooBig', { size: bytes(MAXFILESIZE) })
|
||||
.then(alert);
|
||||
}
|
||||
|
||||
pageOne.hidden = true;
|
||||
uploadError.hidden = true;
|
||||
uploadProgress.hidden = false;
|
||||
document.l10n
|
||||
.formatValue('uploadingPageProgress', {
|
||||
size: bytes(file.size),
|
||||
filename: file.name
|
||||
})
|
||||
.then(str => {
|
||||
document.getElementById('upload-filename').textContent = str;
|
||||
});
|
||||
document.l10n.formatValue('importingFile').then(progress.setText);
|
||||
//don't allow drag and drop when not on page-one
|
||||
document.body.removeEventListener('drop', upload);
|
||||
|
||||
const fileSender = new FileSender(file);
|
||||
document.getElementById('cancel-upload').addEventListener('click', () => {
|
||||
fileSender.cancel();
|
||||
metrics.cancelledUpload({
|
||||
size: file.size,
|
||||
type: clickOrDrop
|
||||
});
|
||||
location.reload();
|
||||
});
|
||||
|
||||
let uploadStart;
|
||||
fileSender.on('progress', data => {
|
||||
uploadStart = uploadStart || Date.now();
|
||||
progress.setProgress({
|
||||
complete: data[0],
|
||||
total: data[1]
|
||||
});
|
||||
});
|
||||
|
||||
fileSender.on('encrypting', () => {
|
||||
document.l10n.formatValue('encryptingFile').then(progress.setText);
|
||||
});
|
||||
|
||||
let t;
|
||||
const startTime = Date.now();
|
||||
metrics.startedUpload({
|
||||
size: file.size,
|
||||
type: clickOrDrop
|
||||
});
|
||||
// For large files we need to give the ui a tick to breathe and update
|
||||
// before we kick off the FileSender
|
||||
setTimeout(() => {
|
||||
fileSender
|
||||
.upload()
|
||||
.then(info => {
|
||||
const endTime = Date.now();
|
||||
const time = endTime - startTime;
|
||||
const uploadTime = endTime - uploadStart;
|
||||
const speed = file.size / (uploadTime / 1000);
|
||||
const expiration = EXPIRE_SECONDS * 1000;
|
||||
|
||||
link.setAttribute('value', `${info.url}#${info.secretKey}`);
|
||||
|
||||
metrics.completedUpload({
|
||||
size: file.size,
|
||||
time,
|
||||
speed,
|
||||
type: clickOrDrop
|
||||
});
|
||||
|
||||
const fileData = {
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
fileId: info.fileId,
|
||||
url: info.url,
|
||||
secretKey: info.secretKey,
|
||||
deleteToken: info.deleteToken,
|
||||
creationDate: new Date(),
|
||||
expiry: expiration,
|
||||
totalTime: time,
|
||||
typeOfUpload: clickOrDrop,
|
||||
uploadSpeed: speed
|
||||
};
|
||||
|
||||
document.getElementById('delete-file').addEventListener('click', () => {
|
||||
FileSender.delete(fileData.fileId, fileData.deleteToken).then(() => {
|
||||
const ttl =
|
||||
ONE_DAY_IN_MS - (Date.now() - fileData.creationDate.getTime());
|
||||
metrics
|
||||
.deletedUpload({
|
||||
size: fileData.size,
|
||||
time: fileData.totalTime,
|
||||
speed: fileData.uploadSpeed,
|
||||
type: fileData.typeOfUpload,
|
||||
location: 'success-screen',
|
||||
ttl
|
||||
})
|
||||
.then(() => {
|
||||
storage.remove(fileData.fileId);
|
||||
location.reload();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
storage.addFile(info.fileId, fileData);
|
||||
|
||||
pageOne.hidden = true;
|
||||
uploadProgress.hidden = true;
|
||||
uploadError.hidden = true;
|
||||
document.getElementById('share-link').hidden = false;
|
||||
|
||||
fileList.addFile(fileData);
|
||||
document.l10n.formatValue('notifyUploadDone').then(str => {
|
||||
notify(str);
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
// err is 0 when coming from a cancel upload event
|
||||
if (err === 0) {
|
||||
return;
|
||||
}
|
||||
// only show error page when the error is anything other than user cancelling the upload
|
||||
Raven.captureException(err);
|
||||
pageOne.hidden = true;
|
||||
uploadProgress.hidden = true;
|
||||
uploadError.hidden = false;
|
||||
window.clearTimeout(t);
|
||||
|
||||
metrics.stoppedUpload({
|
||||
size: file.size,
|
||||
type: clickOrDrop,
|
||||
err
|
||||
});
|
||||
});
|
||||
}, 10);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
gcmCompliant()
|
||||
.then(function() {
|
||||
const pageOne = document.getElementById('page-one');
|
||||
const copyBtn = document.getElementById('copy-btn');
|
||||
const link = document.getElementById('link');
|
||||
const uploadWindow = document.querySelector('.upload-window');
|
||||
|
||||
pageOne.hidden = false;
|
||||
document.getElementById('file-upload').addEventListener('change', upload);
|
||||
|
||||
document.body.addEventListener('dragover', allowDrop);
|
||||
document.body.addEventListener('drop', upload);
|
||||
|
||||
// reset copy button
|
||||
copyBtn.disabled = !allowedCopy();
|
||||
copyBtn.setAttribute('data-l10n-id', 'copyUrlFormButton');
|
||||
|
||||
link.disabled = false;
|
||||
|
||||
// copy link to clipboard
|
||||
copyBtn.addEventListener('click', () => {
|
||||
if (allowedCopy() && copyToClipboard(link.getAttribute('value'))) {
|
||||
metrics.copiedLink({ location: 'success-screen' });
|
||||
|
||||
//disable button for 3s
|
||||
copyBtn.disabled = true;
|
||||
link.disabled = true;
|
||||
copyBtn.innerHtml =
|
||||
'<img src="/resources/check-16.svg" class="icon-check"></img>';
|
||||
setTimeout(() => {
|
||||
copyBtn.disabled = !allowedCopy();
|
||||
copyBtn.setAttribute('data-l10n-id', 'copyUrlFormButton');
|
||||
link.disabled = false;
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
|
||||
uploadWindow.addEventListener('dragover', () =>
|
||||
uploadWindow.classList.add('ondrag')
|
||||
);
|
||||
uploadWindow.addEventListener('dragleave', () =>
|
||||
uploadWindow.classList.remove('ondrag')
|
||||
);
|
||||
|
||||
// on file upload by browse or drag & drop
|
||||
|
||||
function allowDrop(ev) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
metrics.unsupported({ err }).then(() => {
|
||||
location.replace('/unsupported/gcm');
|
||||
});
|
||||
});
|
||||
});
|
||||
6423
package-lock.json
generated
204
package.json
@@ -1,87 +1,130 @@
|
||||
{
|
||||
"name": "firefox-send",
|
||||
"description": "File Sharing Experiment",
|
||||
"version": "1.1.1",
|
||||
"version": "2.0.0",
|
||||
"author": "Mozilla (https://mozilla.org)",
|
||||
"dependencies": {
|
||||
"aws-sdk": "^2.98.0",
|
||||
"body-parser": "^1.17.2",
|
||||
"connect-busboy": "0.0.2",
|
||||
"convict": "^3.0.0",
|
||||
"express": "^4.15.3",
|
||||
"express-handlebars": "^3.0.0",
|
||||
"helmet": "^3.8.0",
|
||||
"mozlog": "^2.1.1",
|
||||
"raven": "^2.1.0",
|
||||
"redis": "^2.8.0"
|
||||
"repository": "mozilla/send",
|
||||
"homepage": "https://github.com/mozilla/send/",
|
||||
"license": "MPL-2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"precommit": "lint-staged",
|
||||
"clean": "rimraf dist",
|
||||
"build": "npm run clean && webpack -p",
|
||||
"lint": "npm-run-all lint:*",
|
||||
"lint:css": "stylelint 'assets/*.css'",
|
||||
"lint:js": "eslint .",
|
||||
"lint-locales": "node scripts/lint-locales",
|
||||
"lint-locales:dev": "npm run lint-locales",
|
||||
"lint-locales:prod": "npm run lint-locales -- --production",
|
||||
"format": "prettier '**/*.js' 'assets/*.css' --single-quote --write",
|
||||
"get-prod-locales": "node scripts/get-prod-locales",
|
||||
"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",
|
||||
"release": "npm-run-all contributors changelog",
|
||||
"test": "mocha test/unit",
|
||||
"start": "cross-env NODE_ENV=development webpack-dev-server",
|
||||
"prod": "node server/prod.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"asmcrypto.js": "0.0.11",
|
||||
"autoprefixer": "^7.1.2",
|
||||
"babel-core": "^6.25.0",
|
||||
"babel-loader": "^7.1.1",
|
||||
"babel-polyfill": "^6.23.0",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"babel-preset-stage-2": "^6.24.1",
|
||||
"bel": "^5.0.3",
|
||||
"browserify": "^14.4.0",
|
||||
"copy-webpack-plugin": "^4.0.1",
|
||||
"cross-env": "^5.0.5",
|
||||
"css-loader": "^0.28.4",
|
||||
"css-mqpacker": "^6.0.1",
|
||||
"cssnano": "^3.10.0",
|
||||
"eslint": "^4.3.0",
|
||||
"eslint-plugin-mocha": "^4.11.0",
|
||||
"eslint-plugin-node": "^5.1.1",
|
||||
"eslint-plugin-security": "^1.4.0",
|
||||
"extract-loader": "^1.0.0",
|
||||
"file-loader": "^0.11.2",
|
||||
"git-rev-sync": "^1.9.1",
|
||||
"html-loader": "^0.5.1",
|
||||
"html-webpack-plugin": "^2.30.1",
|
||||
"husky": "^0.14.3",
|
||||
"l20n": "^5.0.0",
|
||||
"lint-staged": "^4.0.3",
|
||||
"mkdirp": "^0.5.1",
|
||||
"mocha": "^3.4.2",
|
||||
"npm-run-all": "^4.0.2",
|
||||
"postcss-cli": "^4.1.0",
|
||||
"postcss-loader": "^2.0.6",
|
||||
"prettier": "^1.5.3",
|
||||
"proxyquire": "^1.8.0",
|
||||
"raven-js": "^3.17.0",
|
||||
"rimraf": "^2.6.1",
|
||||
"selenium-webdriver": "^3.5.0",
|
||||
"sinon": "^2.3.8",
|
||||
"stylelint": "^8.0.0",
|
||||
"stylelint-config-standard": "^17.0.0",
|
||||
"stylelint-no-unsupported-browser-features": "^1.0.0",
|
||||
"supertest": "^3.0.0",
|
||||
"testpilot-ga": "^0.3.0",
|
||||
"webcrypto-liner": "^0.1.25",
|
||||
"webpack": "^3.5.4",
|
||||
"webpack-dev-middleware": "^1.12.0"
|
||||
"lint-staged": {
|
||||
"*.js": [
|
||||
"prettier --single-quote --write",
|
||||
"eslint",
|
||||
"git add"
|
||||
],
|
||||
"*.css": [
|
||||
"prettier --single-quote --write",
|
||||
"stylelint",
|
||||
"git add"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.2.0"
|
||||
},
|
||||
"homepage": "https://github.com/mozilla/send/",
|
||||
"license": "MPL-2.0",
|
||||
"repository": "mozilla/send",
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^7.1.5",
|
||||
"babel-core": "^6.26.0",
|
||||
"babel-loader": "^7.1.2",
|
||||
"babel-plugin-yo-yoify": "^1.0.1",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"babel-preset-env": "^1.6.0",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"babel-preset-stage-2": "^6.24.1",
|
||||
"base64-js": "^1.2.1",
|
||||
"copy-webpack-plugin": "^4.1.1",
|
||||
"cross-env": "^5.0.5",
|
||||
"css-loader": "^0.28.7",
|
||||
"css-mqpacker": "^6.0.1",
|
||||
"cssnano": "^3.10.0",
|
||||
"eslint": "^4.8.0",
|
||||
"eslint-plugin-mocha": "^4.11.0",
|
||||
"eslint-plugin-node": "^5.2.0",
|
||||
"eslint-plugin-security": "^1.4.0",
|
||||
"expose-loader": "^0.7.3",
|
||||
"extract-loader": "^1.0.1",
|
||||
"file-loader": "^1.1.5",
|
||||
"git-rev-sync": "^1.9.1",
|
||||
"github-changes": "^1.1.0",
|
||||
"html-loader": "^0.5.1",
|
||||
"husky": "^0.14.3",
|
||||
"lint-staged": "^4.2.3",
|
||||
"mocha": "^3.5.3",
|
||||
"nanobus": "^4.2.0",
|
||||
"npm-run-all": "^4.1.1",
|
||||
"postcss-loader": "^2.0.6",
|
||||
"prettier": "^1.7.4",
|
||||
"proxyquire": "^1.8.0",
|
||||
"raven-js": "^3.18.1",
|
||||
"redis-mock": "^0.20.0",
|
||||
"require-from-string": "^2.0.1",
|
||||
"rimraf": "^2.6.2",
|
||||
"selenium-webdriver": "^3.6.0",
|
||||
"sinon": "^4.0.1",
|
||||
"string-hash": "^1.1.3",
|
||||
"stylelint-config-standard": "^17.0.0",
|
||||
"stylelint-no-unsupported-browser-features": "^1.0.1",
|
||||
"supertest": "^3.0.0",
|
||||
"testpilot-ga": "^0.3.0",
|
||||
"val-loader": "^1.0.2",
|
||||
"webpack": "^3.6.0",
|
||||
"webpack-dev-server": "^2.9.1",
|
||||
"webpack-manifest-plugin": "^1.3.2",
|
||||
"webpack-unassert-loader": "^1.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"aws-sdk": "^2.130.0",
|
||||
"body-parser": "^1.18.2",
|
||||
"choo": "^6.4.2",
|
||||
"connect-busboy": "0.0.2",
|
||||
"convict": "^4.0.1",
|
||||
"express": "^4.16.2",
|
||||
"express-request-language": "^1.1.12",
|
||||
"fluent": "^0.4.1",
|
||||
"fluent-langneg": "^0.1.0",
|
||||
"helmet": "^3.8.2",
|
||||
"mkdirp": "^0.5.1",
|
||||
"mozlog": "^2.1.1",
|
||||
"raven": "^2.2.1",
|
||||
"redis": "^2.8.0"
|
||||
},
|
||||
"availableLanguages": [
|
||||
"en-US",
|
||||
"ast",
|
||||
"az",
|
||||
"ca",
|
||||
"cak",
|
||||
"cs",
|
||||
"cy",
|
||||
"de",
|
||||
"dsb",
|
||||
"el",
|
||||
"en-US",
|
||||
"es-AR",
|
||||
"es-CL",
|
||||
"es-ES",
|
||||
"es-MX",
|
||||
"et",
|
||||
"fa",
|
||||
"fr",
|
||||
"fy-NL",
|
||||
"hsb",
|
||||
@@ -107,40 +150,5 @@
|
||||
"vi",
|
||||
"zh-CN",
|
||||
"zh-TW"
|
||||
],
|
||||
"scripts": {
|
||||
"precommit": "lint-staged",
|
||||
"clean": "rimraf dist",
|
||||
"build": "npm-run-all build:*",
|
||||
"build:js": "webpack -p",
|
||||
"build:version": "node scripts/version",
|
||||
"contributors": "git shortlog -s | awk -F\\t '{print $2}' > CONTRIBUTORS",
|
||||
"dev": "npm run clean && npm run build && npm start",
|
||||
"format": "prettier '{,frontend/src/,scripts/,server/,test/**/!(bundle)}*.{js,css}' --single-quote --write",
|
||||
"get-prod-locales": "node scripts/get-prod-locales",
|
||||
"get-prod-locales:write": "npm run get-prod-locales -- --write",
|
||||
"lint": "npm-run-all lint:*",
|
||||
"lint:css": "stylelint 'frontend/src/*.css'",
|
||||
"lint:js": "eslint .",
|
||||
"lint-locales": "node scripts/lint-locales",
|
||||
"lint-locales:dev": "npm run lint-locales",
|
||||
"lint-locales:prod": "npm run lint-locales -- --production",
|
||||
"start": "node server/server",
|
||||
"test": "cross-env NODE_ENV=test npm-run-all test:*",
|
||||
"test:unit": "mocha test/unit",
|
||||
"test:server": "mocha test/server",
|
||||
"test--browser": "browserify test/frontend/frontend.bundle.js -o test/frontend/bundle.js -d && node test/frontend/driver.js"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.js": [
|
||||
"prettier --single-quote --write",
|
||||
"eslint",
|
||||
"git add"
|
||||
],
|
||||
"*.css": [
|
||||
"prettier --single-quote --write",
|
||||
"stylelint",
|
||||
"git add"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@ const autoprefixer = require('autoprefixer');
|
||||
const cssnano = require('cssnano');
|
||||
const mqpacker = require('css-mqpacker');
|
||||
|
||||
const conf = require('./server/config');
|
||||
const config = require('./server/config');
|
||||
|
||||
const options = {
|
||||
plugins: [autoprefixer, mqpacker, cssnano]
|
||||
};
|
||||
|
||||
if (conf.env === 'development') {
|
||||
if (config.env === 'development') {
|
||||
options.map = { inline: true };
|
||||
}
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 3.3 KiB |
78
public/locales/ar/send.ftl
Normal file
@@ -0,0 +1,78 @@
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
title = فَيَرفُكس سِنْد
|
||||
siteSubtitle = تجربة وِبّيّة
|
||||
siteFeedback = الانطباعات
|
||||
uploadPageHeader = شارِك ملفاتك بخصوصية وتعمية
|
||||
uploadPageExplainer = أرسل الملفات عبر رابط آمن خاص ومعمّى تنتهي صلاحيته تلقائيا لتضمن عدم بقاء ما ترسله إلى الأبد.
|
||||
uploadPageLearnMore = اطّلع على المزيد
|
||||
uploadPageDropMessage = أسقِط ملفّك هنا لبدء الرفع
|
||||
uploadPageSizeMessage = لتتحصل على أفضل تجربة، من المستحسن أن يكون الملف أصغر من 1 غ.بايت
|
||||
uploadPageBrowseButton = اختر ملفّا على حاسوبك
|
||||
.title = اختر ملفّا على حاسوبك
|
||||
uploadPageBrowseButton1 = اختر ملفّا لرفعه
|
||||
uploadPageMultipleFilesAlert = رفع عدة ملفات (أو رفع مجلد) ليس مدعوما حاليا.
|
||||
importingFile = يستورد…
|
||||
encryptingFile = يعمّي…
|
||||
decryptingFile = يفك التعمية…
|
||||
notifyUploadDone = انتهى الرفع.
|
||||
uploadingPageMessage = ما إن يُرفع الملف سيُتاح ضبط خيارات انتهاء صلاحيته.
|
||||
uploadingPageCancel = ألغِ الرفع
|
||||
.title = ألغِ الرفع
|
||||
uploadCancelNotification = أُلغي الرفع.
|
||||
uploadingPageLargeFileMessage = هذا الملف كبير الحجم وسيأخذ رفعه وقتا. انتظر رجاءً.
|
||||
uploadingFileNotification = أعلِمني عندما يكتمل الرفع.
|
||||
uploadSvgAlt
|
||||
.alt = ارفع
|
||||
copyUrlFormLabelWithName = انسخ الرابط وشاركه لإرسال الملف: { $filename }
|
||||
copyUrlFormButton = انسخ إلى الحافظة
|
||||
.title = انسخ إلى الحافظة
|
||||
copiedUrl = نُسخ!
|
||||
deleteFileButton = احذف الملف
|
||||
.title = احذف الملف
|
||||
sendAnotherFileLink = أرسل ملفّا آخر
|
||||
.title = أرسل ملفّا آخر
|
||||
// Alternative text used on the download link/button (indicates an action).
|
||||
downloadAltText
|
||||
.alt = نزّل
|
||||
downloadFileName = نزّل { $filename }
|
||||
unlockInputLabel = أدخل كلمة السر
|
||||
unlockInputPlaceholder = كلمة السر
|
||||
downloadFileTitle = نزِّل الملف المعمّى
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
downloadMessage = يُرسل إليك صديقك ملفا عبر «فَيَرفُكس سِنْد»، وهي خدمة تتيح لك مشاركة الملفات عبر رابط آمن وخاص ومعمّى، حيث تنتهي صلاحياتها تلقائيا لتضمن عدم بقاء ما ترسله إلى الأبد.
|
||||
// Text and title used on the download link/button (indicates an action).
|
||||
downloadButtonLabel = نزّل
|
||||
.title = نزّل
|
||||
downloadNotification = لقد اكتمل التنزيل.
|
||||
downloadFinish = اكتمل التنزيل
|
||||
// This message is displayed when uploading or downloading a file, e.g. "(1,3 MB of 10 MB)".
|
||||
fileSizeProgress = ({ $partialSize } من أصل { $totalSize })
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
sendYourFilesLink = جرِّب «فَيَرفُكس سِنْد»
|
||||
downloadingPageMessage = رجاء أبقِ هذا اللسان مفتوحا حتى نجلب الملف ونفك تعميته.
|
||||
errorAltText
|
||||
.alt = خطأ أثناء الرفع
|
||||
errorPageHeader = حدث خطب ما.
|
||||
errorPageMessage = حدث خطب ما أثناء رفع الملف.
|
||||
errorPageLink = أرسل ملفا آخر
|
||||
fileTooBig = حجم الملف كبير للغاية لرفعه. يجب أن يكون أصغر من { $size }.
|
||||
notSupportedHeader = متصفحك غير مدعوم.
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
notSupportedDetail = للأسف فإن متصفحك لا يدعم تقنية الوِب التي يعتمد عليها «فَيَرفُكس سِنْد». عليك تجربة متصفح آخر، ونحن ننصحك بِفَيَرفُكس!
|
||||
notSupportedLink = لماذا متصفحي غير مدعوم؟
|
||||
notSupportedOutdatedDetail = للأسف فإن إصدارة فَيَرفُكس هذه لا تدعم تقنية الوِب التي يعتمد عليها «فَيَرفُكس سِنْد». عليك تحديث متصفحك.
|
||||
updateFirefox = حدّث فَيَرفُكس
|
||||
copyFileList = انسخ الرابط
|
||||
deleteFileList = احذف
|
||||
legalHeader = الشروط والخصوصية
|
||||
deletePopupText = أأحذف هذا الملف؟
|
||||
deletePopupYes = نعم
|
||||
deletePopupCancel = ألغِ
|
||||
deleteButtonHover
|
||||
.title = احذف
|
||||
copyUrlHover
|
||||
.title = انسخ الرابط
|
||||
footerLinkTerms = الشروط
|
||||
footerLinkCookies = الكعكات
|
||||
requirePasswordCheckbox = اطلب كلمة سر لتنزيل هذا الملف
|
||||
addPasswordButton = أضِف كلمة سر
|
||||
@@ -8,9 +8,7 @@ uploadPageLearnMore = Deprendi más
|
||||
uploadPageDropMessage = Suelta equí'l to ficheru p'aniciar la xuba
|
||||
uploadPageSizeMessage = Pal meyor funcionamientu, lo meyor ye que'l to ficheru seya menor de 1GB
|
||||
uploadPageBrowseButton = Esbilla un ficheru nel to ordenador
|
||||
.title = Esbilla un ficheru nel to ordenador
|
||||
uploadPageBrowseButton1 = Esbilla un ficheru pa unviar
|
||||
.title = Esbilla un ficheru pa unviar
|
||||
uploadPageMultipleFilesAlert = Anguaño nun se sofita la xuba múltiple de ficheros o carpetes.
|
||||
uploadPageBrowseButtonTitle = Xubir ficheru
|
||||
uploadingPageProgress = Xubiendo { $filename } ({ $size })
|
||||
@@ -21,52 +19,43 @@ decryptingFile = Descifrando...
|
||||
notifyUploadDone = Finó la to xuba.
|
||||
uploadingPageMessage = Namái que'l ficheru xuba, sedrás a afitar les opciones de caducidá.
|
||||
uploadingPageCancel = Encaboxar xuba
|
||||
.title = Encaboxar xuba
|
||||
uploadCancelNotification = Encaboxóse la to xuba.
|
||||
uploadingPageLargeFileMessage = Esti ficheru ye grande y pue entardar daqué en xubir. ¡Paciencia!
|
||||
uploadingFileNotification = Avísame cuando se complete la xuba.
|
||||
uploadSuccessConfirmHeader = Preparáu pa unviar
|
||||
uploadSvgAlt
|
||||
.alt = Xubir
|
||||
uploadSvgAlt = Xubir
|
||||
uploadSuccessTimingHeader = L'enllaz del to ficheru caducará dempués d'una descarga o en 24 hores.
|
||||
copyUrlFormLabelWithName = Copia y comparti l'enllaz pa unviar el to ficheru: { $filename }
|
||||
// Note: Title text for button should be the same.
|
||||
copyUrlFormButton = Copiar al cartafueyu
|
||||
.title = Copiar al cartafueyu
|
||||
copiedUrl = ¡Copióse!
|
||||
// Note: Title text for button should be the same.
|
||||
deleteFileButton = Desaniciar ficheru
|
||||
.title = Desaniciar ficheru
|
||||
// Note: Title text for button should be the same.
|
||||
sendAnotherFileLink = Unviar otru ficheru
|
||||
.title = Unviar otru ficheru
|
||||
// Alternative text used on the download link/button (indicates an action).
|
||||
downloadAltText
|
||||
.alt = Baxar
|
||||
downloadAltText = Baxar
|
||||
downloadFileName = Baxar { $filename }
|
||||
downloadFileSize = ({ $size })
|
||||
unlockInputLabel = Introducir contraseña
|
||||
unlockInputPlaceholder = Contraseña
|
||||
unlockButtonLabel = Desbloquiar
|
||||
downloadFileTitle = Baxar ficheru cifráu
|
||||
// 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.
|
||||
// Text and title used on the download link/button (indicates an action).
|
||||
downloadButtonLabel = Baxar
|
||||
.title = Baxar
|
||||
downloadNotification = Completóse la to descarga.
|
||||
downloadFinish = Descarga completada
|
||||
// This message is displayed when uploading or downloading a file, e.g. "(1,3 MB of 10 MB)".
|
||||
fileSizeProgress = ({ $partialSize } de { $totalSize })
|
||||
// Firefox Send is a brand name and should not be localized. Title text for button should be the same.
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
sendYourFilesLink = Prueba Firefox Send
|
||||
.title = Prueba Firefox Send
|
||||
downloadingPageProgress = Baxando { $filename } ({ $size })
|
||||
downloadingPageMessage = Dexa esta llingüeta abierta entrín vamos en cata del to ficheru y lu desciframos, por favor.
|
||||
errorAltText
|
||||
.alt = Fallu de xuba
|
||||
errorAltText = Fallu de xuba
|
||||
errorPageHeader = ¡Daqué foi mal!
|
||||
errorPageMessage = Hebo un fallu xubiendo'l ficheru.
|
||||
errorPageLink = Unviar otru ficheru
|
||||
fileTooBig = Esti ficheru ye mui grande como pa xubilu. Debería tener menos de { $size }.
|
||||
linkExpiredAlt
|
||||
.alt = Enllaz caducáu
|
||||
linkExpiredAlt = Enllaz caducáu
|
||||
expiredPageHeader = ¡Esti enllaz caducó o enxamás nun esistó!
|
||||
notSupportedHeader = El to restolador nun ta sofitáu.
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
@@ -87,13 +76,14 @@ legalNoticeMozilla = L'usu de Firefox Send tamién ta suxetu al <a>Avisu de priv
|
||||
deletePopupText = ¿Desaniciar esti ficheru?
|
||||
deletePopupYes = Sí
|
||||
deletePopupCancel = Encaboxar
|
||||
deleteButtonHover
|
||||
.title = Desaniciar
|
||||
copyUrlHover
|
||||
.title = Copiar URL
|
||||
deleteButtonHover = Desaniciar
|
||||
copyUrlHover = Copiar URL
|
||||
footerLinkLegal = Llegal
|
||||
// Test Pilot is a proper name and should not be localized.
|
||||
footerLinkAbout = Tocante a Test Pilot
|
||||
footerLinkPrivacy = Privacidá
|
||||
footerLinkTerms = Términos
|
||||
footerLinkCookies = Cookies
|
||||
requirePasswordCheckbox = Riquir una contraseña pa baxar esti ficheru
|
||||
addPasswordButton = Amestar contraseña
|
||||
passwordTryAgain = Contraseña incorreuta. Volvi tentalo.
|
||||
|
||||
@@ -8,9 +8,7 @@ uploadPageLearnMore = Ətraflı öyrən
|
||||
uploadPageDropMessage = Yükləmək üçün faylınızı buraya daşıyın
|
||||
uploadPageSizeMessage = Xidmətin daha yaxşı işləməsi üçün faylınız 1 GB-dan az olmalıdır
|
||||
uploadPageBrowseButton = Kompüterinizdən fayl seçin
|
||||
.title = Kompüterinizdən fayl seçin
|
||||
uploadPageBrowseButton1 = Yüklənəcək faylı seçin
|
||||
.title = Yüklənəcək faylı seçin
|
||||
uploadPageMultipleFilesAlert = Birdən çox fayl və ya qovluq yükləmə hələlik dəstəklənmir.
|
||||
uploadPageBrowseButtonTitle = Fayl yüklə
|
||||
uploadingPageProgress = { $filename } ({ $size }) yüklənir
|
||||
@@ -21,52 +19,43 @@ decryptingFile = Şifrə açılır...
|
||||
notifyUploadDone = Yükləməniz hazırdır.
|
||||
uploadingPageMessage = Faylınız yükləndikdən sonra vaxtı çıxma seçimlərini qura biləcəksiz.
|
||||
uploadingPageCancel = Yükləməni ləğv et
|
||||
.title = Yükləməni ləğv et
|
||||
uploadCancelNotification = Yükləməniz ləğv edildi.
|
||||
uploadingPageLargeFileMessage = Fayl böyükdür və yükləmək çox vaxt ala bilər. Səbirli olun!
|
||||
uploadingFileNotification = Yükləmə bitdiyində xəbər ver.
|
||||
uploadSuccessConfirmHeader = Göndərməyə hazır
|
||||
uploadSvgAlt
|
||||
.alt = Yüklə
|
||||
uploadSvgAlt = Yüklə
|
||||
uploadSuccessTimingHeader = Faylınızın keçidinin 1 endirmədən və ya 24 saatdan sonra vaxtı çıxacaq.
|
||||
copyUrlFormLabelWithName = Faylınızı göndərmək üçün keçidi köçürün: { $filename }
|
||||
// Note: Title text for button should be the same.
|
||||
copyUrlFormButton = Buferə köçür
|
||||
.title = Mübadilə buferinə köçür
|
||||
copiedUrl = Köçürüldü!
|
||||
// Note: Title text for button should be the same.
|
||||
deleteFileButton = Faylı sil
|
||||
.title = Faylı sil
|
||||
// Note: Title text for button should be the same.
|
||||
sendAnotherFileLink = Başqa fayl göndər
|
||||
.title = Başqa fayl göndər
|
||||
// Alternative text used on the download link/button (indicates an action).
|
||||
downloadAltText
|
||||
.alt = Endir
|
||||
downloadAltText = Endir
|
||||
downloadFileName = { $filename } faylını endir
|
||||
downloadFileSize = ({ $size })
|
||||
unlockInputLabel = Parol daxil edin
|
||||
unlockInputPlaceholder = Parol
|
||||
unlockButtonLabel = Aç
|
||||
downloadFileTitle = Şifrələnmiş Faylı Endir
|
||||
// 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.
|
||||
// Text and title used on the download link/button (indicates an action).
|
||||
downloadButtonLabel = Endir
|
||||
.title = Endir
|
||||
downloadNotification = Endirməniz tamamlandı.
|
||||
downloadFinish = Endirmə Tamamlandı
|
||||
// This message is displayed when uploading or downloading a file, e.g. "(1,3 MB of 10 MB)".
|
||||
fileSizeProgress = ({ $partialSize } / { $totalSize })
|
||||
// Firefox Send is a brand name and should not be localized. Title text for button should be the same.
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
sendYourFilesLink = Firefox Send Yoxla
|
||||
.title = Firefox Send Yoxla
|
||||
downloadingPageProgress = { $filename } faylı ({ $size }) endirilir
|
||||
downloadingPageMessage = Lütfən faylı endirib şifrəsini açarkən vərəqi açıq buraxın.
|
||||
errorAltText
|
||||
.alt = Yükləmə xətası
|
||||
errorAltText = Yükləmə xətası
|
||||
errorPageHeader = Nəsə səhv getdi!
|
||||
errorPageMessage = Faylı yüklərkən xəta baş verdi.
|
||||
errorPageLink = Başqa fayl göndər
|
||||
fileTooBig = Fayl yükləmək üçün çox böyükdür. Fayl { $size }-dan az olmalıdır.
|
||||
linkExpiredAlt
|
||||
.alt = Keçidin vaxtı çıxıb
|
||||
linkExpiredAlt = Keçidin vaxtı çıxıb
|
||||
expiredPageHeader = Keçidin vaxtı çıxıb və ya heç vaxt olmayıb!
|
||||
notSupportedHeader = Səyyahınız dəstəklənmir.
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
@@ -87,13 +76,14 @@ legalNoticeMozilla = Firefox Send saytının istifadəsi həmçinin Mozilla-nın
|
||||
deletePopupText = Fayl silinsin?
|
||||
deletePopupYes = Bəli
|
||||
deletePopupCancel = Ləğv et
|
||||
deleteButtonHover
|
||||
.title = Sil
|
||||
copyUrlHover
|
||||
.title = Keçidi Köçürt
|
||||
deleteButtonHover = Sil
|
||||
copyUrlHover = Keçidi Köçürt
|
||||
footerLinkLegal = Hüquqi
|
||||
// Test Pilot is a proper name and should not be localized.
|
||||
footerLinkAbout = Test Pilot Haqqında
|
||||
footerLinkPrivacy = Məxfilik
|
||||
footerLinkTerms = Şərtlər
|
||||
footerLinkCookies = Çərəzlər
|
||||
requirePasswordCheckbox = Bu faylı endirmək üçün parol tələb et
|
||||
addPasswordButton = Parol əlavə et
|
||||
incorrectPassword = Xətalı parol. Təkrar yoxlayın.
|
||||
|
||||
@@ -4,7 +4,6 @@ siteSubtitle = ওয়েব গবেষণা
|
||||
siteFeedback = প্রতিক্রিয়া
|
||||
uploadPageLearnMore = আরও জানুন
|
||||
uploadPageBrowseButton = আপনার কম্পিউটারে ফাইল নির্বাচন করুন
|
||||
.title = আপনার কম্পিউটারে ফাইল নির্বাচন করুন
|
||||
uploadPageBrowseButtonTitle = ফাইল আপলোড
|
||||
importingFile = ইম্পোর্ট হচ্ছে...
|
||||
verifyingFile = যাচাই হচ্ছে...
|
||||
@@ -12,33 +11,22 @@ encryptingFile = ইনক্রিপট হচ্ছে...
|
||||
decryptingFile = ডিক্রিপট হচ্ছে...
|
||||
notifyUploadDone = আপনার আপলোড সম্পন্ন হয়েছে।
|
||||
uploadingPageCancel = আপলোড বাতিল করুন
|
||||
.title = আপলোড বাতিল করুন
|
||||
uploadCancelNotification = আপনার অাপলোড বাতিল করা হয়েছে।
|
||||
uploadSuccessConfirmHeader = পাঠানোর জন্য প্রস্তুত
|
||||
uploadSvgAlt
|
||||
.alt = আপলোড
|
||||
// Note: Title text for button should be the same.
|
||||
uploadSvgAlt = আপলোড
|
||||
copyUrlFormButton = ক্লিপবোর্ডে কপি করুন
|
||||
.title = ক্লিপবোর্ডে কপি করুন
|
||||
copiedUrl = কপি করা হয়েছে!
|
||||
// Note: Title text for button should be the same.
|
||||
deleteFileButton = ফাইল মুছুন
|
||||
.title = ফাইল মুছুন
|
||||
// Note: Title text for button should be the same.
|
||||
sendAnotherFileLink = আরেকটি ফাইল পাঠান
|
||||
.title = আরেকটি ফাইল পাঠান
|
||||
// Alternative text used on the download link/button (indicates an action).
|
||||
downloadAltText
|
||||
.alt = ডাউনলোড
|
||||
downloadAltText = ডাউনলোড
|
||||
downloadFileName = ডাউনলোড { $filename }
|
||||
downloadFileSize = ({ $size })
|
||||
// Text and title used on the download link/button (indicates an action).
|
||||
downloadButtonLabel = ডাউনলোড
|
||||
.title = ডাউনলোড
|
||||
downloadNotification = আপনার ডাউনলোড সম্পন্ন হয়েছে।
|
||||
downloadFinish = ডাউনলোড সম্পন্ন
|
||||
errorAltText
|
||||
.alt = আপালোডে ত্রুটি
|
||||
errorAltText = আপালোডে ত্রুটি
|
||||
errorPageHeader = কোন সমস্যা হয়েছে!
|
||||
errorPageLink = আরেকটি ফাইল পাঠান
|
||||
updateFirefox = Firefox হালনাগাদ করুন
|
||||
@@ -53,10 +41,8 @@ legalHeader = শর্তাবলী এবং গোপনীয়তা
|
||||
deletePopupText = ফাইলটি মুছতে চান?
|
||||
deletePopupYes = হ্যাঁ
|
||||
deletePopupCancel = বাতিল
|
||||
deleteButtonHover
|
||||
.title = মুছে ফেলুন
|
||||
copyUrlHover
|
||||
.title = URL অনুলিপি করুন
|
||||
deleteButtonHover = মুছে ফেলুন
|
||||
copyUrlHover = URL অনুলিপি করুন
|
||||
footerLinkLegal = আইনগত
|
||||
// Test Pilot is a proper name and should not be localized.
|
||||
footerLinkAbout = Test Pilot পরিচিতি
|
||||
|
||||
91
public/locales/bs/send.ftl
Normal file
@@ -0,0 +1,91 @@
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
title = Firefox Send
|
||||
siteSubtitle = web eksperiment
|
||||
siteFeedback = Povratne informacije
|
||||
uploadPageHeader = Privatno, šifrovano dijeljenje datoteka
|
||||
uploadPageExplainer = Pošaljite datoteke putem sigurne, privatne i šifrovane veze koja automatski ističe kako bi se osiguralo da vaše stvari ne ostaju na mreži zauvijek.
|
||||
uploadPageLearnMore = Saznajte više
|
||||
uploadPageDropMessage = Prevucite vaše datoteke ovdje da biste počeli sa otpremanjem
|
||||
uploadPageSizeMessage = Za bolji rad predlažemo da datoteka ne bude veća od 1GB
|
||||
uploadPageBrowseButton = Odaberite datoteku na računaru
|
||||
uploadPageBrowseButton1 = Odaberite datoteku za otpremanje
|
||||
uploadPageMultipleFilesAlert = Otpremanje direktorija ili više datoteka trenutno nije podržano.
|
||||
uploadPageBrowseButtonTitle = Otpremi datoteku
|
||||
uploadingPageProgress = Otpremam { $filename } ({ $size })
|
||||
importingFile = Uvozim...
|
||||
verifyingFile = Potvrđujem...
|
||||
encryptingFile = Šifrujem...
|
||||
decryptingFile = Dešifrujem...
|
||||
notifyUploadDone = Vaše otpremanje je završeno.
|
||||
uploadingPageMessage = Nakon što se vaša datoteka otpremi, moći ćete da podesite opcije isteka.
|
||||
uploadingPageCancel = Otkaži otpremanje
|
||||
uploadCancelNotification = Vaše otpremanje je otkazano.
|
||||
uploadingPageLargeFileMessage = Ova datoteka je velika i otpremanje može potrajati. Budite strpljivi!
|
||||
uploadingFileNotification = Obavijesti me kada otpremanje bude gotovo.
|
||||
uploadSuccessConfirmHeader = Spremno za slanje
|
||||
uploadSvgAlt = Otpremi
|
||||
uploadSuccessTimingHeader = Veza prema vašoj datoteci će isteći nakon prvog preuzimanja ili za 24 sata.
|
||||
copyUrlFormLabelWithName = Iskopirajte i podijelite vezu da biste poslali datoteku: { $filename }
|
||||
copyUrlFormButton = Kopiraj u međuspremnik
|
||||
copiedUrl = Kopirano!
|
||||
deleteFileButton = Izbriši datoteku
|
||||
sendAnotherFileLink = Pošalji drugu datoteku
|
||||
// Alternative text used on the download link/button (indicates an action).
|
||||
downloadAltText = Preuzmi
|
||||
downloadFileName = Preuzmi { $filename }
|
||||
downloadFileSize = ({ $size })
|
||||
unlockInputLabel = Unesite lozinku
|
||||
unlockInputPlaceholder = Lozinka
|
||||
unlockButtonLabel = Otključaj
|
||||
downloadFileTitle = Preuzmi šifrovanu datoteku
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
downloadMessage = Vaš prijatelj vam je poslao datoteku preko usluge Firefox Send koja vam omogućava da dijelite datoteke preko sigurne, privatne i šifrovane veze koja samostalno ističe da vaše stvari ne ostanu zauvijek na internetu.
|
||||
// Text and title used on the download link/button (indicates an action).
|
||||
downloadButtonLabel = Preuzmi
|
||||
downloadNotification = Vaše preuzimanje je završeno.
|
||||
downloadFinish = Preuzimanje završeno
|
||||
// This message is displayed when uploading or downloading a file, e.g. "(1,3 MB of 10 MB)".
|
||||
fileSizeProgress = ({ $partialSize } od { $totalSize })
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
sendYourFilesLink = Probajte Firefox Send
|
||||
downloadingPageProgress = Preuzimanje { $filename } ({ $size })
|
||||
downloadingPageMessage = Ostavite ovaj tab otvorenim dok ne dobavimo vašu datoteku i dok je ne dešifrujemo.
|
||||
errorAltText = Greška pri otpremanju
|
||||
errorPageHeader = Nešto nije uredu!
|
||||
errorPageMessage = Dogodila se greška pri otpremanju datoteke.
|
||||
errorPageLink = Pošalji drugu datoteku
|
||||
fileTooBig = Ta datoteka je prevelika za otpremanje. Treba biti manja od { $size }.
|
||||
linkExpiredAlt = Veza istekla
|
||||
expiredPageHeader = Veza je istekla ili nikad nije postojala!
|
||||
notSupportedHeader = Vaš pretraživač nije podržan.
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
notSupportedDetail = Ovaj pretraživač nažalost ne podržava web tehnologiju koja omogućava Firefox Send. Trebate probati drugi pretraživač. Preporučujemo Firefox!
|
||||
notSupportedLink = Zašto moj pretraživač nije podržan?
|
||||
notSupportedOutdatedDetail = Nažalost ova verzija Firefoxa ne podržava web tehnologiju koja omogućava Firefox Send. Morate ažurirati vaš pretraživač.
|
||||
updateFirefox = Ažuriraj Firefox
|
||||
downloadFirefoxButtonSub = Besplatno preuzimanje
|
||||
uploadedFile = Datoteka
|
||||
copyFileList = Kopiraj URL
|
||||
// expiryFileList is used as a column header
|
||||
expiryFileList = Ističe za
|
||||
deleteFileList = Izbriši
|
||||
nevermindButton = Zanemari
|
||||
legalHeader = Uslovi i privatnost
|
||||
legalNoticeTestPilot = Firefox Send je trenutno Test Pilot eksperiment i podržan je <a>uslovima korištenja</a> i <a>obavještenjem o privatnosti</a>. Možete saznati više o ovom eksperimentu i o njegovom sakupljanju podataka <a>ovdje</a>.
|
||||
legalNoticeMozilla = Korištenje Firefox Send web stranice podlaže Mozillinom <a>obavještenju o privatnosti na web stranicama</a> i <a>uslovima korištenja web stranica</a>.
|
||||
deletePopupText = Izbrisati ovu datoteku?
|
||||
deletePopupYes = Da
|
||||
deletePopupCancel = Otkaži
|
||||
deleteButtonHover = Izbriši
|
||||
copyUrlHover = Kopiraj URL
|
||||
footerLinkLegal = Pravno
|
||||
// Test Pilot is a proper name and should not be localized.
|
||||
footerLinkAbout = O Test Pilotu
|
||||
footerLinkPrivacy = Privatnost
|
||||
footerLinkTerms = Uslovi
|
||||
footerLinkCookies = Kolačići
|
||||
requirePasswordCheckbox = Zahtjevaj lozinku za preuzimanje ove datoteke
|
||||
addPasswordButton = Dodaj lozinku
|
||||
passwordTryAgain = Netačna lozinka. Pokušajte ponovo.
|
||||
// This label is followed by the password needed to download a file
|
||||
passwordResult = Lozinka: { $password }
|
||||
@@ -8,9 +8,7 @@ uploadPageLearnMore = Més informació
|
||||
uploadPageDropMessage = Arrossegueu el fitxer aquí per començar a pujar-lo
|
||||
uploadPageSizeMessage = Funciona millor quan els fitxers tenen menys d'1 GB
|
||||
uploadPageBrowseButton = Trieu un fitxer de l'ordinador
|
||||
.title = Trieu un fitxer de l'ordinador
|
||||
uploadPageBrowseButton1 = Seleccioneu el fitxer que voleu pujar
|
||||
.title = Seleccioneu el fitxer que voleu pujar
|
||||
uploadPageMultipleFilesAlert = Actualment no es permet pujar diversos fitxers ni una carpeta.
|
||||
uploadPageBrowseButtonTitle = Puja el fitxer
|
||||
uploadingPageProgress = S'està pujant { $filename } ({ $size })
|
||||
@@ -21,52 +19,39 @@ decryptingFile = S'està desxifrant…
|
||||
notifyUploadDone = La pujada ha acabat.
|
||||
uploadingPageMessage = Quan s'hagi acabat de pujat el fitxer, podreu definir les opcions de caducitat.
|
||||
uploadingPageCancel = Cancel·la la pujada
|
||||
.title = Cancel·la la pujada
|
||||
uploadCancelNotification = La pujada s'ha cancel·lat.
|
||||
uploadingPageLargeFileMessage = Aquest fitxer és gros i pot trigar una estona a pujar. Espereu assegut…
|
||||
uploadingFileNotification = Notifica'm quan s'acabi de pujar.
|
||||
uploadSuccessConfirmHeader = Llest per enviar
|
||||
uploadSvgAlt
|
||||
.alt = Puja
|
||||
uploadSvgAlt = Puja
|
||||
uploadSuccessTimingHeader = L'enllaç al fitxer caducarà quan es baixi una vegada o d'aquí 24 hores.
|
||||
copyUrlFormLabelWithName = Copieu l'enllaç i compartiu-lo per enviar el fitxer: { $filename }
|
||||
// Note: Title text for button should be the same.
|
||||
copyUrlFormButton = Copia al porta-retalls
|
||||
.title = Copia al porta-retalls
|
||||
copiedUrl = Copiat!
|
||||
// Note: Title text for button should be the same.
|
||||
deleteFileButton = Suprimeix el fitxer
|
||||
.title = Suprimeix el fitxer
|
||||
// Note: Title text for button should be the same.
|
||||
sendAnotherFileLink = Envieu un altre fitxer
|
||||
.title = Envieu un altre fitxer
|
||||
// Alternative text used on the download link/button (indicates an action).
|
||||
downloadAltText
|
||||
.alt = Baixa
|
||||
downloadAltText = Baixa
|
||||
downloadFileName = Baixeu { $filename }
|
||||
downloadFileSize = ({ $size })
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
downloadMessage = Un amic us ha enviat un fitxer amb el Firefox Send, un servei que permet compartir fitxers mitjançant un enllaç segur, privat i xifrat que caduca automàticament per tal que les vostres dades no es conservin a Internet per sempre.
|
||||
// Text and title used on the download link/button (indicates an action).
|
||||
downloadButtonLabel = Baixa
|
||||
.title = Baixa
|
||||
downloadNotification = La baixada ha acabat.
|
||||
downloadFinish = Ha acabat la baixada
|
||||
// This message is displayed when uploading or downloading a file, e.g. "(1,3 MB of 10 MB)".
|
||||
fileSizeProgress = ({ $partialSize } de { $totalSize })
|
||||
// Firefox Send is a brand name and should not be localized. Title text for button should be the same.
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
sendYourFilesLink = Proveu el Firefox Send
|
||||
.title = Proveu el Firefox Send
|
||||
downloadingPageProgress = S'està baixant { $filename } ({ $size })
|
||||
downloadingPageMessage = Deixeu aquesta pestanya oberta per tal que el fitxer es pugui baixar i desxifrar.
|
||||
errorAltText
|
||||
.alt = S'ha produït un error en pujar
|
||||
errorAltText = S'ha produït un error en pujar
|
||||
errorPageHeader = Hi ha hagut un problema
|
||||
errorPageMessage = S'ha produït un error en pujar el fitxer.
|
||||
errorPageLink = Envieu un altre fitxer
|
||||
fileTooBig = Aquest fitxer és massa gros per pujar-lo. Ha de tenir menys de { $size }.
|
||||
linkExpiredAlt
|
||||
.alt = L'enllaç ha caducat
|
||||
linkExpiredAlt = L'enllaç ha caducat
|
||||
expiredPageHeader = Aquest enllaç ha caducat o no existeix.
|
||||
notSupportedHeader = El vostre navegador no és compatible.
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
@@ -87,10 +72,8 @@ legalNoticeMozilla = L'ús del Firefox Send també està subjecte a l'<a>Avís d
|
||||
deletePopupText = Voleu suprimir aquest fitxer?
|
||||
deletePopupYes = Sí
|
||||
deletePopupCancel = Cancel·la
|
||||
deleteButtonHover
|
||||
.title = Suprimeix
|
||||
copyUrlHover
|
||||
.title = Copia l'URL
|
||||
deleteButtonHover = Suprimeix
|
||||
copyUrlHover = Copia l'URL
|
||||
footerLinkLegal = Avís legal
|
||||
// Test Pilot is a proper name and should not be localized.
|
||||
footerLinkAbout = Quant al Test Pilot
|
||||
|
||||
91
public/locales/cak/send.ftl
Normal file
@@ -0,0 +1,91 @@
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
title = Firefox Send
|
||||
siteSubtitle = ajk'amaya'l solna'onem
|
||||
siteFeedback = Rutzijol
|
||||
uploadPageHeader = Kekomonïx Ichinan chuqa' Ewan Kisik'ixik taq Yakb'äl
|
||||
uploadPageExplainer = Ketaq taq yakb'äl rik'in jun ewan rusik'ixik, ichinan chuqa' jikon ximonel, ri ruyonil xtikis ruq'ijul richin ke ri' ri taq atzij man e okel ta pa k'amaya'l junelïk.
|
||||
uploadPageLearnMore = Tetamäx ch'aqa' chik
|
||||
uploadPageDropMessage = Tajik'a' pe ri ayakb'al wawe' richin nachäp kijotob'axik
|
||||
uploadPageSizeMessage = Richin chi ütz nel ri samaj, k'o ta chi ri yakb'äl man tik'o chi re ri 1GB
|
||||
uploadPageBrowseButton = Tacha' jun yakb'äl pan akematz'ib'
|
||||
uploadPageBrowseButton1 = Tacha' jun yakb'äl richin najotob'a'
|
||||
uploadPageMultipleFilesAlert = K'a man nuk'öch ta nijotob'äx jalajöj yakb'äl o jun molwuj.
|
||||
uploadPageBrowseButtonTitle = Tijotob'äx yakb'äl
|
||||
uploadingPageProgress = Tajin nijotob'äx { $filename } ({ $size })
|
||||
importingFile = Tajin nijik…
|
||||
verifyingFile = Tajin nijikib'äx...
|
||||
encryptingFile = Tajin newäx rusik'ixik...
|
||||
decryptingFile = Tajin netamäx rusik'ixik...
|
||||
notifyUploadDone = Xak'is rujotob'axik.
|
||||
uploadingPageMessage = Toq xtijotob'äx ri yakb'äl xkatikïr xtak'ëx pa taq cha'oj ri ruq'ijul xtik'is.
|
||||
uploadingPageCancel = Tiq'at jotob'anïk
|
||||
uploadCancelNotification = Xq'at ri ajotob'anik
|
||||
uploadingPageLargeFileMessage = Yalan nïm re yakb'äl re' ruma ri' toq xtiyoke' richin xtijote'. ¡Man tik'o ak'u'x!
|
||||
uploadingFileNotification = Tiya' pe rutzijol chwe toq xtitz'aqät rujotob'axik.
|
||||
uploadSuccessConfirmHeader = Ütz chik richin Nitaq
|
||||
uploadSvgAlt = Tijotob'äx
|
||||
uploadSuccessTimingHeader = Ri ruximonel yakb'äl xtik'is ruq'ijul toq xtiqasäx jumul o pa 24 ramaj.
|
||||
copyUrlFormLabelWithName = Tiwachib'ëx chuqa' tikomonïx ri ximonel richin nitaq ri ayakb'äl: { $filename }
|
||||
copyUrlFormButton = Tiwachib'ëx pa molwuj
|
||||
copiedUrl = ¡Xwachib'ëx!
|
||||
deleteFileButton = Tiyuj yakb'äl
|
||||
sendAnotherFileLink = Titaq jun chik yakb'äl
|
||||
// Alternative text used on the download link/button (indicates an action).
|
||||
downloadAltText = Tiqasäx
|
||||
downloadFileName = Tiqasäx { $filename }
|
||||
downloadFileSize = ({ $size })
|
||||
unlockInputLabel = Titz'ib'äx Ewan Tzij
|
||||
unlockInputPlaceholder = Ewan tzij
|
||||
unlockButtonLabel = Titzij chik
|
||||
downloadFileTitle = Tiqasäx Yakb'äl Ewan Rusik'ixik
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
downloadMessage = Jun awachib'il xutäq jun yakb'äl chawe rik'in ri Firefox Send, jun samaj ri nuya' q'ij chawe ye'akomonij taq yakb'äl rik'in jun jikïl, ichinan chuqa' ewan rusik'ixik ximonel, ri nik'is ruq'ijul pa ruyonil richin chi ri taq awachinaq man junelïk ta e okel pa k'amab'ey.
|
||||
// Text and title used on the download link/button (indicates an action).
|
||||
downloadButtonLabel = Tiqasäx
|
||||
downloadNotification = Xtz'aqät ri aqasanik.
|
||||
downloadFinish = Xtz'aqät qasanïk
|
||||
// This message is displayed when uploading or downloading a file, e.g. "(1,3 MB of 10 MB)".
|
||||
fileSizeProgress = ({ $partialSize } richin { $totalSize })
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
sendYourFilesLink = Titojtob'ëx Firefox Send
|
||||
downloadingPageProgress = Tajin niqasäx { $filename } ({ $size })
|
||||
downloadingPageMessage = Tijaq kan re ruwi' re' richin niqaqasaj ri yakb'äl chuqa' richin niqetamaj rusik'ixik.
|
||||
errorAltText = Xsach toq nijotob'äx
|
||||
errorPageHeader = ¡K'o ri man ütz ta xub'än!
|
||||
errorPageMessage = Xk'ulwachitäj jun sachoj toq tajin nijotob'äx ri yakb'äl.
|
||||
errorPageLink = Titaq jun chik yakb'äl
|
||||
fileTooBig = Yalan nïm re yakb'äl re' richin nijotob'äx. K'o ta chi man nik'o ta chi re ri { $size }.
|
||||
linkExpiredAlt = Xk'is ruq'ijul ri ximonel
|
||||
expiredPageHeader = ¡Xk'is ruq'ijul re ximonel re' o rik'in jub'a' majub'ey xk'oje'!
|
||||
notSupportedHeader = Man koch'el ta ri awokik'amaya'l.
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
notSupportedDetail = K'ayew ruma re okik'amaya'l re' man nuköch' ta ajk'amaya'l na'ob'äl nik'atzin chi re ri Firefox Send. K'o chi natojtob'ej jun chik okik'amaya'l. ¡Niqachilab'ej chawe ri Firefox!
|
||||
notSupportedLink = ¿Achike ruma man nikoch' taq ri wokik'amaya'l?
|
||||
notSupportedOutdatedDetail = K'ayew ruma re ruwäch Firefox re' man nuköch' ta ri ajk'amaya'l na'ob'äl nrajo' ri Firefox Send. Rajowaxik nak'ëx ri awokik'amaya'l.
|
||||
updateFirefox = Tik'ex ri Firefox
|
||||
downloadFirefoxButtonSub = Sipan Ruqasaxik
|
||||
uploadedFile = Yakb'äl
|
||||
copyFileList = Tiwachib'ëx URL
|
||||
// expiryFileList is used as a column header
|
||||
expiryFileList = Nik'is Ruq'ijul Pa
|
||||
deleteFileList = Tiyuj
|
||||
nevermindButton = Junam nub'än
|
||||
legalHeader = Ojqanem chuqa' Ichinanem
|
||||
legalNoticeTestPilot = Firefox Send k'a jun rutojtob'enik Test Pilot, chuqa' rutaqen rutzij ri <a>Rojqanem Samaj</a> chuqa' rik'in ri <a>Rutzijol Ichinanem</a>. Yatikïr nawetamaj ch'aq'a' chik chi rij re solna'oj re' chuqa' ri rumolik tzij <a>wawe'</a>.
|
||||
legalNoticeMozilla = Richin nokisäx ri ruxaq ruk'amaya'l Firefox Send k'o chi nitaqëx ri <a>Rutzijol Richinanem Ajk'amaya'l Ruxaq</a> chuqa' <a>Rojqanem rokisaxik Ajk'amaya'l Ruxaq</a>.
|
||||
deletePopupText = ¿La niyuj el re yakb'äl re'?
|
||||
deletePopupYes = Ja'
|
||||
deletePopupCancel = Tiq'at
|
||||
deleteButtonHover = Tiyuj
|
||||
copyUrlHover = Tiwachib'ëx URL
|
||||
footerLinkLegal = Taqanel tzijol
|
||||
// Test Pilot is a proper name and should not be localized.
|
||||
footerLinkAbout = Chi rij Test Pilot
|
||||
footerLinkPrivacy = Ichinanem
|
||||
footerLinkTerms = Taq ojqanem
|
||||
footerLinkCookies = Taq kaxlanwey
|
||||
requirePasswordCheckbox = Tik'utüx jun ewan tzij richin niqasäx re yakb'äl re'
|
||||
addPasswordButton = Titz'aqatisäx Ewan Tzij
|
||||
passwordTryAgain = Itzel ri ewan tzij. Tatojtob'ej chik.
|
||||
// This label is followed by the password needed to download a file
|
||||
passwordResult = Ewan tzij: { $password }
|
||||
@@ -8,9 +8,7 @@ uploadPageLearnMore = Zjistit více
|
||||
uploadPageDropMessage = Přesunutím souboru sem spustíte jeho nahrávání
|
||||
uploadPageSizeMessage = Nahrávání funguje nejlépe pro soubory do velikosti 1 GB.
|
||||
uploadPageBrowseButton = Vybrat soubor z počítače
|
||||
.title = Výběr souboru z počítače
|
||||
uploadPageBrowseButton1 = Zvolte soubor k nahrání
|
||||
.title = Zvolte soubor k nahrání
|
||||
uploadPageMultipleFilesAlert = Nahrávání více souborů najednou nebo celých složek zatím není podporováno.
|
||||
uploadPageBrowseButtonTitle = Nahrát soubor
|
||||
uploadingPageProgress = Nahrávání souboru { $filename } ({ $size })
|
||||
@@ -21,52 +19,43 @@ decryptingFile = Probíhá dešifrování…
|
||||
notifyUploadDone = Nahrávání vašeho souboru bylo dokončeno.
|
||||
uploadingPageMessage = Po dokončení nahrávání můžete nastavit dobu expirace souboru.
|
||||
uploadingPageCancel = Zrušit nahrávání
|
||||
.title = Zrušit nahrávání
|
||||
uploadCancelNotification = Nahrávání vašeho souboru bylo zrušeno.
|
||||
uploadingPageLargeFileMessage = Tento soubor je veliký a jeho nahrávání může chvíli trvat. Posaďte se na chvilku.
|
||||
uploadingFileNotification = Upozornit, až bude nahrávání dokončeno.
|
||||
uploadSuccessConfirmHeader = Připraveno k odeslání
|
||||
uploadSvgAlt
|
||||
.alt = Nahrát
|
||||
uploadSvgAlt = Nahrát
|
||||
uploadSuccessTimingHeader = Platnost odkazu na váš soubor vyprší po jeho prvním stažení, nebo po 24 hodinách.
|
||||
copyUrlFormLabelWithName = Zkopírujte a sdílejte odkaz na váš soubor: { $filename }
|
||||
// Note: Title text for button should be the same.
|
||||
copyUrlFormButton = Zkopírovat do schránky
|
||||
.title = Zkopírovat do schránky
|
||||
copiedUrl = Zkopírováno!
|
||||
// Note: Title text for button should be the same.
|
||||
deleteFileButton = Smazat soubor
|
||||
.title = Smazání souboru
|
||||
// Note: Title text for button should be the same.
|
||||
sendAnotherFileLink = Poslat další soubor
|
||||
.title = Poslat další soubor
|
||||
// Alternative text used on the download link/button (indicates an action).
|
||||
downloadAltText
|
||||
.alt = Stáhnout
|
||||
downloadAltText = Stáhnout
|
||||
downloadFileName = Stáhnout { $filename }
|
||||
downloadFileSize = ({ $size })
|
||||
unlockInputLabel = Zadejte heslo
|
||||
unlockInputPlaceholder = Heslo
|
||||
unlockButtonLabel = Odemknout
|
||||
downloadFileTitle = Stáhnout šifrovaný soubor
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
downloadMessage = Někdo vám posílá soubor pomocí služby Firefox Send, které umožňuje bezpečné, soukromí a šifrované sdílení souborů, které jsou pak automaticky smazány, aby nezůstaly na internetu navěky.
|
||||
downloadMessage = Někdo vám posílá soubor pomocí služby Firefox Send, které umožňuje bezpečné, soukromé a šifrované sdílení souborů, které jsou pak automaticky smazány, aby nezůstaly na internetu navěky.
|
||||
// Text and title used on the download link/button (indicates an action).
|
||||
downloadButtonLabel = Stáhnout
|
||||
.title = Stáhnout
|
||||
downloadNotification = Stahování bylo dokončeno.
|
||||
downloadFinish = Stahování dokončeno
|
||||
// This message is displayed when uploading or downloading a file, e.g. "(1,3 MB of 10 MB)".
|
||||
fileSizeProgress = ({ $partialSize } z { $totalSize })
|
||||
// Firefox Send is a brand name and should not be localized. Title text for button should be the same.
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
sendYourFilesLink = Vyzkoušejte Firefox Send
|
||||
.title = Vyzkoušejte Firefox Send
|
||||
downloadingPageProgress = Stahování { $filename } ({ $size })
|
||||
downloadingPageMessage = Ponechte prosím tento panel otevřený, dokud nepřipravíme váš soubor a nedešifrujeme ho.
|
||||
errorAltText
|
||||
.alt = Chyba při nahrávání souboru
|
||||
errorAltText = Chyba při nahrávání souboru
|
||||
errorPageHeader = Nastala chyba!
|
||||
errorPageMessage = Při nahrávání souboru se vyskytl problém.
|
||||
errorPageLink = Poslat další soubor
|
||||
fileTooBig = Tento soubor je příliš veliký. Velikost nahrávaných souborů by neměla překročit { $size }.
|
||||
linkExpiredAlt
|
||||
.alt = Platnost odkazu vypršela
|
||||
linkExpiredAlt = Platnost odkazu vypršela
|
||||
expiredPageHeader = Platnost tohoto odkazu buď vypršela, nebo vůbec nikdy neexistoval.
|
||||
notSupportedHeader = Váš prohlížeč není podporován.
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
@@ -87,13 +76,16 @@ legalNoticeMozilla = Používání webové služby Firefox Send se řídí <a>Z
|
||||
deletePopupText = Smazat tento soubor?
|
||||
deletePopupYes = Ano
|
||||
deletePopupCancel = Zrušit
|
||||
deleteButtonHover
|
||||
.title = Smazat
|
||||
copyUrlHover
|
||||
.title = Kopírovat URL
|
||||
deleteButtonHover = Smazat
|
||||
copyUrlHover = Kopírovat URL
|
||||
footerLinkLegal = Právní informace
|
||||
// Test Pilot is a proper name and should not be localized.
|
||||
footerLinkAbout = O programu Test Pilot
|
||||
footerLinkPrivacy = Soukromí
|
||||
footerLinkTerms = Podmínky
|
||||
footerLinkCookies = Cookies
|
||||
requirePasswordCheckbox = Vyžadovat heslo pro stažení tohoto souboru
|
||||
addPasswordButton = Přidat heslo
|
||||
passwordTryAgain = Špatné heslo. Zkuste to znovu.
|
||||
// This label is followed by the password needed to download a file
|
||||
passwordResult = Heslo: { $password }
|
||||
|
||||
@@ -2,16 +2,14 @@
|
||||
title = Firefox Send
|
||||
siteSubtitle = arbrawf gwe
|
||||
siteFeedback = Adborth
|
||||
uploadPageHeader = Rhannu Ffeiliau wedi eu Hamgryptio Preifat
|
||||
uploadPageHeader = Rhannu Ffeiliau wedi eu Hamgryptio yn Breifat
|
||||
uploadPageExplainer = Anfon ffeiliau drwy ddolen diogel, breifat ac wedi ei amgryptio sy'n dod i ben yn awtomatig er mwyn sicrhau nad yw eich pethau'n bodoli ar lein am byth.
|
||||
uploadPageLearnMore = Dysgu rhagor
|
||||
uploadPageDropMessage = Gollyngwch eich ffeiliau yma i gychwyn llwytho i fyny
|
||||
uploadPageSizeMessage = Mae'n well cadw maint y ffeiliau o dan 1GB er mwyn iddo weithio ar ei orau.
|
||||
uploadPageBrowseButton = Dewiswch ffeil ar eich cyfrifiadur
|
||||
.title = Dewiswch ffeil ar eich cyfrifiadur
|
||||
uploadPageBrowseButton1 = Dewiswch ffeil i'w llwytho i fyny
|
||||
.title = Dewiswch ffeil i'w llwytho i fyny
|
||||
uploadPageMultipleFilesAlert = Nid yw llwytho nifer lluosog o ffeilia neu ffolder yn cael ei gynnal ar hyn o bryd.
|
||||
uploadPageMultipleFilesAlert = Nid yw llwytho nifer lluosog o ffeiliau neu ffolder yn cael ei gynnal ar hyn o bryd.
|
||||
uploadPageBrowseButtonTitle = Llwytho ffeil i fyny
|
||||
uploadingPageProgress = Llwytho $filename} i fyny ({ $size })
|
||||
importingFile = Mewnforio…
|
||||
@@ -19,60 +17,51 @@ verifyingFile = Wrthi'n gwirio…
|
||||
encryptingFile = Wrthi'n amgryptio…
|
||||
decryptingFile = Wrthi'n dadgryptio…
|
||||
notifyUploadDone = Mae eich llwytho wedi gorffen.
|
||||
uploadingPageMessage = Unwaith y bydd eich ffeil wedi llwytho bydd modd gosod manylion dod i ben.
|
||||
uploadingPageMessage = Unwaith y bydd eich ffeil wedi llwytho bydd modd gosod y manylion dod i ben.
|
||||
uploadingPageCancel = Diddymu'r llwytho
|
||||
.title = Diddymu'r llwytho
|
||||
uploadCancelNotification = Cafodd eich llwytho ei ddiddymu.
|
||||
uploadingPageLargeFileMessage = Mae'r ffeil yn fawr a gall gymryd peth amser i'w llwytho. Arhoswch!
|
||||
uploadingFileNotification = Dweud wrtha i pan fydd y llwytho wedi gorffen.
|
||||
uploadingPageLargeFileMessage = Mae'r ffeil yn fawr a gall gymryd peth amser i'w llwytho. Amynedd!
|
||||
uploadingFileNotification = Dweud pan fydd y llwytho wedi gorffen.
|
||||
uploadSuccessConfirmHeader = Yn Barod i Anfon
|
||||
uploadSvgAlt
|
||||
.alt = LLwytho i Fyny
|
||||
uploadSvgAlt = Llwytho i Fyny
|
||||
uploadSuccessTimingHeader = Bydd y ddolen i'ch ffeil y dod i ben ar ôl 1 llwytho neu o fewn 24 awr.
|
||||
copyUrlFormLabelWithName = Copïo a rhannu'r ddolen i anfon eich ffeil: { $filename }
|
||||
// Note: Title text for button should be the same.
|
||||
copyUrlFormButton = Copïo i'r clipfwrdd
|
||||
.title = Copïo i'r clipfwrdd
|
||||
copiedUrl = Wedi eu copïo!
|
||||
// Note: Title text for button should be the same.
|
||||
deleteFileButton = Dileu ffeil
|
||||
.title = Dileu ffeil
|
||||
// Note: Title text for button should be the same.
|
||||
sendAnotherFileLink = Anfon ffeil arall
|
||||
.title = Anfon ffeil arall
|
||||
// Alternative text used on the download link/button (indicates an action).
|
||||
downloadAltText
|
||||
.alt = Llwytho i lawr
|
||||
downloadAltText = Llwytho i lawr
|
||||
downloadFileName = Llwytho i lawr { $filename }
|
||||
downloadFileSize = ({ $size })
|
||||
unlockInputLabel = Rhowch Gyfrinair
|
||||
unlockInputPlaceholder = Cyfrinair
|
||||
unlockButtonLabel = Datgloi
|
||||
downloadFileTitle = Llwythwch Ffeil wedi ei Hamgryptio i Lawr
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
downloadMessage = Mae ffrind i chi yn anfon ffeil atoch drwy Firefox Send, gwasanaeth sy'n caniatáu i chi rannu ffeiliau drwy ddolen ddiogel, breifat ac wedi ei amgryptio sy'n dod i ben yn awtomatig er mwyn sicrhau nad yw eich deunydd yn aros ar-lein am byth.
|
||||
// Text and title used on the download link/button (indicates an action).
|
||||
downloadButtonLabel = Llwytho i Lawr
|
||||
.title = Llwytho i Lawr
|
||||
downloadNotification = Mae eich llwytho wedi gorffen
|
||||
downloadFinish = Llwytho wedi Gorffen
|
||||
// This message is displayed when uploading or downloading a file, e.g. "(1,3 MB of 10 MB)".
|
||||
fileSizeProgress = ({ $partialSize } o { $totalSize })
|
||||
// Firefox Send is a brand name and should not be localized. Title text for button should be the same.
|
||||
sendYourFilesLink = Profwch Firefox Send
|
||||
.title = Profwch Firefox Send
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
sendYourFilesLink = Rhowch gynnig ar Firefox Send
|
||||
downloadingPageProgress = Llwytho i lawr { $filename } ({ $size })
|
||||
downloadingPageMessage = Gadewch y tab yma ar agor tra fyddwn yn estyn eich ffeil a'i dad-amgryptio.
|
||||
errorAltText
|
||||
.alt = Gwall llwytho
|
||||
errorAltText = Gwall llwytho
|
||||
errorPageHeader = Aeth rhywbeth o'i le!
|
||||
errorPageMessage = Bu gwall wrth lwytho'r ffeil.
|
||||
errorPageLink = Anfon ffeil arall
|
||||
fileTooBig = Mae'r ffeil yn rhy fawr i'w llwytho. Dylai fod yn llai na { $size }.
|
||||
linkExpiredAlt
|
||||
.alt = Mae'r ddolen wedi dod i ben
|
||||
linkExpiredAlt = Mae'r ddolen wedi dod i ben
|
||||
expiredPageHeader = Mae'r ddolen wedi dod i ben neu nad yw wedi bodoli erioed!
|
||||
notSupportedHeader = Nid yw eich porwr yn cael ei gynnal.
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
notSupportedDetail = Yn anffodus nid yw'r porwr hwn yn cynnal y technoleg gwe sy'n cynnal Firefox Send. Bydd angen i chi ddefnyddio porwr arall. Rydym ni'n argymell Firefox!
|
||||
notSupportedDetail = Yn anffodus, nid yw'r porwr hwn yn cynnal y technoleg gwe sy'n cynnal Firefox Send. Bydd angen i chi ddefnyddio porwr arall. Rydym ni'n argymell Firefox!
|
||||
notSupportedLink = Pam nad yw fy mhorwr yn cael ei gynnal?
|
||||
notSupportedOutdatedDetail = Yn anffodus nid yw'r fersiwn yma o Firefox yn cynnal y technoleg gwe sy'n gyrru Firefox Send. Bydd angen i chi ddiweddaru eich porwr.
|
||||
notSupportedOutdatedDetail = Yn anffodus, nid yw'r fersiwn yma o Firefox yn cynnal y technoleg gwe sy'n gyrru Firefox Send. Bydd angen i chi ddiweddaru eich porwr.
|
||||
updateFirefox = Diweddaru Firefox
|
||||
downloadFirefoxButtonSub = Llwytho i Lawr am Ddim
|
||||
uploadedFile = Ffeil
|
||||
@@ -82,18 +71,21 @@ expiryFileList = Daw i ben ymhen
|
||||
deleteFileList = Dileu
|
||||
nevermindButton = Dim ots
|
||||
legalHeader = Amodau a Phreifatrwydd
|
||||
legalNoticeTestPilot = Ar hyn o mae Firefox Send yn arbrawf o fewn rhaglen Test Pilot ac yn destun <a>Amodau Gwasanaeth</a> a <a>Hysbysiad Preifatrwydd</a> Test Pilot . Gallwch ddysgu rhagor am yr arbrawf a'i gasglu data <a>yma</a>.
|
||||
legalNoticeTestPilot = Ar hyn o mae Firefox Send yn arbrawf o fewn rhaglen Test Pilot ac yn destun <a>Amodau Gwasanaeth</a> a <a>Hysbysiad Preifatrwydd</a> Test Pilot . Gallwch ddysgu rhagor am yr arbrawf a'r data mae'n ei gasglu <a>yma</a>.
|
||||
legalNoticeMozilla = Mae'r defnydd o wefan Firefox Send hefyd yn destun <a>Hysbysiad Preifatrwydd Gwefannau</a> ac <a>Amodau Defnydd Gwefannau</a> Mozilla.
|
||||
deletePopupText = Dileu'r ffeil?
|
||||
deletePopupYes = Iawn
|
||||
deletePopupCancel = Diddymu
|
||||
deleteButtonHover
|
||||
.title = Dileu
|
||||
copyUrlHover
|
||||
.title = Copïo'r URL
|
||||
deleteButtonHover = Dileu
|
||||
copyUrlHover = Copïo'r URL
|
||||
footerLinkLegal = Cyfreithiol
|
||||
// Test Pilot is a proper name and should not be localized.
|
||||
footerLinkAbout = Ynghylch Test Pilot
|
||||
footerLinkPrivacy = Preifatrwydd
|
||||
footerLinkTerms = Amodau
|
||||
footerLinkCookies = Cwcis
|
||||
requirePasswordCheckbox = Gosod angen cyfrinair i lwytho'r ffeil hon i lawr
|
||||
addPasswordButton = Ychwanegu Cyfrinair
|
||||
passwordTryAgain = Cyfrinair anghywir. Ceisiwch eto.
|
||||
// This label is followed by the password needed to download a file
|
||||
passwordResult = Cyfrinair: { $password }
|
||||
|
||||
@@ -8,9 +8,7 @@ uploadPageLearnMore = Mehr erfahren
|
||||
uploadPageDropMessage = Ziehen Sie eine Datei zum Hochladen hierher
|
||||
uploadPageSizeMessage = Dateien unter 1 GB sorgen für erhöhte Zuverlässigkeit des Betriebs
|
||||
uploadPageBrowseButton = Wählen Sie eine Datei auf Ihrem Computer aus
|
||||
.title = Wählen Sie eine Datei auf Ihrem Computer aus
|
||||
uploadPageBrowseButton1 = Datei zum Hochladen auswählen
|
||||
.title = Datei zum Hochladen auswählen
|
||||
uploadPageMultipleFilesAlert = Hochladen mehrerer Dateien oder eines Ordners wird derzeit nicht unterstützt.
|
||||
uploadPageBrowseButtonTitle = Datei hochladen
|
||||
uploadingPageProgress = { $filename } ({ $size }) wird hochgeladen
|
||||
@@ -21,52 +19,43 @@ decryptingFile = Wird entschlüsselt…
|
||||
notifyUploadDone = Ihr Upload ist abgeschlossen.
|
||||
uploadingPageMessage = Sobald Ihre Datei hochgeladen wird, können Sie die Optionen zum Ablaufdatum auswählen.
|
||||
uploadingPageCancel = Hochladen abbrechen
|
||||
.title = Hochladen abbrechen
|
||||
uploadCancelNotification = Ihr Upload wurde abgebrochen.
|
||||
uploadingPageLargeFileMessage = Diese Datei ist groß, sodass das hochladen einige Zeit dauern könnte. Haben Sie Geduld!
|
||||
uploadingFileNotification = Mich benachrichtigen, wenn der Upload abgeschlossen ist.
|
||||
uploadSuccessConfirmHeader = Bereit zum Senden
|
||||
uploadSvgAlt
|
||||
.alt = Hochladen
|
||||
uploadSvgAlt = Hochladen
|
||||
uploadSuccessTimingHeader = Der Link zu Ihrer Datei läuft nach einem Download oder in 24 Stunden ab.
|
||||
copyUrlFormLabelWithName = Kopieren und teilen Sie den Link, um Ihre Datei zu senden: { $filename }
|
||||
// Note: Title text for button should be the same.
|
||||
copyUrlFormButton = In Zwischenablage kopieren
|
||||
.title = In Zwischenablage kopieren
|
||||
copiedUrl = Kopiert!
|
||||
// Note: Title text for button should be the same.
|
||||
deleteFileButton = Datei löschen
|
||||
.title = Datei löschen
|
||||
// Note: Title text for button should be the same.
|
||||
sendAnotherFileLink = Eine weitere Datei senden
|
||||
.title = Eine weitere Datei senden
|
||||
// Alternative text used on the download link/button (indicates an action).
|
||||
downloadAltText
|
||||
.alt = Herunterladen
|
||||
downloadAltText = Herunterladen
|
||||
downloadFileName = { $filename } herunterladen
|
||||
downloadFileSize = ({ $size })
|
||||
unlockInputLabel = Passwort eingeben
|
||||
unlockInputPlaceholder = Passwort
|
||||
unlockButtonLabel = Entsperren
|
||||
downloadFileTitle = Verschlüsselte Datei herunterladen
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
downloadMessage = Ihr Freund schickt Ihnen eine Datei mit Firefox Send, einem Dienst, mit dem Sie Dateien über einen sicheren, privaten und verschlüsselten Link teilen können, der automatisch abläuft, damit Ihre Daten nicht für immer im Internet bleiben.
|
||||
// Text and title used on the download link/button (indicates an action).
|
||||
downloadButtonLabel = Herunterladen
|
||||
.title = Herunterladen
|
||||
downloadNotification = Der Download wurde abgeschlossen.
|
||||
downloadFinish = Download abgeschlossen
|
||||
// This message is displayed when uploading or downloading a file, e.g. "(1,3 MB of 10 MB)".
|
||||
fileSizeProgress = ({ $partialSize } von { $totalSize })
|
||||
// Firefox Send is a brand name and should not be localized. Title text for button should be the same.
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
sendYourFilesLink = Firefox Send ausprobieren
|
||||
.title = Firefox Send ausprobieren
|
||||
downloadingPageProgress = { $filename } ({ $size }) wird heruntergeladen
|
||||
downloadingPageMessage = Bitte lassen Sie diesen Tab geöffnet, während Ihre Datei heruntergeladen und entschlüsselt wird.
|
||||
errorAltText
|
||||
.alt = Fehler beim Hochladen
|
||||
errorAltText = Fehler beim Hochladen
|
||||
errorPageHeader = Ein Fehler ist aufgetreten!
|
||||
errorPageMessage = Beim Hochladen der Datei ist ein Fehler aufgetreten.
|
||||
errorPageLink = Eine weitere Datei senden
|
||||
fileTooBig = Die Datei ist zu groß zum Hochladen. Sie sollte maximal { $size } groß sein.
|
||||
linkExpiredAlt
|
||||
.alt = Link abgelaufen
|
||||
linkExpiredAlt = Link abgelaufen
|
||||
expiredPageHeader = Dieser Link ist abgelaufen oder hat nie existiert!
|
||||
notSupportedHeader = Ihr Browser wird nicht unterstützt.
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
@@ -87,13 +76,16 @@ legalNoticeMozilla = Die Nutzung der Website von Firefox Send unterliegt außerd
|
||||
deletePopupText = Diese Datei löschen?
|
||||
deletePopupYes = Ja
|
||||
deletePopupCancel = Abbrechen
|
||||
deleteButtonHover
|
||||
.title = Löschen
|
||||
copyUrlHover
|
||||
.title = Adresse kopieren
|
||||
deleteButtonHover = Löschen
|
||||
copyUrlHover = Adresse kopieren
|
||||
footerLinkLegal = Rechtliches
|
||||
// Test Pilot is a proper name and should not be localized.
|
||||
footerLinkAbout = Über Test Pilot
|
||||
footerLinkPrivacy = Datenschutz
|
||||
footerLinkTerms = Nutzungsbedingungen
|
||||
footerLinkCookies = Cookies
|
||||
requirePasswordCheckbox = Zum Herunterladen dieser Datei soll ein Passwort erforderlich sein
|
||||
addPasswordButton = Passwort hinzufügen
|
||||
passwordTryAgain = Falsches Passwort. Versuchen Sie es erneut.
|
||||
// This label is followed by the password needed to download a file
|
||||
passwordResult = Passwort: { $password }
|
||||
|
||||
@@ -8,9 +8,7 @@ uploadPageLearnMore = Dalšne informacije
|
||||
uploadPageDropMessage = Śěgniśo swóju dataju sem, aby ju nagrał
|
||||
uploadPageSizeMessage = Wužywajśo nejlěpje dataje, kótarež su mjeńše ako 1 GB za lěpšu spušćobnosć.
|
||||
uploadPageBrowseButton = Wubjeŕśo dataju na swójom licadle
|
||||
.title = Wubjeŕśo dataju na swójom licadle
|
||||
uploadPageBrowseButton1 = Wubjeŕśo dataju za nagraśe
|
||||
.title = Wubjeŕśo dataju za nagraśe
|
||||
uploadPageMultipleFilesAlert = Nagrawanje někotarych datajow abo zarědnika se tuchylu njepódpěra.
|
||||
uploadPageBrowseButtonTitle = Dataju nagraś
|
||||
uploadingPageProgress = { $filename } ({ $size }) se nagrawa
|
||||
@@ -21,52 +19,43 @@ decryptingFile = Dešifrěrujo se...
|
||||
notifyUploadDone = Wašo nagraśe jo dokóńcone.
|
||||
uploadingPageMessage = Gaž se waša dataja nagrawa, móžośo nastajenja spadnjenja póstajiś.
|
||||
uploadingPageCancel = Nagraśe pśetergnus
|
||||
.title = Nagraśe pśetergnus
|
||||
uploadCancelNotification = Wašo nagraśe jo se pśetergnuło.
|
||||
uploadingPageLargeFileMessage = Toś ta dataja jo wjelika a nagrawanje mógło chylku traś. Buźćo sćerpliwy!
|
||||
uploadingFileNotification = K wěsći daś, gaž nagraśe jo dokóńcone.
|
||||
uploadSuccessConfirmHeader = Gótowy za słanje
|
||||
uploadSvgAlt
|
||||
.alt = Nagraś
|
||||
uploadSvgAlt = Nagraś
|
||||
uploadSuccessTimingHeader = Wótkaz k wašej dataji pó 1 ześěgnjenju abo 24 góźinach spadnjo.
|
||||
copyUrlFormLabelWithName = Kopěrujśo a źělśo wótkaz, aby swóju dataju pósłał: { $filename }
|
||||
// Note: Title text for button should be the same.
|
||||
copyUrlFormButton = Do mjazywótkłada kopěrowaś
|
||||
.title = Do mjazywótkłada kopěrowaś
|
||||
copiedUrl = Kopěrowany!
|
||||
// Note: Title text for button should be the same.
|
||||
deleteFileButton = Dataju wulašowaś
|
||||
.title = Dataju wulašowaś
|
||||
// Note: Title text for button should be the same.
|
||||
sendAnotherFileLink = Drugu dataju pósłaś
|
||||
.title = Drugu dataju pósłaś
|
||||
// Alternative text used on the download link/button (indicates an action).
|
||||
downloadAltText
|
||||
.alt = Ześěgnuś
|
||||
downloadAltText = Ześěgnuś
|
||||
downloadFileName = { $filename } ześěgnuś
|
||||
downloadFileSize = ({ $size })
|
||||
unlockInputLabel = Gronidło zapódaś
|
||||
unlockInputPlaceholder = Gronidło
|
||||
unlockButtonLabel = Wótwóriś
|
||||
downloadFileTitle = Skoděrowanu dataju ześěgnuś
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
downloadMessage = Waš pśijaśel wam dataju z Firefox Send sćelo, słužba, kótaraž wam zmóžnja, dataje pśez wěsty, priwatny a skoděrowany wótkaz źěliś, kótaryž awtomatiski spadnjo, až njeby waše daty na pśecej online wóstawali.
|
||||
// Text and title used on the download link/button (indicates an action).
|
||||
downloadButtonLabel = Ześěgnuś
|
||||
.title = Ześěgnuś
|
||||
downloadNotification = Wašo ześěgnjenje jo dokóńcone.
|
||||
downloadFinish = Ześěgnjenje dokóńcone
|
||||
// This message is displayed when uploading or downloading a file, e.g. "(1,3 MB of 10 MB)".
|
||||
fileSizeProgress = ({ $partialSize } z { $totalSize })
|
||||
// Firefox Send is a brand name and should not be localized. Title text for button should be the same.
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
sendYourFilesLink = Firefox Send wopytaś
|
||||
.title = Firefox Send wopytaś
|
||||
downloadingPageProgress = { $filename } ({ $size }) se ześěgujo
|
||||
downloadingPageMessage = Pšosym wóstajśo toś ten rejtark wócynjony, mjaztym až wašu dataju ześěgujomy a dešifrěrujomy.
|
||||
errorAltText
|
||||
.alt = Nagrawańska zmólka
|
||||
errorAltText = Nagrawańska zmólka
|
||||
errorPageHeader = Něco njejo se raźiło!
|
||||
errorPageMessage = Pśi nagrawanju dataje jo zmólka nastała.
|
||||
errorPageLink = Drugu dataju pósłaś
|
||||
fileTooBig = Toś ta dataja jo pśewjelika za nagraśe. Měła mjeńša ako { $size } byś.
|
||||
linkExpiredAlt
|
||||
.alt = Wótkaz spadnjony
|
||||
linkExpiredAlt = Wótkaz spadnjony
|
||||
expiredPageHeader = Toś ten wótkaz jo spadnjony abo njejo nigda eksistěrował!
|
||||
notSupportedHeader = Waš wobglědowak se njepódpěra.
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
@@ -87,13 +76,16 @@ legalNoticeMozilla = Teke wužywanje websedła Firefox Send <a>pokazce priwatnos
|
||||
deletePopupText = Toś tu dataju lašowaś?
|
||||
deletePopupYes = Jo
|
||||
deletePopupCancel = Pśetergnuś
|
||||
deleteButtonHover
|
||||
.title = Wulašowaś
|
||||
copyUrlHover
|
||||
.title = URL kopěrowaś
|
||||
deleteButtonHover = Wulašowaś
|
||||
copyUrlHover = URL kopěrowaś
|
||||
footerLinkLegal = Pšawniske
|
||||
// Test Pilot is a proper name and should not be localized.
|
||||
footerLinkAbout = Wó Test Pilot
|
||||
footerLinkPrivacy = Priwatnosć
|
||||
footerLinkTerms = Wuměnjenja
|
||||
footerLinkCookies = Cookieje
|
||||
requirePasswordCheckbox = Gronidło za ześěgnjenje toś teje dataje pominaś
|
||||
addPasswordButton = Gronidło pśidaś
|
||||
passwordTryAgain = Wopacne gronidło. Wopytajśo hyšći raz.
|
||||
// This label is followed by the password needed to download a file
|
||||
passwordResult = Gronidło: { $password }
|
||||
|
||||