Compare commits
506 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82fe65ada2 | ||
|
|
ce79d7b745 | ||
|
|
755cc4f5ec | ||
|
|
fde4d311e3 | ||
|
|
b08f40aaa3 | ||
|
|
e12ade6b31 | ||
|
|
43fc80ef41 | ||
|
|
6ca96157f6 | ||
|
|
856181ea54 | ||
|
|
f84bd46cdc | ||
|
|
0b43924ee2 | ||
|
|
16d3fd3828 | ||
|
|
cf5defa6a9 | ||
|
|
3de760db12 | ||
|
|
1366f0b68e | ||
|
|
be498e0bd3 | ||
|
|
a4e13f032a | ||
|
|
fef3136b1b | ||
|
|
6b318c248f | ||
|
|
fd6a3a5579 | ||
|
|
2292267e39 | ||
|
|
dbfae53222 | ||
|
|
d39ed267f3 | ||
|
|
9d04ad704a | ||
|
|
11cbb4df23 | ||
|
|
232911f725 | ||
|
|
41a0c6c73f | ||
|
|
10e80edb1d | ||
|
|
af3848586c | ||
|
|
48807bf030 | ||
|
|
21a14cb225 | ||
|
|
b725d07744 | ||
|
|
94e707da8a | ||
|
|
4fb4041f13 | ||
|
|
545da556d2 | ||
|
|
9f3da3454f | ||
|
|
15105426b1 | ||
|
|
b2d2e27945 | ||
|
|
d93a4cf9f3 | ||
|
|
8c3ec5da3d | ||
|
|
d067b1d55a | ||
|
|
55070ace8b | ||
|
|
0f35961173 | ||
|
|
b63f68bf74 | ||
|
|
8c6d6db07e | ||
|
|
a74a70fd8c | ||
|
|
97ad674be2 | ||
|
|
81534d2c13 | ||
|
|
51b7036456 | ||
|
|
e40fdad251 | ||
|
|
e4b0e97ac0 | ||
|
|
003320ca34 | ||
|
|
780addc50b | ||
|
|
8892a5f43d | ||
|
|
75e52ebca7 | ||
|
|
e7b3bbcd0e | ||
|
|
b9d60639e2 | ||
|
|
2ce0ce65e1 | ||
|
|
a32bdcb06a | ||
|
|
f9f8dff8b4 | ||
|
|
5c5cd1e501 | ||
|
|
89bcd1c4a8 | ||
|
|
6b7b142961 | ||
|
|
565e47aef8 | ||
|
|
dd448cb3ed | ||
|
|
7a9a19e3b9 | ||
|
|
a6e94441aa | ||
|
|
68d66d8ab3 | ||
|
|
aa5cc7ff9f | ||
|
|
eb8ede8376 | ||
|
|
4dc92bae4e | ||
|
|
9e1a975b23 | ||
|
|
e5a26fa0be | ||
|
|
4035a3a1e5 | ||
|
|
0380d286cd | ||
|
|
91dc3aa5d2 | ||
|
|
4bbffcf35e | ||
|
|
bb73eeb7ea | ||
|
|
546f8a12e4 | ||
|
|
3a865d4aaa | ||
|
|
05feb6b1b0 | ||
|
|
efa6fe3ef1 | ||
|
|
e917927979 | ||
|
|
61689ed451 | ||
|
|
b74919c376 | ||
|
|
67cc4f9600 | ||
|
|
ec749de3fa | ||
|
|
b99cd7ed3f | ||
|
|
87b9c955ca | ||
|
|
00aee2771e | ||
|
|
291276494d | ||
|
|
fc3823f0af | ||
|
|
855ef73a9a | ||
|
|
517a8d2104 | ||
|
|
a042795ec1 | ||
|
|
e427d65bf5 | ||
|
|
5ce319456f | ||
|
|
03828bfdaa | ||
|
|
7c836d121c | ||
|
|
b2ee3c5f30 | ||
|
|
04205783b8 | ||
|
|
f27e71fbd8 | ||
|
|
fbca679334 | ||
|
|
5b594ba0b4 | ||
|
|
3d4b5b0c1e | ||
|
|
c7574de7dc | ||
|
|
c8a3bb5f81 | ||
|
|
118edef773 | ||
|
|
050a1aff83 | ||
|
|
9ad74e9cbf | ||
|
|
09f8decc59 | ||
|
|
0decdf156b | ||
|
|
9316655b8c | ||
|
|
1bcc4fd2d7 | ||
|
|
239a4da145 | ||
|
|
d8e66c9b6a | ||
|
|
72279f4995 | ||
|
|
48b21de011 | ||
|
|
f7dc86ab2b | ||
|
|
39bfe6d2cb | ||
|
|
d6f534c3c0 | ||
|
|
45067d0354 | ||
|
|
da1ff63f72 | ||
|
|
5d715c50de | ||
|
|
47072ae1fe | ||
|
|
72f301fa45 | ||
|
|
e9b89629a6 | ||
|
|
3ef5ef166f | ||
|
|
37ca7a706a | ||
|
|
734c65fbda | ||
|
|
145605d628 | ||
|
|
7ac432fbc5 | ||
|
|
0c92cec2ea | ||
|
|
f6a788b36f | ||
|
|
72dffcf46b | ||
|
|
3140cdd148 | ||
|
|
d87adbce63 | ||
|
|
ffdf2bc0cd | ||
|
|
be5d7a1c9f | ||
|
|
94288b5cef | ||
|
|
63abbf5949 | ||
|
|
49214281f7 | ||
|
|
9688dde1a4 | ||
|
|
fdcc31f049 | ||
|
|
55ed6100e0 | ||
|
|
7fb11ba912 | ||
|
|
f3d77fdcf2 | ||
|
|
6489ab6a56 | ||
|
|
bace117ada | ||
|
|
76175d61af | ||
|
|
87110095a0 | ||
|
|
50ba8bec5a | ||
|
|
1741b1c686 | ||
|
|
99097baf9d | ||
|
|
bac1cc8243 | ||
|
|
096489d486 | ||
|
|
9811a9a3e1 | ||
|
|
910cde4380 | ||
|
|
4f1ccf83c8 | ||
|
|
9501c1ce4b | ||
|
|
069f0e53e1 | ||
|
|
aedfba795e | ||
|
|
9f162c0703 | ||
|
|
7b96c46e39 | ||
|
|
3d48ea71b9 | ||
|
|
24ee984a2e | ||
|
|
4255cbe540 | ||
|
|
fe16f24c41 | ||
|
|
a6e1fc5c44 | ||
|
|
8434312728 | ||
|
|
46f641aaec | ||
|
|
c246d8d517 | ||
|
|
4ec1aeafaf | ||
|
|
27cfd04ea7 | ||
|
|
797cfcb98d | ||
|
|
96a9b52e6d | ||
|
|
3b7462070b | ||
|
|
f08dd5960b | ||
|
|
9972196f70 | ||
|
|
ebbf06787c | ||
|
|
1d2b0cb093 | ||
|
|
6f27c6e4aa | ||
|
|
7b6008c37e | ||
|
|
cf5405fbe4 | ||
|
|
b4ec7402fc | ||
|
|
ff9a107a29 | ||
|
|
417ad87bcc | ||
|
|
265f99f327 | ||
|
|
0f8c3caf18 | ||
|
|
3a7677b46d | ||
|
|
3fd64d9956 | ||
|
|
1d26f4b24f | ||
|
|
1a4cea7b4f | ||
|
|
bbb1c42641 | ||
|
|
7ce2320eda | ||
|
|
1729b30f89 | ||
|
|
25d6595d5e | ||
|
|
3b31b9d65b | ||
|
|
e9e64fed3f | ||
|
|
3b88ea9b1c | ||
|
|
623f76fa6c | ||
|
|
19efcb1a0d | ||
|
|
31c8916622 | ||
|
|
7a0b189a1d | ||
|
|
fa0d56d57a | ||
|
|
85670bbc6a | ||
|
|
fc9a85b6ad | ||
|
|
8760b132da | ||
|
|
b80ee8d778 | ||
|
|
c8e168aa3e | ||
|
|
106aef579f | ||
|
|
d7084829c3 | ||
|
|
243d72eed5 | ||
|
|
aa60e97d5d | ||
|
|
4ae12db99c | ||
|
|
98ad058e2e | ||
|
|
58cc381abf | ||
|
|
d039c38f00 | ||
|
|
81f3347981 | ||
|
|
b96d2949f7 | ||
|
|
a2e745a349 | ||
|
|
0fe02b18ce | ||
|
|
655eb8c253 | ||
|
|
d9356f8171 | ||
|
|
be7d23163c | ||
|
|
7ac2e57484 | ||
|
|
727ea1283a | ||
|
|
3314057059 | ||
|
|
df1b00fa2c | ||
|
|
6434329f61 | ||
|
|
454853dc93 | ||
|
|
b663bf94e4 | ||
|
|
081cf081c5 | ||
|
|
f86b164c62 | ||
|
|
92b79e8272 | ||
|
|
335086c9d7 | ||
|
|
60806a3954 | ||
|
|
e0e96e7b9b | ||
|
|
cda35eb127 | ||
|
|
bf1ef345a2 | ||
|
|
e54ae7a51f | ||
|
|
a5a2d654ae | ||
|
|
b6340d8657 | ||
|
|
cdb87e2ac2 | ||
|
|
a0baed4e14 | ||
|
|
573a2d4fb1 | ||
|
|
df3ae9e22d | ||
|
|
52bffa024e | ||
|
|
459499d5f5 | ||
|
|
7cc94f6829 | ||
|
|
53d5b08559 | ||
|
|
c517b074f1 | ||
|
|
503ba43ebc | ||
|
|
b0755f4bc5 | ||
|
|
89ece1ec4c | ||
|
|
9fcaefbd99 | ||
|
|
81f33c7bbd | ||
|
|
0ce1bf2360 | ||
|
|
bb10c1aa9d | ||
|
|
b2c1daa6c7 | ||
|
|
da4338ed69 | ||
|
|
1849d712b1 | ||
|
|
28a7e0f717 | ||
|
|
ddbfc79cae | ||
|
|
ec8900d38f | ||
|
|
8aadd5d775 | ||
|
|
268f9306d9 | ||
|
|
866552254c | ||
|
|
42f87424c6 | ||
|
|
4899fb27ec | ||
|
|
c2c8d2bdaa | ||
|
|
daa373967f | ||
|
|
dccc9bfbcd | ||
|
|
f51a873752 | ||
|
|
bad5efc5e8 | ||
|
|
edf8384348 | ||
|
|
65c2f0e191 | ||
|
|
427a09296c | ||
|
|
ba11d62003 | ||
|
|
900dbe781e | ||
|
|
ee19aa8f2e | ||
|
|
e9cff66852 | ||
|
|
f63894bea7 | ||
|
|
35a285d71a | ||
|
|
ba80b01150 | ||
|
|
c9be359db4 | ||
|
|
46a3933dbb | ||
|
|
7b4060f9e1 | ||
|
|
beb3a6e67b | ||
|
|
b393c35284 | ||
|
|
ad371553ba | ||
|
|
0107fb9486 | ||
|
|
7cf70317e8 | ||
|
|
7d8aaac5d2 | ||
|
|
2b5f7d126f | ||
|
|
3cbc71ca1b | ||
|
|
9c4ac38a6c | ||
|
|
9ae64a97ba | ||
|
|
2807b1cad5 | ||
|
|
157e832c95 | ||
|
|
d78fcd3721 | ||
|
|
ac7ab79aef | ||
|
|
00fb353465 | ||
|
|
f0ec5a9496 | ||
|
|
7a31082da1 | ||
|
|
b54f4575ee | ||
|
|
58840e2c00 | ||
|
|
490a1e88eb | ||
|
|
2f8a3c9904 | ||
|
|
e7fdf76120 | ||
|
|
d0d41b743a | ||
|
|
a2995411d6 | ||
|
|
3246c4a621 | ||
|
|
48faf929a4 | ||
|
|
b7f922a999 | ||
|
|
bfcdf9340d | ||
|
|
4ed515f5a3 | ||
|
|
84b2737ffb | ||
|
|
deabca5a94 | ||
|
|
e9a49e23e8 | ||
|
|
cfdef23365 | ||
|
|
7c4b6a9de4 | ||
|
|
bb8866f73f | ||
|
|
33c42648ef | ||
|
|
7feddd2eee | ||
|
|
1344b84cf5 | ||
|
|
ef03b750c5 | ||
|
|
0c07d78a37 | ||
|
|
b5885e446c | ||
|
|
0fa3d4481a | ||
|
|
49e7c2e05b | ||
|
|
2dc7d046ef | ||
|
|
3e0bd41efd | ||
|
|
929eaca2d8 | ||
|
|
60517c5ab6 | ||
|
|
97a8a2b305 | ||
|
|
8fc54bdbe2 | ||
|
|
6ff251b24a | ||
|
|
7237800a91 | ||
|
|
b4cc8e92c7 | ||
|
|
2e233da16d | ||
|
|
b703f78db9 | ||
|
|
13e792cf4d | ||
|
|
552c2d74f3 | ||
|
|
593f23b021 | ||
|
|
1baf83bac3 | ||
|
|
d43ca3190e | ||
|
|
c4c7860876 | ||
|
|
0decdeb37c | ||
|
|
47505dcc31 | ||
|
|
e71d6e792b | ||
|
|
29796cfec8 | ||
|
|
4bdf255c3a | ||
|
|
11efe8b8d1 | ||
|
|
3dd2d09584 | ||
|
|
ed4e8e8f25 | ||
|
|
181a74df88 | ||
|
|
166b2f3a52 | ||
|
|
bd5cdc52f9 | ||
|
|
5d0318c102 | ||
|
|
17adc644fb | ||
|
|
f48159dc0b | ||
|
|
360697c034 | ||
|
|
45dd833b27 | ||
|
|
98491eed01 | ||
|
|
fdafc1c59e | ||
|
|
a32638ed4c | ||
|
|
39080a6046 | ||
|
|
a124d36714 | ||
|
|
7f9e619643 | ||
|
|
db12f2f5c8 | ||
|
|
415e0b70f3 | ||
|
|
0e4b1e5ec7 | ||
|
|
054a97371c | ||
|
|
2b25b6a6ea | ||
|
|
780ed3120e | ||
|
|
bdd3cbd4c7 | ||
|
|
0c8013038f | ||
|
|
c020d59c56 | ||
|
|
a5c336494b | ||
|
|
aaa4655f45 | ||
|
|
ed18ec0bc5 | ||
|
|
1de43f31b6 | ||
|
|
1a04c86edd | ||
|
|
2bbdcae82e | ||
|
|
9b0aa5d601 | ||
|
|
cdc261e30a | ||
|
|
09133a66b0 | ||
|
|
64f1a31533 | ||
|
|
9a92a50a5f | ||
|
|
2afd93e82f | ||
|
|
cb62cc1e9d | ||
|
|
2cd3fc5af9 | ||
|
|
bac710a17f | ||
|
|
fb8b0f78ca | ||
|
|
e18ce15753 | ||
|
|
ed8ce9e3ca | ||
|
|
af5ef04115 | ||
|
|
9530a3df52 | ||
|
|
2794ac653f | ||
|
|
43eb758d73 | ||
|
|
d0364cd101 | ||
|
|
6a008bf312 | ||
|
|
dfb271410c | ||
|
|
789d67209c | ||
|
|
a31f6b75d9 | ||
|
|
f814427a7d | ||
|
|
b0307e92d4 | ||
|
|
7fa3d69aa1 | ||
|
|
58555a6b85 | ||
|
|
52113395db | ||
|
|
8dd1309c21 | ||
|
|
202e428412 | ||
|
|
6e7ed3cea3 | ||
|
|
41cb49141b | ||
|
|
82a8283b6e | ||
|
|
a5d28adc44 | ||
|
|
82e206bccf | ||
|
|
1e4d6646c6 | ||
|
|
acbf9fc32f | ||
|
|
046f227003 | ||
|
|
50ac9e32be | ||
|
|
3459dcaa15 | ||
|
|
9410defab6 | ||
|
|
b5a26e11f8 | ||
|
|
c51481628d | ||
|
|
24fa51a12c | ||
|
|
7328520d05 | ||
|
|
409d206f1e | ||
|
|
b0b393f3d9 | ||
|
|
c4499088c8 | ||
|
|
4cccd6ac5c | ||
|
|
188b28fce3 | ||
|
|
24adda6c7d | ||
|
|
4b49302fbe | ||
|
|
402ab350de | ||
|
|
47b68770af | ||
|
|
60aa16a327 | ||
|
|
5702a4806b | ||
|
|
74c4bdb660 | ||
|
|
203a2cf7fb | ||
|
|
1faa2733b3 | ||
|
|
ffa432a876 | ||
|
|
009fd29265 | ||
|
|
8f05c2324e | ||
|
|
e1ab515883 | ||
|
|
a68a7a60a7 | ||
|
|
b4dc274646 | ||
|
|
b31892bdc6 | ||
|
|
f388a1348d | ||
|
|
52dacbddf9 | ||
|
|
cc52f60aa1 | ||
|
|
1af818b691 | ||
|
|
d76f7758e7 | ||
|
|
48ab2f2400 | ||
|
|
0aa844eebc | ||
|
|
481b02ccf2 | ||
|
|
67e6ef6fda | ||
|
|
7d19f86d7a | ||
|
|
2d27d8a47c | ||
|
|
10fac130ef | ||
|
|
c6b632543d | ||
|
|
32eb8157eb | ||
|
|
3628f22114 | ||
|
|
177fa37041 | ||
|
|
717f6576ea | ||
|
|
f1b2ffa0fa | ||
|
|
ac73c23c73 | ||
|
|
ac40308b1c | ||
|
|
6fb81aa78c | ||
|
|
92430c78c2 | ||
|
|
cb7ddaa295 | ||
|
|
786d079632 | ||
|
|
4af25a505a | ||
|
|
3218803aae | ||
|
|
2311d5bcef | ||
|
|
e56d92334f | ||
|
|
bc24a069da | ||
|
|
837747f8f7 | ||
|
|
a8c32ae49c | ||
|
|
32c5b414de | ||
|
|
12c81a22e8 | ||
|
|
0c5d0d4bb2 | ||
|
|
234f9c624d | ||
|
|
da669b44ff | ||
|
|
3c39f5f085 | ||
|
|
6de91b5872 | ||
|
|
ff9a0979f6 | ||
|
|
e1e8af2489 | ||
|
|
1eb000f615 | ||
|
|
e20fd97e59 | ||
|
|
d10ceacd67 | ||
|
|
208c28ee01 | ||
|
|
cdd1bb3c29 | ||
|
|
3d9c4fa320 | ||
|
|
9c4d18ef3b | ||
|
|
ce9ff3959f | ||
|
|
51aef4e1e5 | ||
|
|
90247059d0 | ||
|
|
c97abb46ed | ||
|
|
b8f5e371c7 | ||
|
|
401311a05f | ||
|
|
652b8e4e15 | ||
|
|
99b7e7c0f1 | ||
|
|
81442bb6f2 | ||
|
|
137f474b69 |
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
node_modules
|
||||
dist
|
||||
.idea
|
||||
|
||||
3
.nsprc
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"exceptions": ["https://nodesecurity.io/advisories/534"]
|
||||
}
|
||||
18
README.md
@@ -7,6 +7,20 @@
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
* [What it does](#what-it-does)
|
||||
* [Requirements](#requirements)
|
||||
* [Development](#development)
|
||||
* [Commands](#commands)
|
||||
* [Configuration](#configuration)
|
||||
* [Localization](#localization)
|
||||
* [Contributing](#contributing)
|
||||
* [Testing](#testing)
|
||||
* [License](#license)
|
||||
|
||||
---
|
||||
|
||||
## What it does
|
||||
|
||||
A file sharing experiment which allows you to send encrypted files to other users.
|
||||
@@ -55,13 +69,13 @@ The server is configured with environment variables. See [server/config.js](serv
|
||||
|
||||
## Localization
|
||||
|
||||
Firefox Send localization is managed via [Pontoon](https://pontoon.mozilla.org/projects/test-pilot-firefox-send/), not direct pull requests to the repository. If you want to fix a typo, add a new language, or simply know more about localization, please get in touch with the [existing localization team](https://pontoon.mozilla.org/teams/) for your language, or Mozilla’s [l10n-drivers](https://wiki.mozilla.org/L10n:Mozilla_Team#Mozilla_Corporation) for guidance.
|
||||
Firefox Send localization is managed via [Pontoon](https://pontoon.mozilla.org/projects/test-pilot-firefox-send/), not direct pull requests to the repository. If you want to fix a typo, add a new language, or simply know more about localization, please get in touch with the [existing localization team](https://pontoon.mozilla.org/teams/) for your language or Mozilla’s [l10n-drivers](https://wiki.mozilla.org/L10n:Mozilla_Team#Mozilla_Corporation) for guidance.
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
Pull requests are always welcome! Feel free to check out the list of ["good first bugs"](https://github.com/mozilla/send/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+bug%22).
|
||||
Pull requests are always welcome! Feel free to check out the list of ["good first issues"](https://github.com/mozilla/send/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22).
|
||||
|
||||
---
|
||||
|
||||
|
||||
213
app/api.js
Normal file
@@ -0,0 +1,213 @@
|
||||
import { arrayToB64, b64ToArray } from './utils';
|
||||
|
||||
function post(obj) {
|
||||
return {
|
||||
method: 'POST',
|
||||
headers: new Headers({
|
||||
'Content-Type': 'application/json'
|
||||
}),
|
||||
body: JSON.stringify(obj)
|
||||
};
|
||||
}
|
||||
|
||||
function parseNonce(header) {
|
||||
header = header || '';
|
||||
return header.split(' ')[1];
|
||||
}
|
||||
|
||||
async function fetchWithAuth(url, params, keychain) {
|
||||
const result = {};
|
||||
params = params || {};
|
||||
const h = await keychain.authHeader();
|
||||
params.headers = new Headers({ Authorization: h });
|
||||
const response = await fetch(url, params);
|
||||
result.response = response;
|
||||
result.ok = response.ok;
|
||||
const nonce = parseNonce(response.headers.get('WWW-Authenticate'));
|
||||
result.shouldRetry = response.status === 401 && nonce !== keychain.nonce;
|
||||
keychain.nonce = nonce;
|
||||
return result;
|
||||
}
|
||||
|
||||
async function fetchWithAuthAndRetry(url, params, keychain) {
|
||||
const result = await fetchWithAuth(url, params, keychain);
|
||||
if (result.shouldRetry) {
|
||||
return fetchWithAuth(url, params, keychain);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function del(id, owner_token) {
|
||||
const response = await fetch(`/api/delete/${id}`, post({ owner_token }));
|
||||
return response.ok;
|
||||
}
|
||||
|
||||
export async function setParams(id, owner_token, params) {
|
||||
const response = await fetch(
|
||||
`/api/params/${id}`,
|
||||
post({
|
||||
owner_token,
|
||||
dlimit: params.dlimit
|
||||
})
|
||||
);
|
||||
return response.ok;
|
||||
}
|
||||
|
||||
export async function fileInfo(id, owner_token) {
|
||||
const response = await fetch(`/api/info/${id}`, post({ owner_token }));
|
||||
if (response.ok) {
|
||||
const obj = await response.json();
|
||||
return obj;
|
||||
}
|
||||
throw new Error(response.status);
|
||||
}
|
||||
|
||||
export async function metadata(id, keychain) {
|
||||
const result = await fetchWithAuthAndRetry(
|
||||
`/api/metadata/${id}`,
|
||||
{ method: 'GET' },
|
||||
keychain
|
||||
);
|
||||
if (result.ok) {
|
||||
const data = await result.response.json();
|
||||
const meta = await keychain.decryptMetadata(b64ToArray(data.metadata));
|
||||
return {
|
||||
size: data.size,
|
||||
ttl: data.ttl,
|
||||
iv: meta.iv,
|
||||
name: meta.name,
|
||||
type: meta.type
|
||||
};
|
||||
}
|
||||
throw new Error(result.response.status);
|
||||
}
|
||||
|
||||
export async function setPassword(id, owner_token, keychain) {
|
||||
const auth = await keychain.authKeyB64();
|
||||
const response = await fetch(
|
||||
`/api/password/${id}`,
|
||||
post({ owner_token, auth })
|
||||
);
|
||||
return response.ok;
|
||||
}
|
||||
|
||||
export function uploadFile(encrypted, metadata, verifierB64, keychain) {
|
||||
const xhr = new XMLHttpRequest();
|
||||
const upload = {
|
||||
onprogress: function() {},
|
||||
cancel: function() {
|
||||
xhr.abort();
|
||||
},
|
||||
result: new Promise(function(resolve, reject) {
|
||||
xhr.addEventListener('loadend', function() {
|
||||
const authHeader = xhr.getResponseHeader('WWW-Authenticate');
|
||||
if (authHeader) {
|
||||
keychain.nonce = parseNonce(authHeader);
|
||||
}
|
||||
if (xhr.status === 200) {
|
||||
const responseObj = JSON.parse(xhr.responseText);
|
||||
return resolve({
|
||||
url: responseObj.url,
|
||||
id: responseObj.id,
|
||||
ownerToken: responseObj.owner
|
||||
});
|
||||
}
|
||||
reject(new Error(xhr.status));
|
||||
});
|
||||
})
|
||||
};
|
||||
const dataView = new DataView(encrypted);
|
||||
const blob = new Blob([dataView], { type: 'application/octet-stream' });
|
||||
const fd = new FormData();
|
||||
fd.append('data', blob);
|
||||
xhr.upload.addEventListener('progress', function(event) {
|
||||
if (event.lengthComputable) {
|
||||
upload.onprogress([event.loaded, event.total]);
|
||||
}
|
||||
});
|
||||
xhr.open('post', '/api/upload', true);
|
||||
xhr.setRequestHeader('X-File-Metadata', arrayToB64(new Uint8Array(metadata)));
|
||||
xhr.setRequestHeader('Authorization', `send-v1 ${verifierB64}`);
|
||||
xhr.send(fd);
|
||||
return upload;
|
||||
}
|
||||
|
||||
function download(id, keychain) {
|
||||
const xhr = new XMLHttpRequest();
|
||||
const download = {
|
||||
onprogress: function() {},
|
||||
cancel: function() {
|
||||
xhr.abort();
|
||||
},
|
||||
result: new Promise(async function(resolve, reject) {
|
||||
xhr.addEventListener('loadend', function() {
|
||||
const authHeader = xhr.getResponseHeader('WWW-Authenticate');
|
||||
if (authHeader) {
|
||||
keychain.nonce = parseNonce(authHeader);
|
||||
}
|
||||
if (xhr.status === 404) {
|
||||
return reject(new Error('notfound'));
|
||||
}
|
||||
if (xhr.status !== 200) {
|
||||
return reject(new Error(xhr.status));
|
||||
}
|
||||
|
||||
const blob = new Blob([xhr.response]);
|
||||
const fileReader = new FileReader();
|
||||
fileReader.readAsArrayBuffer(blob);
|
||||
fileReader.onload = function() {
|
||||
resolve(this.result);
|
||||
};
|
||||
});
|
||||
xhr.addEventListener('progress', function(event) {
|
||||
if (event.lengthComputable && event.target.status === 200) {
|
||||
download.onprogress([event.loaded, event.total]);
|
||||
}
|
||||
});
|
||||
const auth = await keychain.authHeader();
|
||||
xhr.open('get', `/api/download/${id}`);
|
||||
xhr.setRequestHeader('Authorization', auth);
|
||||
xhr.responseType = 'blob';
|
||||
xhr.send();
|
||||
})
|
||||
};
|
||||
|
||||
return download;
|
||||
}
|
||||
|
||||
async function tryDownload(id, keychain, onprogress, tries = 1) {
|
||||
const dl = download(id, keychain);
|
||||
dl.onprogress = onprogress;
|
||||
try {
|
||||
const result = await dl.result;
|
||||
return result;
|
||||
} catch (e) {
|
||||
if (e.message === '401' && --tries > 0) {
|
||||
return tryDownload(id, keychain, onprogress, tries);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export function downloadFile(id, keychain) {
|
||||
let cancelled = false;
|
||||
function updateProgress(p) {
|
||||
if (cancelled) {
|
||||
// This is a bit of a hack
|
||||
// We piggyback off of the progress event as a chance to cancel.
|
||||
// Otherwise wiring the xhr abort up while allowing retries
|
||||
// gets pretty nasty.
|
||||
// 'this' here is the object returned by download(id, keychain)
|
||||
return this.cancel();
|
||||
}
|
||||
dl.onprogress(p);
|
||||
}
|
||||
const dl = {
|
||||
onprogress: function() {},
|
||||
cancel: function() {
|
||||
cancelled = true;
|
||||
},
|
||||
result: tryDownload(id, keychain, updateProgress, 2)
|
||||
};
|
||||
return dl;
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
/* global MAXFILESIZE */
|
||||
const { bytes } = require('./utils');
|
||||
|
||||
export default function(state, emitter) {
|
||||
emitter.on('DOMContentLoaded', () => {
|
||||
document.body.addEventListener('dragover', event => {
|
||||
@@ -6,17 +9,26 @@ export default function(state, emitter) {
|
||||
}
|
||||
});
|
||||
document.body.addEventListener('drop', event => {
|
||||
if (state.route === '/' && !state.transfer) {
|
||||
if (state.route === '/' && !state.uploading) {
|
||||
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) {
|
||||
if (target.files.length > 1) {
|
||||
return alert(state.translate('uploadPageMultipleFilesAlert'));
|
||||
}
|
||||
const file = target.files[0];
|
||||
if (file.size === 0) {
|
||||
return;
|
||||
}
|
||||
if (file.size > MAXFILESIZE) {
|
||||
window.alert(
|
||||
state.translate('fileTooBig', { size: bytes(MAXFILESIZE) })
|
||||
);
|
||||
return;
|
||||
}
|
||||
emitter.emit('upload', { file, type: 'drop' });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,28 +1,33 @@
|
||||
import hash from 'string-hash';
|
||||
|
||||
const experiments = {
|
||||
'5YHCzn2CQTmBwWwTmZupBA': {
|
||||
id: '5YHCzn2CQTmBwWwTmZupBA',
|
||||
S9wqVl2SQ4ab2yZtqDI3Dw: {
|
||||
id: 'S9wqVl2SQ4ab2yZtqDI3Dw',
|
||||
run: function(variant, state, emitter) {
|
||||
state.experiment = {
|
||||
xid: this.id,
|
||||
xvar: variant
|
||||
};
|
||||
// Beefy UI
|
||||
if (variant === 1) {
|
||||
state.config.uploadWindowStyle = 'upload-window upload-window-b';
|
||||
state.config.uploadButtonStyle = 'btn browse browse-b';
|
||||
} else {
|
||||
state.config.uploadWindowStyle = 'upload-window';
|
||||
state.config.uploadButtonStyle = 'btn browse';
|
||||
switch (variant) {
|
||||
case 1:
|
||||
state.promo = 'blue';
|
||||
break;
|
||||
case 2:
|
||||
state.promo = 'pink';
|
||||
break;
|
||||
default:
|
||||
state.promo = 'grey';
|
||||
}
|
||||
emitter.emit('render');
|
||||
},
|
||||
eligible: function(state) {
|
||||
return this.luckyNumber(state) >= 0.5;
|
||||
eligible: function() {
|
||||
return (
|
||||
!/firefox/i.test(navigator.userAgent) &&
|
||||
document.querySelector('html').lang === 'en-US'
|
||||
);
|
||||
},
|
||||
variant: function(state) {
|
||||
return this.luckyNumber(state) < 0.5 ? 0 : 1;
|
||||
const n = this.luckyNumber(state);
|
||||
if (n < 0.33) {
|
||||
return 0;
|
||||
}
|
||||
return n < 0.66 ? 1 : 2;
|
||||
},
|
||||
luckyNumber: function(state) {
|
||||
return luckyNumber(
|
||||
@@ -33,6 +38,7 @@ const experiments = {
|
||||
};
|
||||
|
||||
//Returns a number between 0 and 1
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function luckyNumber(str) {
|
||||
return hash(str) / 0xffffffff;
|
||||
}
|
||||
@@ -61,12 +67,12 @@ export default function initialize(state, emitter) {
|
||||
checkExperiments(state, emitter);
|
||||
});
|
||||
} else {
|
||||
const enrolled = state.storage.enrolled;
|
||||
enrolled.forEach(([id, variant]) => {
|
||||
const enrolled = state.storage.enrolled.filter(([id, variant]) => {
|
||||
const xp = experiments[id];
|
||||
if (xp) {
|
||||
xp.run(variant, state, emitter);
|
||||
}
|
||||
return !!xp;
|
||||
});
|
||||
// single experiment per session for now
|
||||
if (enrolled.length === 0) {
|
||||
|
||||
@@ -1,57 +1,15 @@
|
||||
/* global EXPIRE_SECONDS */
|
||||
import FileSender from './fileSender';
|
||||
import FileReceiver from './fileReceiver';
|
||||
import { copyToClipboard, delay, fadeOut, percent } from './utils';
|
||||
import {
|
||||
copyToClipboard,
|
||||
delay,
|
||||
fadeOut,
|
||||
openLinksInNewTab,
|
||||
percent,
|
||||
saveFile
|
||||
} 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;
|
||||
@@ -64,10 +22,14 @@ export default function(state, emitter) {
|
||||
const files = state.storage.files;
|
||||
let rerender = false;
|
||||
for (const file of files) {
|
||||
const ok = await exists(file.id);
|
||||
if (!ok) {
|
||||
const oldLimit = file.dlimit;
|
||||
const oldTotal = file.dtotal;
|
||||
await file.updateDownloadCount();
|
||||
if (file.dtotal === file.dlimit) {
|
||||
state.storage.remove(file.id);
|
||||
rerender = true;
|
||||
} else if (oldLimit !== file.dlimit || oldTotal !== file.dtotal) {
|
||||
rerender = true;
|
||||
}
|
||||
}
|
||||
if (rerender) {
|
||||
@@ -97,6 +59,12 @@ export default function(state, emitter) {
|
||||
lastRender = Date.now();
|
||||
});
|
||||
|
||||
emitter.on('changeLimit', async ({ file, value }) => {
|
||||
await file.changeLimit(value);
|
||||
state.storage.writeFile(file);
|
||||
metrics.changedDownloadLimit(file);
|
||||
});
|
||||
|
||||
emitter.on('delete', async ({ file, location }) => {
|
||||
try {
|
||||
metrics.deletedUpload({
|
||||
@@ -108,11 +76,10 @@ export default function(state, emitter) {
|
||||
location
|
||||
});
|
||||
state.storage.remove(file.id);
|
||||
await FileSender.delete(file.id, file.deleteToken);
|
||||
await file.del();
|
||||
} catch (e) {
|
||||
state.raven.captureException(e);
|
||||
}
|
||||
state.fileInfo = null;
|
||||
});
|
||||
|
||||
emitter.on('cancel', () => {
|
||||
@@ -125,34 +92,28 @@ export default function(state, emitter) {
|
||||
sender.on('progress', updateProgress);
|
||||
sender.on('encrypting', render);
|
||||
state.transfer = sender;
|
||||
state.uploading = true;
|
||||
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 });
|
||||
const ownedFile = await sender.upload();
|
||||
ownedFile.type = type;
|
||||
state.storage.totalUploads += 1;
|
||||
metrics.completedUpload(ownedFile);
|
||||
|
||||
state.storage.addFile(ownedFile);
|
||||
|
||||
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}`);
|
||||
emitter.emit('pushState', `/share/${ownedFile.id}`);
|
||||
} catch (err) {
|
||||
state.transfer = null;
|
||||
console.error(err);
|
||||
|
||||
if (err.message === '0') {
|
||||
//cancelled. do nothing
|
||||
metrics.cancelledUpload({ size, type });
|
||||
@@ -160,41 +121,76 @@ export default function(state, emitter) {
|
||||
}
|
||||
state.raven.captureException(err);
|
||||
metrics.stoppedUpload({ size, type, err });
|
||||
emitter.emit('replaceState', '/error');
|
||||
emitter.emit('pushState', '/error');
|
||||
} finally {
|
||||
state.uploading = false;
|
||||
state.transfer = null;
|
||||
}
|
||||
});
|
||||
|
||||
emitter.on('download', async file => {
|
||||
const size = file.size;
|
||||
const url = `/api/download/${file.id}`;
|
||||
const receiver = new FileReceiver(url, file.key);
|
||||
receiver.on('progress', updateProgress);
|
||||
receiver.on('decrypting', render);
|
||||
state.transfer = receiver;
|
||||
const links = openLinksInNewTab();
|
||||
emitter.on('password', async ({ password, file }) => {
|
||||
try {
|
||||
await file.setPassword(password);
|
||||
state.storage.writeFile(file);
|
||||
metrics.addedPassword({ size: file.size });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
render();
|
||||
});
|
||||
|
||||
emitter.on('getMetadata', async () => {
|
||||
const file = state.fileInfo;
|
||||
const receiver = new FileReceiver(file);
|
||||
try {
|
||||
await receiver.getMetadata();
|
||||
receiver.on('progress', updateProgress);
|
||||
receiver.on('decrypting', render);
|
||||
state.transfer = receiver;
|
||||
} catch (e) {
|
||||
if (e.message === '401') {
|
||||
file.password = null;
|
||||
if (!file.requiresPassword) {
|
||||
return emitter.emit('pushState', '/404');
|
||||
}
|
||||
}
|
||||
}
|
||||
render();
|
||||
});
|
||||
|
||||
emitter.on('download', async file => {
|
||||
state.transfer.on('progress', 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 receiver.download();
|
||||
const f = await state.transfer.download();
|
||||
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.reset();
|
||||
metrics.completedDownload({ size, time, speed });
|
||||
emitter.emit('pushState', '/completed');
|
||||
} catch (err) {
|
||||
// TODO cancelled download
|
||||
if (err.message === '0') {
|
||||
// download cancelled
|
||||
state.transfer.reset();
|
||||
return render();
|
||||
}
|
||||
console.error(err);
|
||||
state.transfer = null;
|
||||
const location = err.message === 'notfound' ? '/404' : '/error';
|
||||
if (location === '/error') {
|
||||
state.raven.captureException(err);
|
||||
metrics.stoppedDownload({ size, err });
|
||||
}
|
||||
emitter.emit('replaceState', location);
|
||||
emitter.emit('pushState', location);
|
||||
} finally {
|
||||
state.transfer = null;
|
||||
openLinksInNewTab(links, false);
|
||||
}
|
||||
});
|
||||
@@ -204,6 +200,14 @@ export default function(state, emitter) {
|
||||
metrics.copiedLink({ location });
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
// poll for updates of the download counts
|
||||
// TODO something for the share page: || state.route === '/share/:id'
|
||||
if (state.route === '/') {
|
||||
checkFiles();
|
||||
}
|
||||
}, 2 * 60 * 1000);
|
||||
|
||||
setInterval(() => {
|
||||
// poll for rerendering the file list countdown timers
|
||||
if (
|
||||
|
||||
@@ -1,26 +1,17 @@
|
||||
import Nanobus from 'nanobus';
|
||||
import { hexToArray, bytes } from './utils';
|
||||
import Keychain from './keychain';
|
||||
import { bytes } from './utils';
|
||||
import { metadata, downloadFile } from './api';
|
||||
|
||||
export default class FileReceiver extends Nanobus {
|
||||
constructor(url, k) {
|
||||
constructor(fileInfo) {
|
||||
super('FileReceiver');
|
||||
this.key = window.crypto.subtle.importKey(
|
||||
'jwk',
|
||||
{
|
||||
k,
|
||||
kty: 'oct',
|
||||
alg: 'A128GCM',
|
||||
ext: true
|
||||
},
|
||||
{
|
||||
name: 'AES-GCM'
|
||||
},
|
||||
false,
|
||||
['decrypt']
|
||||
);
|
||||
this.url = url;
|
||||
this.msg = 'fileSizeProgress';
|
||||
this.progress = [0, 1];
|
||||
this.keychain = new Keychain(fileInfo.secretKey, fileInfo.nonce);
|
||||
if (fileInfo.requiresPassword) {
|
||||
this.keychain.setPassword(fileInfo.password, fileInfo.url);
|
||||
}
|
||||
this.fileInfo = fileInfo;
|
||||
this.reset();
|
||||
}
|
||||
|
||||
get progressRatio() {
|
||||
@@ -35,66 +26,64 @@ export default class FileReceiver extends Nanobus {
|
||||
}
|
||||
|
||||
cancel() {
|
||||
// TODO
|
||||
this.cancelled = true;
|
||||
if (this.fileDownload) {
|
||||
this.fileDownload.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
downloadFile() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
reset() {
|
||||
this.fileDownload = null;
|
||||
this.msg = 'fileSizeProgress';
|
||||
this.state = 'initialized';
|
||||
this.progress = [0, 1];
|
||||
this.cancelled = false;
|
||||
}
|
||||
|
||||
xhr.onprogress = event => {
|
||||
if (event.lengthComputable && event.target.status !== 404) {
|
||||
this.progress = [event.loaded, event.total];
|
||||
this.emit('progress', this.progress);
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onload = function(event) {
|
||||
if (xhr.status === 404) {
|
||||
reject(new Error('notfound'));
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = new Blob([this.response]);
|
||||
const meta = JSON.parse(xhr.getResponseHeader('X-File-Metadata'));
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = function() {
|
||||
resolve({
|
||||
data: this.result,
|
||||
name: meta.filename,
|
||||
type: meta.mimeType,
|
||||
iv: meta.id
|
||||
});
|
||||
};
|
||||
|
||||
fileReader.readAsArrayBuffer(blob);
|
||||
};
|
||||
|
||||
xhr.open('get', this.url);
|
||||
xhr.responseType = 'blob';
|
||||
xhr.send();
|
||||
});
|
||||
async getMetadata() {
|
||||
const meta = await metadata(this.fileInfo.id, this.keychain);
|
||||
if (meta) {
|
||||
this.keychain.setIV(meta.iv);
|
||||
this.fileInfo.name = meta.name;
|
||||
this.fileInfo.type = meta.type;
|
||||
this.fileInfo.iv = meta.iv;
|
||||
this.fileInfo.size = meta.size;
|
||||
this.state = 'ready';
|
||||
return;
|
||||
}
|
||||
this.state = 'invalid';
|
||||
return;
|
||||
}
|
||||
|
||||
async download() {
|
||||
const key = await this.key;
|
||||
const file = await this.downloadFile();
|
||||
this.msg = 'decryptingFile';
|
||||
this.emit('decrypting');
|
||||
const plaintext = await window.crypto.subtle.decrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: hexToArray(file.iv),
|
||||
tagLength: 128
|
||||
},
|
||||
key,
|
||||
file.data
|
||||
);
|
||||
this.msg = 'downloadFinish';
|
||||
return {
|
||||
plaintext,
|
||||
name: decodeURIComponent(file.name),
|
||||
type: file.type
|
||||
};
|
||||
this.state = 'downloading';
|
||||
this.emit('progress', this.progress);
|
||||
try {
|
||||
const download = await downloadFile(this.fileInfo.id, this.keychain);
|
||||
download.onprogress = p => {
|
||||
this.progress = p;
|
||||
this.emit('progress', p);
|
||||
};
|
||||
this.fileDownload = download;
|
||||
const ciphertext = await download.result;
|
||||
this.fileDownload = null;
|
||||
this.msg = 'decryptingFile';
|
||||
this.state = 'decrypting';
|
||||
this.emit('decrypting');
|
||||
const plaintext = await this.keychain.decryptFile(ciphertext);
|
||||
if (this.cancelled) {
|
||||
throw new Error(0);
|
||||
}
|
||||
this.msg = 'downloadFinish';
|
||||
this.state = 'complete';
|
||||
return {
|
||||
plaintext,
|
||||
name: decodeURIComponent(this.fileInfo.name),
|
||||
type: this.fileInfo.type
|
||||
};
|
||||
} catch (e) {
|
||||
this.state = 'invalid';
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +1,19 @@
|
||||
/* global EXPIRE_SECONDS */
|
||||
import Nanobus from 'nanobus';
|
||||
import { arrayToHex, bytes } from './utils';
|
||||
import OwnedFile from './ownedFile';
|
||||
import Keychain from './keychain';
|
||||
import { arrayToB64, bytes } from './utils';
|
||||
import { uploadFile } from './api';
|
||||
|
||||
export default class FileSender extends Nanobus {
|
||||
constructor(file) {
|
||||
super('FileSender');
|
||||
this.file = file;
|
||||
this.uploadRequest = null;
|
||||
this.msg = 'importingFile';
|
||||
this.progress = [0, 1];
|
||||
this.cancelled = false;
|
||||
this.iv = window.crypto.getRandomValues(new Uint8Array(12));
|
||||
this.uploadXHR = new XMLHttpRequest();
|
||||
this.key = window.crypto.subtle.generateKey(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
length: 128
|
||||
},
|
||||
true,
|
||||
['encrypt']
|
||||
);
|
||||
}
|
||||
|
||||
static delete(id, token) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!id || !token) {
|
||||
return reject();
|
||||
}
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', `/api/delete/${id}`);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
xhr.send(JSON.stringify({ delete_token: token }));
|
||||
});
|
||||
this.keychain = new Keychain();
|
||||
}
|
||||
|
||||
get progressRatio() {
|
||||
@@ -52,8 +29,8 @@ export default class FileSender extends Nanobus {
|
||||
|
||||
cancel() {
|
||||
this.cancelled = true;
|
||||
if (this.msg === 'fileSizeProgress') {
|
||||
this.uploadXHR.abort();
|
||||
if (this.uploadRequest) {
|
||||
this.uploadRequest.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +38,7 @@ export default class FileSender extends Nanobus {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsArrayBuffer(this.file);
|
||||
// TODO: progress?
|
||||
reader.onload = function(event) {
|
||||
const plaintext = new Uint8Array(this.result);
|
||||
resolve(plaintext);
|
||||
@@ -71,76 +49,56 @@ export default class FileSender extends Nanobus {
|
||||
});
|
||||
}
|
||||
|
||||
uploadFile(encrypted, keydata) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const file = this.file;
|
||||
const id = arrayToHex(this.iv);
|
||||
const dataView = new DataView(encrypted);
|
||||
const blob = new Blob([dataView], { type: file.type });
|
||||
const fd = new FormData();
|
||||
fd.append('data', blob, file.name);
|
||||
|
||||
const xhr = this.uploadXHR;
|
||||
|
||||
xhr.upload.addEventListener('progress', e => {
|
||||
if (e.lengthComputable) {
|
||||
this.progress = [e.loaded, e.total];
|
||||
this.emit('progress', this.progress);
|
||||
}
|
||||
});
|
||||
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
if (xhr.status === 200) {
|
||||
this.progress = [1, 1];
|
||||
this.msg = 'notifyUploadDone';
|
||||
const responseObj = JSON.parse(xhr.responseText);
|
||||
return resolve({
|
||||
url: responseObj.url,
|
||||
id: responseObj.id,
|
||||
secretKey: keydata.k,
|
||||
deleteToken: responseObj.delete
|
||||
});
|
||||
}
|
||||
this.msg = 'errorPageHeader';
|
||||
reject(new Error(xhr.status));
|
||||
}
|
||||
};
|
||||
|
||||
xhr.open('post', '/api/upload', true);
|
||||
xhr.setRequestHeader(
|
||||
'X-File-Metadata',
|
||||
JSON.stringify({
|
||||
id: id,
|
||||
filename: encodeURIComponent(file.name)
|
||||
})
|
||||
);
|
||||
xhr.send(fd);
|
||||
this.msg = 'fileSizeProgress';
|
||||
});
|
||||
}
|
||||
|
||||
async upload() {
|
||||
const key = await this.key;
|
||||
const start = Date.now();
|
||||
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
|
||||
},
|
||||
key,
|
||||
plaintext
|
||||
);
|
||||
const encrypted = await this.keychain.encryptFile(plaintext);
|
||||
const metadata = await this.keychain.encryptMetadata(this.file);
|
||||
const authKeyB64 = await this.keychain.authKeyB64();
|
||||
if (this.cancelled) {
|
||||
throw new Error(0);
|
||||
}
|
||||
const keydata = await window.crypto.subtle.exportKey('jwk', key);
|
||||
return this.uploadFile(encrypted, keydata);
|
||||
this.uploadRequest = uploadFile(
|
||||
encrypted,
|
||||
metadata,
|
||||
authKeyB64,
|
||||
this.keychain
|
||||
);
|
||||
this.msg = 'fileSizeProgress';
|
||||
this.uploadRequest.onprogress = p => {
|
||||
this.progress = p;
|
||||
this.emit('progress', p);
|
||||
};
|
||||
try {
|
||||
const result = await this.uploadRequest.result;
|
||||
const time = Date.now() - start;
|
||||
this.msg = 'notifyUploadDone';
|
||||
this.uploadRequest = null;
|
||||
this.progress = [1, 1];
|
||||
const secretKey = arrayToB64(this.keychain.rawSecret);
|
||||
const ownedFile = new OwnedFile({
|
||||
id: result.id,
|
||||
url: `${result.url}#${secretKey}`,
|
||||
name: this.file.name,
|
||||
size: this.file.size,
|
||||
time: time,
|
||||
speed: this.file.size / (time / 1000),
|
||||
createdAt: Date.now(),
|
||||
expiresAt: Date.now() + EXPIRE_SECONDS * 1000,
|
||||
secretKey: secretKey,
|
||||
nonce: this.keychain.nonce,
|
||||
ownerToken: result.ownerToken
|
||||
});
|
||||
return ownedFile;
|
||||
} catch (e) {
|
||||
this.msg = 'errorPageHeader';
|
||||
this.uploadRequest = null;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
209
app/keychain.js
Normal file
@@ -0,0 +1,209 @@
|
||||
import { arrayToB64, b64ToArray } from './utils';
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
export default class Keychain {
|
||||
constructor(secretKeyB64, nonce, ivB64) {
|
||||
this._nonce = nonce || 'yRCdyQ1EMSA3mo4rqSkuNQ==';
|
||||
if (ivB64) {
|
||||
this.iv = b64ToArray(ivB64);
|
||||
} else {
|
||||
this.iv = window.crypto.getRandomValues(new Uint8Array(12));
|
||||
}
|
||||
if (secretKeyB64) {
|
||||
this.rawSecret = b64ToArray(secretKeyB64);
|
||||
} else {
|
||||
this.rawSecret = window.crypto.getRandomValues(new Uint8Array(16));
|
||||
}
|
||||
this.secretKeyPromise = window.crypto.subtle.importKey(
|
||||
'raw',
|
||||
this.rawSecret,
|
||||
'HKDF',
|
||||
false,
|
||||
['deriveKey']
|
||||
);
|
||||
this.encryptKeyPromise = this.secretKeyPromise.then(function(secretKey) {
|
||||
return window.crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'HKDF',
|
||||
salt: new Uint8Array(),
|
||||
info: encoder.encode('encryption'),
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
secretKey,
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
length: 128
|
||||
},
|
||||
false,
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
});
|
||||
this.metaKeyPromise = this.secretKeyPromise.then(function(secretKey) {
|
||||
return window.crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'HKDF',
|
||||
salt: new Uint8Array(),
|
||||
info: encoder.encode('metadata'),
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
secretKey,
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
length: 128
|
||||
},
|
||||
false,
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
});
|
||||
this.authKeyPromise = this.secretKeyPromise.then(function(secretKey) {
|
||||
return window.crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'HKDF',
|
||||
salt: new Uint8Array(),
|
||||
info: encoder.encode('authentication'),
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
secretKey,
|
||||
{
|
||||
name: 'HMAC',
|
||||
hash: { name: 'SHA-256' }
|
||||
},
|
||||
true,
|
||||
['sign']
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
get nonce() {
|
||||
return this._nonce;
|
||||
}
|
||||
|
||||
set nonce(n) {
|
||||
if (n && n !== this._nonce) {
|
||||
this._nonce = n;
|
||||
}
|
||||
}
|
||||
|
||||
setIV(ivB64) {
|
||||
this.iv = b64ToArray(ivB64);
|
||||
}
|
||||
|
||||
setPassword(password, shareUrl) {
|
||||
this.authKeyPromise = window.crypto.subtle
|
||||
.importKey('raw', encoder.encode(password), { name: 'PBKDF2' }, false, [
|
||||
'deriveKey'
|
||||
])
|
||||
.then(passwordKey =>
|
||||
window.crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'PBKDF2',
|
||||
salt: encoder.encode(shareUrl),
|
||||
iterations: 100,
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
passwordKey,
|
||||
{
|
||||
name: 'HMAC',
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
true,
|
||||
['sign']
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
setAuthKey(authKeyB64) {
|
||||
this.authKeyPromise = window.crypto.subtle.importKey(
|
||||
'raw',
|
||||
b64ToArray(authKeyB64),
|
||||
{
|
||||
name: 'HMAC',
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
true,
|
||||
['sign']
|
||||
);
|
||||
}
|
||||
|
||||
async authKeyB64() {
|
||||
const authKey = await this.authKeyPromise;
|
||||
const rawAuth = await window.crypto.subtle.exportKey('raw', authKey);
|
||||
return arrayToB64(new Uint8Array(rawAuth));
|
||||
}
|
||||
|
||||
async authHeader() {
|
||||
const authKey = await this.authKeyPromise;
|
||||
const sig = await window.crypto.subtle.sign(
|
||||
{
|
||||
name: 'HMAC'
|
||||
},
|
||||
authKey,
|
||||
b64ToArray(this.nonce)
|
||||
);
|
||||
return `send-v1 ${arrayToB64(new Uint8Array(sig))}`;
|
||||
}
|
||||
|
||||
async encryptFile(plaintext) {
|
||||
const encryptKey = await this.encryptKeyPromise;
|
||||
const ciphertext = await window.crypto.subtle.encrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: this.iv,
|
||||
tagLength: 128
|
||||
},
|
||||
encryptKey,
|
||||
plaintext
|
||||
);
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
async encryptMetadata(metadata) {
|
||||
const metaKey = await this.metaKeyPromise;
|
||||
const ciphertext = await window.crypto.subtle.encrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: new Uint8Array(12),
|
||||
tagLength: 128
|
||||
},
|
||||
metaKey,
|
||||
encoder.encode(
|
||||
JSON.stringify({
|
||||
iv: arrayToB64(this.iv),
|
||||
name: metadata.name,
|
||||
type: metadata.type || 'application/octet-stream'
|
||||
})
|
||||
)
|
||||
);
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
async decryptFile(ciphertext) {
|
||||
const encryptKey = await this.encryptKeyPromise;
|
||||
const plaintext = await window.crypto.subtle.decrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: this.iv,
|
||||
tagLength: 128
|
||||
},
|
||||
encryptKey,
|
||||
ciphertext
|
||||
);
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
async decryptMetadata(ciphertext) {
|
||||
const metaKey = await this.metaKeyPromise;
|
||||
const plaintext = await window.crypto.subtle.decrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: new Uint8Array(12),
|
||||
tagLength: 128
|
||||
},
|
||||
metaKey,
|
||||
ciphertext
|
||||
);
|
||||
return JSON.parse(decoder.decode(plaintext));
|
||||
}
|
||||
}
|
||||
34
app/main.js
@@ -1,5 +1,5 @@
|
||||
import 'fluent-intl-polyfill';
|
||||
import app from './routes';
|
||||
import log from 'choo-log';
|
||||
import locale from '../common/locales';
|
||||
import fileManager from './fileManager';
|
||||
import dragManager from './dragManager';
|
||||
@@ -14,31 +14,35 @@ if (navigator.doNotTrack !== '1' && window.RAVEN_CONFIG) {
|
||||
Raven.config(window.SENTRY_ID, window.RAVEN_CONFIG).install();
|
||||
}
|
||||
|
||||
app.use(log());
|
||||
|
||||
app.use((state, emitter) => {
|
||||
// init state
|
||||
state.transfer = null;
|
||||
state.fileInfo = null;
|
||||
state.translate = locale.getTranslator();
|
||||
state.storage = storage;
|
||||
state.raven = Raven;
|
||||
state.config = {
|
||||
uploadWindowStyle: 'upload-window',
|
||||
uploadButtonStyle: 'browse btn'
|
||||
};
|
||||
emitter.on('DOMContentLoaded', async () => {
|
||||
window.appState = state;
|
||||
emitter.on('DOMContentLoaded', async function checkSupport() {
|
||||
let unsupportedReason = null;
|
||||
if (
|
||||
// Firefox < 50
|
||||
/firefox/i.test(navigator.userAgent) &&
|
||||
parseInt(navigator.userAgent.match(/firefox\/*([^\n\r]*)\./i)[1], 10) <=
|
||||
49
|
||||
parseInt(navigator.userAgent.match(/firefox\/*([^\n\r]*)\./i)[1], 10) < 50
|
||||
) {
|
||||
return emitter.emit('replaceState', '/unsupported/outdated');
|
||||
unsupportedReason = 'outdated';
|
||||
}
|
||||
if (/edge\/\d+/i.test(navigator.userAgent)) {
|
||||
unsupportedReason = 'edge';
|
||||
}
|
||||
const ok = await canHasSend(assets.get('cryptofill.js'));
|
||||
if (!ok) {
|
||||
const reason = /firefox/i.test(navigator.userAgent) ? 'outdated' : 'gcm';
|
||||
emitter.emit('replaceState', `/unsupported/${reason}`);
|
||||
unsupportedReason = /firefox/i.test(navigator.userAgent)
|
||||
? 'outdated'
|
||||
: 'gcm';
|
||||
}
|
||||
if (unsupportedReason) {
|
||||
setTimeout(() =>
|
||||
emitter.emit('replaceState', `/unsupported/${unsupportedReason}`)
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -48,4 +52,4 @@ app.use(fileManager);
|
||||
app.use(dragManager);
|
||||
app.use(experiments);
|
||||
|
||||
app.mount('#page-one');
|
||||
app.mount('body');
|
||||
|
||||
@@ -29,6 +29,8 @@ export default function initialize(state, emitter) {
|
||||
});
|
||||
//TODO restart handlers... somewhere
|
||||
});
|
||||
emitter.on('exit', exitEvent);
|
||||
emitter.on('experiment', experimentEvent);
|
||||
}
|
||||
|
||||
function category() {
|
||||
@@ -81,6 +83,8 @@ function urlToMetric(url) {
|
||||
case 'https://testpilot.firefox.com/':
|
||||
case 'https://testpilot.firefox.com/experiments/send':
|
||||
return 'testpilot';
|
||||
case 'https://www.mozilla.org/firefox/new/?utm_campaign=send-acquisition&utm_medium=referral&utm_source=send.firefox.com':
|
||||
return 'promo';
|
||||
default:
|
||||
return 'other';
|
||||
}
|
||||
@@ -147,6 +151,15 @@ function completedUpload(params) {
|
||||
});
|
||||
}
|
||||
|
||||
function addedPassword(params) {
|
||||
return sendEvent('sender', 'password-added', {
|
||||
cm1: params.size,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: storage.files.length,
|
||||
cm7: storage.totalDownloads
|
||||
});
|
||||
}
|
||||
|
||||
function startedDownload(params) {
|
||||
return sendEvent('recipient', 'download-started', {
|
||||
cm1: params.size,
|
||||
@@ -191,6 +204,16 @@ function stoppedUpload(params) {
|
||||
});
|
||||
}
|
||||
|
||||
function changedDownloadLimit(params) {
|
||||
return sendEvent('sender', 'download-limit-changed', {
|
||||
cm1: params.size,
|
||||
cm5: storage.totalUploads,
|
||||
cm6: storage.files.length,
|
||||
cm7: storage.totalDownloads,
|
||||
cm8: params.dlimit
|
||||
});
|
||||
}
|
||||
|
||||
function completedDownload(params) {
|
||||
return sendEvent('recipient', 'download-stopped', {
|
||||
cm1: params.size,
|
||||
@@ -235,6 +258,11 @@ function exitEvent(target) {
|
||||
});
|
||||
}
|
||||
|
||||
function experimentEvent(params) {
|
||||
return sendEvent(category(), 'experiment', params);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function addExitHandlers() {
|
||||
const links = Array.from(document.querySelectorAll('a'));
|
||||
links.forEach(l => {
|
||||
@@ -257,11 +285,13 @@ export {
|
||||
cancelledUpload,
|
||||
stoppedUpload,
|
||||
completedUpload,
|
||||
changedDownloadLimit,
|
||||
deletedUpload,
|
||||
startedDownload,
|
||||
cancelledDownload,
|
||||
stoppedDownload,
|
||||
completedDownload,
|
||||
addedPassword,
|
||||
restart,
|
||||
unsupported
|
||||
};
|
||||
|
||||
77
app/ownedFile.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import Keychain from './keychain';
|
||||
import { arrayToB64 } from './utils';
|
||||
import { del, fileInfo, setParams, setPassword } from './api';
|
||||
|
||||
export default class OwnedFile {
|
||||
constructor(obj) {
|
||||
this.id = obj.id;
|
||||
this.url = obj.url;
|
||||
this.name = obj.name;
|
||||
this.size = obj.size;
|
||||
this.type = obj.type;
|
||||
this.time = obj.time;
|
||||
this.speed = obj.speed;
|
||||
this.createdAt = obj.createdAt;
|
||||
this.expiresAt = obj.expiresAt;
|
||||
this.ownerToken = obj.ownerToken;
|
||||
this.dlimit = obj.dlimit || 1;
|
||||
this.dtotal = obj.dtotal || 0;
|
||||
this.keychain = new Keychain(obj.secretKey);
|
||||
this._hasPassword = !!obj.hasPassword;
|
||||
}
|
||||
|
||||
async setPassword(password) {
|
||||
this.password = password;
|
||||
this._hasPassword = true;
|
||||
this.keychain.setPassword(password, this.url);
|
||||
const result = await setPassword(this.id, this.ownerToken, this.keychain);
|
||||
return result;
|
||||
}
|
||||
|
||||
del() {
|
||||
return del(this.id, this.ownerToken);
|
||||
}
|
||||
|
||||
changeLimit(dlimit) {
|
||||
if (this.dlimit !== dlimit) {
|
||||
this.dlimit = dlimit;
|
||||
return setParams(this.id, this.ownerToken, { dlimit });
|
||||
}
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
get hasPassword() {
|
||||
return !!this._hasPassword;
|
||||
}
|
||||
|
||||
async updateDownloadCount() {
|
||||
try {
|
||||
const result = await fileInfo(this.id, this.ownerToken);
|
||||
this.dtotal = result.dtotal;
|
||||
this.dlimit = result.dlimit;
|
||||
} catch (e) {
|
||||
if (e.message === '404') {
|
||||
this.dtotal = this.dlimit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
url: this.url,
|
||||
name: this.name,
|
||||
size: this.size,
|
||||
type: this.type,
|
||||
time: this.time,
|
||||
speed: this.speed,
|
||||
createdAt: this.createdAt,
|
||||
expiresAt: this.expiresAt,
|
||||
secretKey: arrayToB64(this.keychain.rawSecret),
|
||||
ownerToken: this.ownerToken,
|
||||
dlimit: this.dlimit,
|
||||
dtotal: this.dtotal,
|
||||
hasPassword: this.hasPassword
|
||||
};
|
||||
}
|
||||
}
|
||||
6
app/pages/blank.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const html = require('choo/html');
|
||||
|
||||
module.exports = function() {
|
||||
const div = html`<div></div>`;
|
||||
return div;
|
||||
};
|
||||
34
app/pages/completed.js
Normal file
@@ -0,0 +1,34 @@
|
||||
const html = require('choo/html');
|
||||
const progress = require('../templates/progress');
|
||||
const { fadeOut } = require('../utils');
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
const div = html`
|
||||
<div id="page-one">
|
||||
<div id="download" class="fadeIn">
|
||||
<div id="download-progress">
|
||||
<div id="dl-title" class="title">
|
||||
${state.translate('downloadFinish')}
|
||||
</div>
|
||||
<div class="description"></div>
|
||||
${progress(1)}
|
||||
<div class="upload">
|
||||
<div class="progress-text"></div>
|
||||
</div>
|
||||
</div>
|
||||
<a class="send-new"
|
||||
data-state="completed"
|
||||
href="/"
|
||||
onclick=${sendNew}>${state.translate('sendYourFilesLink')}</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
async function sendNew(e) {
|
||||
e.preventDefault();
|
||||
await fadeOut('download');
|
||||
emit('pushState', '/');
|
||||
}
|
||||
|
||||
return div;
|
||||
};
|
||||
46
app/pages/download.js
Normal file
@@ -0,0 +1,46 @@
|
||||
const html = require('choo/html');
|
||||
const progress = require('../templates/progress');
|
||||
const { bytes } = require('../utils');
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
const transfer = state.transfer;
|
||||
const cancelBtn = html`
|
||||
<button
|
||||
id="cancel-upload"
|
||||
title="${state.translate('deletePopupCancel')}"
|
||||
onclick=${cancel}>
|
||||
${state.translate('deletePopupCancel')}
|
||||
</button>`;
|
||||
|
||||
const div = html`
|
||||
<div id="page-one">
|
||||
<div id="download">
|
||||
<div id="download-progress" class="fadeIn">
|
||||
<div id="dl-title" class="title">
|
||||
${state.translate('downloadingPageProgress', {
|
||||
filename: state.fileInfo.name,
|
||||
size: bytes(state.fileInfo.size)
|
||||
})}
|
||||
</div>
|
||||
<div class="description">
|
||||
${state.translate('downloadingPageMessage')}
|
||||
</div>
|
||||
${progress(transfer.progressRatio)}
|
||||
<div class="upload">
|
||||
<div class="progress-text">
|
||||
${state.translate(transfer.msg, transfer.sizes)}
|
||||
</div>
|
||||
${transfer.state === 'downloading' ? cancelBtn : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
function cancel() {
|
||||
const btn = document.getElementById('cancel-upload');
|
||||
btn.remove();
|
||||
emit('cancel');
|
||||
}
|
||||
return div;
|
||||
};
|
||||
@@ -30,9 +30,5 @@ module.exports = function(state) {
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (state.layout) {
|
||||
return state.layout(state, div);
|
||||
}
|
||||
return div;
|
||||
};
|
||||
@@ -9,17 +9,13 @@ module.exports = function(state) {
|
||||
<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 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;
|
||||
};
|
||||
44
app/pages/preview.js
Normal file
@@ -0,0 +1,44 @@
|
||||
const html = require('choo/html');
|
||||
const assets = require('../../common/assets');
|
||||
const { bytes } = require('../utils');
|
||||
|
||||
module.exports = function(state, pageAction) {
|
||||
const fileInfo = state.fileInfo;
|
||||
|
||||
const size = fileInfo.size
|
||||
? state.translate('downloadFileSize', { size: bytes(fileInfo.size) })
|
||||
: '';
|
||||
|
||||
const title = fileInfo.name
|
||||
? state.translate('downloadFileName', { filename: fileInfo.name })
|
||||
: state.translate('downloadFileTitle');
|
||||
|
||||
const info = html`
|
||||
<div id="dl-file"
|
||||
data-nonce="${fileInfo.nonce}"
|
||||
data-requires-password="${fileInfo.requiresPassword}"></div>`;
|
||||
if (!pageAction) {
|
||||
return info;
|
||||
}
|
||||
const div = html`
|
||||
<div id="page-one">
|
||||
<div id="download">
|
||||
<div id="download-page-one">
|
||||
<div class="title">
|
||||
<span>${title}</span>
|
||||
<span id="dl-filesize">${' ' + size}</span>
|
||||
</div>
|
||||
<div class="description">${state.translate('downloadMessage')}</div>
|
||||
<img
|
||||
src="${assets.get('illustration_download.svg')}"
|
||||
id="download-img"
|
||||
title="${state.translate('downloadAltText')}"/>
|
||||
${pageAction}
|
||||
</div>
|
||||
<a class="send-new" href="/">${state.translate('sendYourFilesLink')}</a>
|
||||
</div>
|
||||
${info}
|
||||
</div>
|
||||
`;
|
||||
return div;
|
||||
};
|
||||
126
app/pages/share.js
Normal file
@@ -0,0 +1,126 @@
|
||||
/* global EXPIRE_SECONDS */
|
||||
const html = require('choo/html');
|
||||
const assets = require('../../common/assets');
|
||||
const notFound = require('./notFound');
|
||||
const uploadPasswordSet = require('../templates/uploadPasswordSet');
|
||||
const uploadPasswordUnset = require('../templates/uploadPasswordUnset');
|
||||
const selectbox = require('../templates/selectbox');
|
||||
const { allowedCopy, delay, fadeOut } = require('../utils');
|
||||
|
||||
function expireInfo(file, translate, emit) {
|
||||
const hours = Math.floor(EXPIRE_SECONDS / 60 / 60);
|
||||
const el = html([
|
||||
`<div>${translate('expireInfo', {
|
||||
downloadCount: '<select></select>',
|
||||
timespan: translate('timespanHours', { num: hours })
|
||||
})}</div>`
|
||||
]);
|
||||
const select = el.querySelector('select');
|
||||
const options = [1, 2, 3, 4, 5, 20].filter(i => i > (file.dtotal || 0));
|
||||
const t = num => translate('downloadCount', { num });
|
||||
const changed = value => emit('changeLimit', { file, value });
|
||||
select.parentNode.replaceChild(
|
||||
selectbox(file.dlimit || 1, options, t, changed),
|
||||
select
|
||||
);
|
||||
return el;
|
||||
}
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
const file = state.storage.getFileById(state.params.id);
|
||||
if (!file) {
|
||||
return notFound(state, emit);
|
||||
}
|
||||
|
||||
const passwordSection = file.hasPassword
|
||||
? uploadPasswordSet(state, emit)
|
||||
: uploadPasswordUnset(state, emit);
|
||||
const div = html`
|
||||
<div id="share-link" class="fadeIn">
|
||||
<div class="title">${expireInfo(file, state.translate, emit)}</div>
|
||||
<div id="share-window">
|
||||
<div id="copy-text">
|
||||
${state.translate('copyUrlFormLabelWithName', { filename: file.name })}
|
||||
</div>
|
||||
<div id="copy">
|
||||
<input id="link" type="url" value="${file.url}" readonly="true"/>
|
||||
<button id="copy-btn"
|
||||
class="btn"
|
||||
title="${state.translate('copyUrlFormButton')}"
|
||||
onclick=${copyLink}>${state.translate('copyUrlFormButton')}</button>
|
||||
</div>
|
||||
${passwordSection}
|
||||
<button id="delete-file"
|
||||
class="btn"
|
||||
title="${state.translate('deleteFileButton')}"
|
||||
onclick=${showPopup}>${state.translate('deleteFileButton')}
|
||||
</button>
|
||||
<div id="deletePopup" class="popup">
|
||||
<div class="popuptext" onblur=${cancel} tabindex="-1">
|
||||
<div class="popup-message">${state.translate('deletePopupText')}
|
||||
</div>
|
||||
<div class="popup-action">
|
||||
<span class="popup-no" onclick=${cancel}>
|
||||
${state.translate('deletePopupCancel')}
|
||||
</span>
|
||||
<span class="popup-yes" onclick=${deleteFile}>
|
||||
${state.translate('deletePopupYes')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a class="send-new"
|
||||
data-state="completed"
|
||||
href="/"
|
||||
onclick=${sendNew}>${state.translate('sendAnotherFileLink')}</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
function showPopup() {
|
||||
const popupText = document.querySelector('.popuptext');
|
||||
popupText.classList.add('show');
|
||||
popupText.focus();
|
||||
}
|
||||
|
||||
function cancel(e) {
|
||||
e.stopPropagation();
|
||||
const popupText = document.querySelector('.popuptext');
|
||||
popupText.classList.remove('show');
|
||||
}
|
||||
|
||||
async function sendNew(e) {
|
||||
e.preventDefault();
|
||||
await fadeOut('share-link');
|
||||
emit('pushState', '/');
|
||||
}
|
||||
|
||||
async function copyLink() {
|
||||
if (allowedCopy()) {
|
||||
emit('copy', { url: file.url, location: 'success-screen' });
|
||||
const input = document.getElementById('link');
|
||||
input.disabled = true;
|
||||
const copyBtn = document.getElementById('copy-btn');
|
||||
copyBtn.disabled = true;
|
||||
copyBtn.classList.add('success');
|
||||
copyBtn.replaceChild(
|
||||
html`<img src="${assets.get('check-16.svg')}" class="icon-check">`,
|
||||
copyBtn.firstChild
|
||||
);
|
||||
await delay(2000);
|
||||
input.disabled = false;
|
||||
if (!copyBtn.parentNode.classList.contains('wait-password')) {
|
||||
copyBtn.disabled = false;
|
||||
}
|
||||
copyBtn.classList.remove('success');
|
||||
copyBtn.textContent = state.translate('copyUrlFormButton');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteFile() {
|
||||
emit('delete', { file, location: 'success-screen' });
|
||||
await fadeOut('share-link');
|
||||
emit('pushState', '/');
|
||||
}
|
||||
return div;
|
||||
};
|
||||
52
app/pages/unsupported.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const html = require('choo/html');
|
||||
const assets = require('../../common/assets');
|
||||
|
||||
module.exports = function(state) {
|
||||
const msg =
|
||||
state.params.reason === 'outdated'
|
||||
? html`
|
||||
<div id="unsupported-browser">
|
||||
<div class="title">${state.translate('notSupportedHeader')}</div>
|
||||
<div class="description">
|
||||
${state.translate('notSupportedOutdatedDetail')}
|
||||
</div>
|
||||
<a
|
||||
id="update-firefox"
|
||||
href="https://support.mozilla.org/kb/update-firefox-latest-version">
|
||||
<img
|
||||
src="${assets.get('firefox_logo-only.svg')}"
|
||||
class="firefox-logo"
|
||||
alt="Firefox"/>
|
||||
<div class="unsupported-button-text">
|
||||
${state.translate('updateFirefox')}
|
||||
</div>
|
||||
</a>
|
||||
<div class="unsupported-description">
|
||||
${state.translate('uploadPageExplainer')}
|
||||
</div>
|
||||
</div>`
|
||||
: html`
|
||||
<div id="unsupported-browser">
|
||||
<div class="title">${state.translate('notSupportedHeader')}</div>
|
||||
<div class="description">${state.translate('notSupportedDetail')}</div>
|
||||
<div class="description">
|
||||
<a href="https://github.com/mozilla/send/blob/master/docs/faq.md#why-is-my-browser-not-supported">
|
||||
${state.translate('notSupportedLink')}
|
||||
</a>
|
||||
</div>
|
||||
<a id="dl-firefox" href="https://www.mozilla.org/firefox/new/?utm_campaign=send-acquisition&utm_medium=referral&utm_source=send.firefox.com">
|
||||
<img
|
||||
src="${assets.get('firefox_logo-only.svg')}"
|
||||
class="firefox-logo"
|
||||
alt="Firefox"/>
|
||||
<div class="unsupported-button-text">Firefox<br>
|
||||
<span>${state.translate('downloadFirefoxButtonSub')}</span>
|
||||
</div>
|
||||
</a>
|
||||
<div class="unsupported-description">
|
||||
${state.translate('uploadPageExplainer')}
|
||||
</div>
|
||||
</div>`;
|
||||
const div = html`<div id="page-one">${msg}</div>`;
|
||||
return div;
|
||||
};
|
||||
41
app/pages/upload.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const html = require('choo/html');
|
||||
const progress = require('../templates/progress');
|
||||
const { bytes } = require('../utils');
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
const transfer = state.transfer;
|
||||
|
||||
const div = html`
|
||||
<div id="download">
|
||||
<div id="upload-progress" class="fadeIn">
|
||||
<div class="title" id="upload-filename">
|
||||
${state.translate('uploadingPageProgress', {
|
||||
filename: transfer.file.name,
|
||||
size: bytes(transfer.file.size)
|
||||
})}
|
||||
</div>
|
||||
<div class="description"></div>
|
||||
${progress(transfer.progressRatio)}
|
||||
<div class="upload">
|
||||
<div class="progress-text">
|
||||
${state.translate(transfer.msg, transfer.sizes)}
|
||||
</div>
|
||||
<button
|
||||
id="cancel-upload"
|
||||
title="${state.translate('uploadingPageCancel')}"
|
||||
onclick=${cancel}>
|
||||
${state.translate('uploadingPageCancel')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
function cancel() {
|
||||
const btn = document.getElementById('cancel-upload');
|
||||
btn.disabled = true;
|
||||
btn.textContent = state.translate('uploadCancelNotification');
|
||||
emit('cancel');
|
||||
}
|
||||
return div;
|
||||
};
|
||||
82
app/pages/welcome.js
Normal file
@@ -0,0 +1,82 @@
|
||||
/* global MAXFILESIZE */
|
||||
const html = require('choo/html');
|
||||
const assets = require('../../common/assets');
|
||||
const fileList = require('../templates/fileList');
|
||||
const { bytes, fadeOut } = require('../utils');
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
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;
|
||||
}
|
||||
if (file.size > MAXFILESIZE) {
|
||||
window.alert(state.translate('fileTooBig', { size: bytes(MAXFILESIZE) }));
|
||||
return;
|
||||
}
|
||||
|
||||
await fadeOut('page-one');
|
||||
emit('upload', { file, type: 'click' });
|
||||
}
|
||||
return div;
|
||||
};
|
||||
@@ -1,9 +1,60 @@
|
||||
const preview = require('../templates/preview');
|
||||
const download = require('../templates/download');
|
||||
const preview = require('../pages/preview');
|
||||
const download = require('../pages/download');
|
||||
const notFound = require('../pages/notFound');
|
||||
const downloadPassword = require('../templates/downloadPassword');
|
||||
const downloadButton = require('../templates/downloadButton');
|
||||
|
||||
function hasFileInfo() {
|
||||
return !!document.getElementById('dl-file');
|
||||
}
|
||||
|
||||
function getFileInfoFromDOM() {
|
||||
const el = document.getElementById('dl-file');
|
||||
if (!el) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
nonce: el.getAttribute('data-nonce'),
|
||||
requiresPassword: !!+el.getAttribute('data-requires-password')
|
||||
};
|
||||
}
|
||||
|
||||
function createFileInfo(state) {
|
||||
const metadata = getFileInfoFromDOM();
|
||||
return {
|
||||
id: state.params.id,
|
||||
secretKey: state.params.key,
|
||||
nonce: metadata.nonce,
|
||||
requiresPassword: metadata.requiresPassword
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
if (state.transfer) {
|
||||
return download(state, emit);
|
||||
if (!state.fileInfo) {
|
||||
// This is a fresh page load
|
||||
// We need to parse the file info from the server's html
|
||||
if (!hasFileInfo()) {
|
||||
return notFound(state, emit);
|
||||
}
|
||||
state.fileInfo = createFileInfo(state);
|
||||
|
||||
if (!state.fileInfo.requiresPassword) {
|
||||
emit('getMetadata');
|
||||
}
|
||||
}
|
||||
return preview(state, emit);
|
||||
|
||||
let pageAction = null; //default state: we don't have file metadata
|
||||
if (state.transfer) {
|
||||
const s = state.transfer.state;
|
||||
if (['downloading', 'decrypting', 'complete'].indexOf(s) > -1) {
|
||||
// Downloading is in progress
|
||||
return download(state, emit);
|
||||
}
|
||||
// we have file metadata
|
||||
pageAction = downloadButton(state, emit);
|
||||
} else if (state.fileInfo.requiresPassword && !state.fileInfo.password) {
|
||||
// we're waiting on the user for a valid password
|
||||
pageAction = downloadPassword(state, emit);
|
||||
}
|
||||
return preview(state, pageAction);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
const welcome = require('../templates/welcome');
|
||||
const upload = require('../templates/upload');
|
||||
const welcome = require('../pages/welcome');
|
||||
const upload = require('../pages/upload');
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
if (state.transfer) {
|
||||
if (state.uploading) {
|
||||
return upload(state, emit);
|
||||
}
|
||||
return welcome(state, emit);
|
||||
|
||||
@@ -1,17 +1,54 @@
|
||||
const choo = require('choo');
|
||||
const html = require('choo/html');
|
||||
const download = require('./download');
|
||||
const header = require('../templates/header');
|
||||
const footer = require('../templates/footer');
|
||||
const fxPromo = require('../templates/fxPromo');
|
||||
|
||||
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'));
|
||||
function banner(state, emit) {
|
||||
if (state.promo && !state.route.startsWith('/unsupported/')) {
|
||||
return fxPromo(state, emit);
|
||||
}
|
||||
}
|
||||
|
||||
function body(template) {
|
||||
return function(state, emit) {
|
||||
const b = html`<body>
|
||||
${banner(state, emit)}
|
||||
${header(state)}
|
||||
<div class="all">
|
||||
<noscript>
|
||||
<h2>${state.translate('javascriptRequired')}</h2>
|
||||
<p>
|
||||
<a href="https://github.com/mozilla/send/blob/master/docs/faq.md#why-does-firefox-send-require-javascript">
|
||||
${state.translate('whyJavascript')}
|
||||
</a>
|
||||
</p>
|
||||
<p>${state.translate('enableJavascript')}</p>
|
||||
</noscript>
|
||||
${template(state, emit)}
|
||||
</div>
|
||||
${footer(state)}
|
||||
</body>`;
|
||||
if (state.layout) {
|
||||
// server side only
|
||||
return state.layout(state, b);
|
||||
}
|
||||
return b;
|
||||
};
|
||||
}
|
||||
|
||||
app.route('/', body(require('./home')));
|
||||
app.route('/share/:id', body(require('../pages/share')));
|
||||
app.route('/download/:id', body(download));
|
||||
app.route('/download/:id/:key', body(download));
|
||||
app.route('/completed', body(require('../pages/completed')));
|
||||
app.route('/unsupported/:reason', body(require('../pages/unsupported')));
|
||||
app.route('/legal', body(require('../pages/legal')));
|
||||
app.route('/error', body(require('../pages/error')));
|
||||
app.route('/blank', body(require('../pages/blank')));
|
||||
app.route('*', body(require('../pages/notFound')));
|
||||
|
||||
module.exports = app;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { isFile } from './utils';
|
||||
import OwnedFile from './ownedFile';
|
||||
|
||||
class Mem {
|
||||
constructor() {
|
||||
@@ -42,7 +43,7 @@ class Storage {
|
||||
const k = this.engine.key(i);
|
||||
if (isFile(k)) {
|
||||
try {
|
||||
const f = JSON.parse(this.engine.getItem(k));
|
||||
const f = new OwnedFile(JSON.parse(this.engine.getItem(k)));
|
||||
if (!f.id) {
|
||||
f.id = f.fileId;
|
||||
}
|
||||
@@ -92,11 +93,7 @@ class Storage {
|
||||
}
|
||||
|
||||
getFileById(id) {
|
||||
try {
|
||||
return JSON.parse(this.engine.getItem(id));
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
return this._files.find(f => f.id === id);
|
||||
}
|
||||
|
||||
get(id) {
|
||||
@@ -112,8 +109,16 @@ class Storage {
|
||||
|
||||
addFile(file) {
|
||||
this._files.push(file);
|
||||
this.writeFile(file);
|
||||
}
|
||||
|
||||
writeFile(file) {
|
||||
this.engine.setItem(file.id, JSON.stringify(file));
|
||||
}
|
||||
|
||||
writeFiles() {
|
||||
this._files.forEach(f => this.writeFile(f));
|
||||
}
|
||||
}
|
||||
|
||||
export default new Storage();
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
const html = require('choo/html');
|
||||
|
||||
module.exports = function(state) {
|
||||
const div = html`<div id="page-one"></div>`;
|
||||
if (state.layout) {
|
||||
return state.layout(state, div);
|
||||
}
|
||||
return div;
|
||||
};
|
||||
@@ -1,31 +0,0 @@
|
||||
const html = require('choo/html');
|
||||
const progress = require('./progress');
|
||||
const { fadeOut } = require('../utils');
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
const div = html`
|
||||
<div id="download" class="fadeIn">
|
||||
<div id="download-progress">
|
||||
<div id="dl-title" class="title">${state.translate(
|
||||
'downloadFinish'
|
||||
)}</div>
|
||||
<div class="description"></div>
|
||||
${progress(1)}
|
||||
<div class="upload">
|
||||
<div class="progress-text"></div>
|
||||
</div>
|
||||
</div>
|
||||
<a class="send-new" data-state="completed" href="/" onclick=${sendNew}>${state.translate(
|
||||
'sendYourFilesLink'
|
||||
)}</a>
|
||||
</div>
|
||||
`;
|
||||
|
||||
async function sendNew(e) {
|
||||
e.preventDefault();
|
||||
await fadeOut('download');
|
||||
emit('pushState', '/');
|
||||
}
|
||||
|
||||
return div;
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
const html = require('choo/html');
|
||||
const progress = require('./progress');
|
||||
const { bytes } = require('../utils');
|
||||
|
||||
module.exports = function(state) {
|
||||
const transfer = state.transfer;
|
||||
const div = html`
|
||||
<div id="download-progress" class="fadeIn">
|
||||
<div id="dl-title" class="title">${state.translate(
|
||||
'downloadingPageProgress',
|
||||
{
|
||||
filename: state.fileInfo.name,
|
||||
size: bytes(state.fileInfo.size)
|
||||
}
|
||||
)}</div>
|
||||
<div class="description">${state.translate('downloadingPageMessage')}</div>
|
||||
${progress(transfer.progressRatio)}
|
||||
<div class="upload">
|
||||
<div class="progress-text">${state.translate(
|
||||
transfer.msg,
|
||||
transfer.sizes
|
||||
)}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return div;
|
||||
};
|
||||
16
app/templates/downloadButton.js
Normal file
@@ -0,0 +1,16 @@
|
||||
const html = require('choo/html');
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
function download(event) {
|
||||
event.preventDefault();
|
||||
emit('download', state.fileInfo);
|
||||
}
|
||||
|
||||
return html`
|
||||
<div>
|
||||
<button id="download-btn"
|
||||
class="btn"
|
||||
onclick=${download}>${state.translate('downloadButtonLabel')}
|
||||
</button>
|
||||
</div>`;
|
||||
};
|
||||
59
app/templates/downloadPassword.js
Normal file
@@ -0,0 +1,59 @@
|
||||
const html = require('choo/html');
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
const fileInfo = state.fileInfo;
|
||||
const label =
|
||||
fileInfo.password === null
|
||||
? html`
|
||||
<label class="red" for="unlock-input">
|
||||
${state.translate('passwordTryAgain')}
|
||||
</label>`
|
||||
: html`
|
||||
<label for="unlock-input">
|
||||
${state.translate('unlockInputLabel')}
|
||||
</label>`;
|
||||
const div = html`
|
||||
<div class="enterPassword">
|
||||
${label}
|
||||
<form id="unlock" onsubmit=${checkPassword} data-no-csrf>
|
||||
<input id="unlock-input"
|
||||
class="unlock-input input-no-btn"
|
||||
maxlength="64"
|
||||
autocomplete="off"
|
||||
placeholder="${state.translate('unlockInputPlaceholder')}"
|
||||
oninput=${inputChanged}
|
||||
type="password"
|
||||
autofocus />
|
||||
<input type="submit"
|
||||
id="unlock-btn"
|
||||
class="btn btn-hidden"
|
||||
value="${state.translate('unlockButtonLabel')}"/>
|
||||
</form>
|
||||
</div>`;
|
||||
|
||||
function inputChanged() {
|
||||
const input = document.getElementById('unlock-input');
|
||||
const btn = document.getElementById('unlock-btn');
|
||||
if (input.value.length > 0) {
|
||||
btn.classList.remove('btn-hidden');
|
||||
input.classList.remove('input-no-btn');
|
||||
} else {
|
||||
btn.classList.add('btn-hidden');
|
||||
input.classList.add('input-no-btn');
|
||||
}
|
||||
}
|
||||
|
||||
function checkPassword(event) {
|
||||
event.preventDefault();
|
||||
const password = document.getElementById('unlock-input').value;
|
||||
if (password.length > 0) {
|
||||
document.getElementById('unlock-btn').disabled = true;
|
||||
state.fileInfo.url = window.location.href;
|
||||
state.fileInfo.password = password;
|
||||
emit('getMetadata');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return div;
|
||||
};
|
||||
@@ -1,47 +1,62 @@
|
||||
const html = require('choo/html');
|
||||
const assets = require('../../common/assets');
|
||||
|
||||
function timeLeft(milliseconds) {
|
||||
function timeLeft(milliseconds, state) {
|
||||
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`;
|
||||
return state.translate('expiresHoursMinutes', {
|
||||
hours,
|
||||
minutes: minutes % 60
|
||||
});
|
||||
} else if (hours === 0) {
|
||||
return `${minutes}m ${seconds}s`;
|
||||
if (minutes === 0) {
|
||||
return state.translate('expiresMinutes', { minutes: '< 1' });
|
||||
}
|
||||
return state.translate('expiresMinutes', { minutes });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = function(file, state, emit) {
|
||||
const ttl = file.expiresAt - Date.now();
|
||||
const remaining = timeLeft(ttl) || state.translate('linkExpiredAlt');
|
||||
const remainingTime =
|
||||
timeLeft(ttl, state) || state.translate('linkExpiredAlt');
|
||||
const downloadLimit = file.dlimit || 1;
|
||||
const totalDownloads = file.dtotal || 0;
|
||||
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 class="overflow-col" title="${file.name}">
|
||||
<a class="link" href="/share/${file.id}">${file.name}</a>
|
||||
</td>
|
||||
<td>${remaining}</td>
|
||||
<td class="center-col">
|
||||
<img onclick=${showPopup} src="${assets.get(
|
||||
'close-16.svg'
|
||||
)}" class="icon-delete" title="${state.translate('deleteButtonHover')}">
|
||||
<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 class="overflow-col">${remainingTime}</td>
|
||||
<td class="center-col">${totalDownloads} / ${downloadLimit}</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>
|
||||
<span class="popup-no" onclick=${cancel}>
|
||||
${state.translate('deletePopupCancel')}
|
||||
</span>
|
||||
<span class="popup-yes" onclick=${deleteFile}>
|
||||
${state.translate('deletePopupYes')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,13 +9,18 @@ module.exports = function(state, emit) {
|
||||
<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>
|
||||
<th id="copy-file-list" class="center-col">
|
||||
${state.translate('copyFileList')}
|
||||
</th>
|
||||
<th id="expiry-time-file-list" >
|
||||
${state.translate('timeFileList')}
|
||||
</th>
|
||||
<th id="expiry-downloads-file-list" >
|
||||
${state.translate('downloadsFileList')}
|
||||
</th>
|
||||
<th id="delete-file-list" class="center-col">
|
||||
${state.translate('deleteFileList')}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
43
app/templates/footer.js
Normal file
@@ -0,0 +1,43 @@
|
||||
const html = require('choo/html');
|
||||
const assets = require('../../common/assets');
|
||||
|
||||
module.exports = function(state) {
|
||||
return html`<div class="footer">
|
||||
<div class="legal-links">
|
||||
<a href="https://www.mozilla.org" role="presentation">
|
||||
<img
|
||||
class="mozilla-logo"
|
||||
src="${assets.get('mozilla-logo.svg')}"
|
||||
alt="mozilla"/>
|
||||
</a>
|
||||
<a href="https://www.mozilla.org/about/legal">
|
||||
${state.translate('footerLinkLegal')}
|
||||
</a>
|
||||
<a href="https://testpilot.firefox.com/about">
|
||||
${state.translate('footerLinkAbout')}
|
||||
</a>
|
||||
<a href="/legal">${state.translate('footerLinkPrivacy')}</a>
|
||||
<a href="/legal">${state.translate('footerLinkTerms')}</a>
|
||||
<a href="https://www.mozilla.org/privacy/websites/#cookies">
|
||||
${state.translate('footerLinkCookies')}
|
||||
</a>
|
||||
<a href="https://www.mozilla.org/about/legal/report-infringement/">
|
||||
${state.translate('reportIPInfringement')}
|
||||
</a>
|
||||
</div>
|
||||
<div class="social-links">
|
||||
<a href="https://github.com/mozilla/send" role="presentation">
|
||||
<img
|
||||
class="github"
|
||||
src="${assets.get('github-icon.svg')}"
|
||||
alt="github"/>
|
||||
</a>
|
||||
<a href="https://twitter.com/FxTestPilot" role="presentation">
|
||||
<img
|
||||
class="twitter"
|
||||
src="${assets.get('twitter-icon.svg')}"
|
||||
alt="twitter"/>
|
||||
</a>
|
||||
</div>
|
||||
</div>`;
|
||||
};
|
||||
33
app/templates/fxPromo.js
Normal file
@@ -0,0 +1,33 @@
|
||||
const html = require('choo/html');
|
||||
const assets = require('../../common/assets');
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
function clicked() {
|
||||
emit('experiment', { cd3: 'promo' });
|
||||
}
|
||||
let classes = 'banner';
|
||||
switch (state.promo) {
|
||||
case 'blue':
|
||||
classes = 'banner banner-blue';
|
||||
break;
|
||||
case 'pink':
|
||||
classes = 'banner banner-pink';
|
||||
break;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="${classes}">
|
||||
<div>
|
||||
<img
|
||||
src="${assets.get('firefox_logo-only.svg')}"
|
||||
class="firefox-logo-small"
|
||||
alt="Firefox"/>
|
||||
<span>Send is brought to you by the all-new Firefox.
|
||||
<a
|
||||
class="link"
|
||||
href="https://www.mozilla.org/firefox/new/?utm_campaign=send-acquisition&utm_medium=referral&utm_source=send.firefox.com"
|
||||
onclick=${clicked}
|
||||
>Download Firefox now ≫</a></span>
|
||||
</div>
|
||||
</div>`;
|
||||
};
|
||||
59
app/templates/header.js
Normal file
@@ -0,0 +1,59 @@
|
||||
const html = require('choo/html');
|
||||
const assets = require('../../common/assets');
|
||||
/*
|
||||
The current weback config uses package.json to generate
|
||||
version.json for /__version__ meaning `require` returns the
|
||||
string 'version.json' in the frontend context but the json
|
||||
on the server.
|
||||
|
||||
We want `version` to be constant at build time so this file
|
||||
has a custom loader (/build/version_loader.js) just to replace
|
||||
string with the value from package.json. 🤢
|
||||
*/
|
||||
const version = require('../../package.json').version || 'VERSION';
|
||||
|
||||
function browserName() {
|
||||
try {
|
||||
if (/firefox/i.test(navigator.userAgent)) {
|
||||
return 'firefox';
|
||||
}
|
||||
if (/edge/i.test(navigator.userAgent)) {
|
||||
return 'edge';
|
||||
}
|
||||
if (/trident/i.test(navigator.userAgent)) {
|
||||
return 'ie';
|
||||
}
|
||||
if (/chrome/i.test(navigator.userAgent)) {
|
||||
return 'chrome';
|
||||
}
|
||||
if (/safari/i.test(navigator.userAgent)) {
|
||||
return 'safari';
|
||||
}
|
||||
return 'other';
|
||||
} catch (e) {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
const browser = browserName();
|
||||
|
||||
module.exports = function(state) {
|
||||
return html`<header class="header">
|
||||
<div class="send-logo">
|
||||
<a href="/">
|
||||
<img
|
||||
src="${assets.get('send_logo.svg')}"
|
||||
alt="Send"/>
|
||||
<h1 class="site-title">Send</h1>
|
||||
</a>
|
||||
<div class="site-subtitle">
|
||||
<a href="https://testpilot.firefox.com">Firefox Test Pilot</a>
|
||||
<div>${state.translate('siteSubtitle')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<a href="https://qsurvey.mozilla.com/s3/txp-firefox-send?ver=${version}&browser=${browser}"
|
||||
rel="noreferrer noopener"
|
||||
class="feedback"
|
||||
target="_blank">${state.translate('siteFeedback')}</a>
|
||||
</header>`;
|
||||
};
|
||||
@@ -1,70 +0,0 @@
|
||||
const html = require('choo/html');
|
||||
const assets = require('../../common/assets');
|
||||
const notFound = require('./notFound');
|
||||
const { bytes } = require('../utils');
|
||||
|
||||
function getFileFromDOM() {
|
||||
const el = document.getElementById('dl-file');
|
||||
if (!el) {
|
||||
return null;
|
||||
}
|
||||
const data = el.dataset;
|
||||
return {
|
||||
name: data.name,
|
||||
size: parseInt(data.size, 10),
|
||||
ttl: parseInt(data.ttl, 10)
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
state.fileInfo = state.fileInfo || getFileFromDOM();
|
||||
if (!state.fileInfo) {
|
||||
return notFound(state, emit);
|
||||
}
|
||||
state.fileInfo.id = state.params.id;
|
||||
state.fileInfo.key = state.params.key;
|
||||
const fileInfo = state.fileInfo;
|
||||
const size = bytes(fileInfo.size);
|
||||
const div = html`
|
||||
<div id="page-one">
|
||||
<div id="download">
|
||||
<div id="download-page-one">
|
||||
<div class="title">
|
||||
<span id="dl-file"
|
||||
data-name="${fileInfo.name}"
|
||||
data-size="${fileInfo.size}"
|
||||
data-ttl="${fileInfo.ttl}">${state.translate('downloadFileName', {
|
||||
filename: fileInfo.name
|
||||
})}</span>
|
||||
<span id="dl-filesize">${' ' +
|
||||
state.translate('downloadFileSize', { size })}</span>
|
||||
</div>
|
||||
<div class="description">${state.translate('downloadMessage')}</div>
|
||||
<img
|
||||
src="${assets.get('illustration_download.svg')}"
|
||||
id="download-img"
|
||||
alt="${state.translate('downloadAltText')}"/>
|
||||
<div>
|
||||
<button
|
||||
id="download-btn"
|
||||
class="btn"
|
||||
title="${state.translate('downloadButtonLabel')}"
|
||||
onclick=${download}>${state.translate(
|
||||
'downloadButtonLabel'
|
||||
)}</button>
|
||||
</div>
|
||||
</div>
|
||||
<a class="send-new" href="/">${state.translate('sendYourFilesLink')}</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
function download(event) {
|
||||
event.preventDefault();
|
||||
emit('download', fileInfo);
|
||||
}
|
||||
|
||||
if (state.layout) {
|
||||
return state.layout(state, div);
|
||||
}
|
||||
return div;
|
||||
};
|
||||
@@ -10,10 +10,30 @@ module.exports = function(progressRatio) {
|
||||
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
|
||||
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>
|
||||
`;
|
||||
|
||||
54
app/templates/selectbox.js
Normal file
@@ -0,0 +1,54 @@
|
||||
const html = require('choo/html');
|
||||
|
||||
module.exports = function(selected, options, translate, changed) {
|
||||
const id = `select-${Math.random()}`;
|
||||
let x = selected;
|
||||
|
||||
function close() {
|
||||
const ul = document.getElementById(id);
|
||||
const body = document.querySelector('body');
|
||||
ul.classList.remove('active');
|
||||
body.removeEventListener('click', close);
|
||||
}
|
||||
|
||||
function toggle(event) {
|
||||
event.stopPropagation();
|
||||
const ul = document.getElementById(id);
|
||||
if (ul.classList.contains('active')) {
|
||||
close();
|
||||
} else {
|
||||
ul.classList.add('active');
|
||||
const body = document.querySelector('body');
|
||||
body.addEventListener('click', close);
|
||||
}
|
||||
}
|
||||
|
||||
function choose(event) {
|
||||
event.stopPropagation();
|
||||
const target = event.target;
|
||||
const value = +target.dataset.value;
|
||||
target.parentNode.previousSibling.firstElementChild.textContent = translate(
|
||||
value
|
||||
);
|
||||
if (x !== value) {
|
||||
x = value;
|
||||
changed(value);
|
||||
}
|
||||
close();
|
||||
}
|
||||
return html`
|
||||
<div class="selectbox">
|
||||
<div onclick=${toggle}>
|
||||
<span class="link">${translate(selected)}</span>
|
||||
<svg width="32" height="32">
|
||||
<polygon points="8 18 17 28 26 18" fill="#0094fb"/>
|
||||
</svg>
|
||||
</div>
|
||||
<ul id="${id}" class="selectOptions">
|
||||
${options.map(
|
||||
i =>
|
||||
html`<li class="selectOption" onclick=${choose} data-value="${i}">${i}</li>`
|
||||
)}
|
||||
</ul>
|
||||
</div>`;
|
||||
};
|
||||
@@ -1,64 +0,0 @@
|
||||
const html = require('choo/html');
|
||||
const assets = require('../../common/assets');
|
||||
const notFound = require('./notFound');
|
||||
const { allowedCopy, delay, fadeOut } = require('../utils');
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
const file = state.storage.getFileById(state.params.id);
|
||||
if (!file) {
|
||||
return notFound(state, emit);
|
||||
}
|
||||
const div = html`
|
||||
<div id="share-link" class="fadeIn">
|
||||
<div class="title">${state.translate('uploadSuccessTimingHeader')}</div>
|
||||
<div id="share-window">
|
||||
<div id="copy-text">${state.translate('copyUrlFormLabelWithName', {
|
||||
filename: file.name
|
||||
})}</div>
|
||||
<div id="copy">
|
||||
<input id="link" type="url" value="${file.url}" readonly="true"/>
|
||||
<button id="copy-btn" class="btn" title="${state.translate(
|
||||
'copyUrlFormButton'
|
||||
)}" onclick=${copyLink}>${state.translate('copyUrlFormButton')}</button>
|
||||
</div>
|
||||
<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.replaceChild(
|
||||
html`<img src="${assets.get('check-16.svg')}" class="icon-check">`,
|
||||
copyBtn.firstChild
|
||||
);
|
||||
await delay(2000);
|
||||
input.disabled = false;
|
||||
copyBtn.disabled = false;
|
||||
copyBtn.textContent = state.translate('copyUrlFormButton');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteFile() {
|
||||
emit('delete', { file, location: 'success-screen' });
|
||||
await fadeOut('share-link');
|
||||
emit('pushState', '/');
|
||||
}
|
||||
return div;
|
||||
};
|
||||
@@ -1,50 +0,0 @@
|
||||
const html = require('choo/html');
|
||||
const assets = require('../../common/assets');
|
||||
|
||||
module.exports = function(state) {
|
||||
const msg =
|
||||
state.params.reason === 'outdated'
|
||||
? html`
|
||||
<div id="unsupported-browser">
|
||||
<div class="title">${state.translate('notSupportedHeader')}</div>
|
||||
<div class="description">${state.translate(
|
||||
'notSupportedOutdatedDetail'
|
||||
)}</div>
|
||||
<a id="update-firefox" href="https://support.mozilla.org/kb/update-firefox-latest-version">
|
||||
<img src="${assets.get(
|
||||
'firefox_logo-only.svg'
|
||||
)}" class="firefox-logo" alt="Firefox"/>
|
||||
<div class="unsupported-button-text">${state.translate(
|
||||
'updateFirefox'
|
||||
)}</div>
|
||||
</a>
|
||||
<div class="unsupported-description">${state.translate(
|
||||
'uploadPageExplainer'
|
||||
)}</div>
|
||||
</div>`
|
||||
: html`
|
||||
<div id="unsupported-browser">
|
||||
<div class="title">${state.translate('notSupportedHeader')}</div>
|
||||
<div class="description">${state.translate('notSupportedDetail')}</div>
|
||||
<div class="description"><a href="https://github.com/mozilla/send/blob/master/docs/faq.md#why-is-my-browser-not-supported">${state.translate(
|
||||
'notSupportedLink'
|
||||
)}</a></div>
|
||||
<a id="dl-firefox" href="https://www.mozilla.org/firefox/new/?scene=2">
|
||||
<img src="${assets.get(
|
||||
'firefox_logo-only.svg'
|
||||
)}" class="firefox-logo" alt="Firefox"/>
|
||||
<div class="unsupported-button-text">Firefox<br>
|
||||
<span>${state.translate('downloadFirefoxButtonSub')}</span>
|
||||
</div>
|
||||
</a>
|
||||
<div class="unsupported-description">${state.translate(
|
||||
'uploadPageExplainer'
|
||||
)}</div>
|
||||
</div>`;
|
||||
const div = html`<div id="page-one">${msg}</div>`;
|
||||
|
||||
if (state.layout) {
|
||||
return state.layout(state, div);
|
||||
}
|
||||
return div;
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
const html = require('choo/html');
|
||||
const progress = require('./progress');
|
||||
const { bytes } = require('../utils');
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
const transfer = state.transfer;
|
||||
|
||||
const div = html`
|
||||
<div id="upload-progress" class="fadeIn">
|
||||
<div class="title" id="upload-filename">${state.translate(
|
||||
'uploadingPageProgress',
|
||||
{
|
||||
filename: transfer.file.name,
|
||||
size: bytes(transfer.file.size)
|
||||
}
|
||||
)}</div>
|
||||
<div class="description"></div>
|
||||
${progress(transfer.progressRatio)}
|
||||
<div class="upload">
|
||||
<div class="progress-text">${state.translate(
|
||||
transfer.msg,
|
||||
transfer.sizes
|
||||
)}</div>
|
||||
<button id="cancel-upload" 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;
|
||||
};
|
||||
80
app/templates/uploadPasswordSet.js
Normal file
@@ -0,0 +1,80 @@
|
||||
const html = require('choo/html');
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
const file = state.storage.getFileById(state.params.id);
|
||||
|
||||
return html`<div class="selectPassword">
|
||||
${passwordSpan(file.password)}
|
||||
<button
|
||||
id="resetButton"
|
||||
onclick=${toggleResetInput}
|
||||
>${state.translate('changePasswordButton')}</button>
|
||||
<form
|
||||
id='reset-form'
|
||||
class="setPassword hidden"
|
||||
onsubmit=${resetPassword}
|
||||
data-no-csrf>
|
||||
<input id="unlock-reset-input"
|
||||
class="unlock-input input-no-btn"
|
||||
maxlength="32"
|
||||
autocomplete="off"
|
||||
type="password"
|
||||
oninput=${inputChanged}
|
||||
placeholder="${state.translate('unlockInputPlaceholder')}">
|
||||
<input type="submit"
|
||||
id="unlock-reset-btn"
|
||||
class="btn btn-hidden"
|
||||
value="${state.translate('changePasswordButton')}"/>
|
||||
</form>
|
||||
</div>`;
|
||||
|
||||
function passwordSpan(password) {
|
||||
password = password || '●●●●●';
|
||||
const span = html([
|
||||
`<span>${state.translate('passwordResult', {
|
||||
password:
|
||||
'<pre class="passwordOriginal"></pre><pre class="passwordMask"></pre>'
|
||||
})}</span>`
|
||||
]);
|
||||
const og = span.querySelector('.passwordOriginal');
|
||||
const masked = span.querySelector('.passwordMask');
|
||||
og.textContent = password;
|
||||
masked.textContent = password.replace(/./g, '●');
|
||||
return span;
|
||||
}
|
||||
|
||||
function inputChanged() {
|
||||
const resetInput = document.getElementById('unlock-reset-input');
|
||||
const resetBtn = document.getElementById('unlock-reset-btn');
|
||||
if (resetInput.value.length > 0) {
|
||||
resetBtn.classList.remove('btn-hidden');
|
||||
resetInput.classList.remove('input-no-btn');
|
||||
} else {
|
||||
resetBtn.classList.add('btn-hidden');
|
||||
resetInput.classList.add('input-no-btn');
|
||||
}
|
||||
}
|
||||
|
||||
function resetPassword(event) {
|
||||
event.preventDefault();
|
||||
const password = document.querySelector('#unlock-reset-input').value;
|
||||
if (password.length > 0) {
|
||||
document.getElementById('copy').classList.remove('wait-password');
|
||||
document.getElementById('copy-btn').disabled = false;
|
||||
emit('password', { password, file });
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function toggleResetInput(event) {
|
||||
const form = event.target.parentElement.querySelector('form');
|
||||
const input = document.getElementById('unlock-reset-input');
|
||||
if (form.style.visibility === 'hidden' || form.style.visibility === '') {
|
||||
form.style.visibility = 'visible';
|
||||
input.focus();
|
||||
} else {
|
||||
form.style.visibility = 'hidden';
|
||||
}
|
||||
inputChanged();
|
||||
}
|
||||
};
|
||||
70
app/templates/uploadPasswordUnset.js
Normal file
@@ -0,0 +1,70 @@
|
||||
const html = require('choo/html');
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
const file = state.storage.getFileById(state.params.id);
|
||||
const div = html`
|
||||
<div class="selectPassword">
|
||||
<div id="addPasswordWrapper">
|
||||
<input
|
||||
id="addPassword"
|
||||
type="checkbox"
|
||||
autocomplete="off"
|
||||
onchange=${togglePasswordInput}/>
|
||||
<label for="addPassword">
|
||||
${state.translate('requirePasswordCheckbox')}
|
||||
</label>
|
||||
</div>
|
||||
<form class="setPassword hidden" onsubmit=${setPassword} data-no-csrf>
|
||||
<input id="unlock-input"
|
||||
class="unlock-input input-no-btn"
|
||||
maxlength="32"
|
||||
autocomplete="off"
|
||||
placeholder="${state.translate('unlockInputPlaceholder')}"
|
||||
type="password"
|
||||
oninput=${inputChanged}/>
|
||||
<input type="submit"
|
||||
id="unlock-btn"
|
||||
class="btn btn-hidden"
|
||||
value="${state.translate('addPasswordButton')}"/>
|
||||
</form>
|
||||
</div>`;
|
||||
|
||||
function inputChanged() {
|
||||
const input = document.getElementById('unlock-input');
|
||||
const btn = document.getElementById('unlock-btn');
|
||||
if (input.value.length > 0) {
|
||||
btn.classList.remove('btn-hidden');
|
||||
input.classList.remove('input-no-btn');
|
||||
} else {
|
||||
btn.classList.add('btn-hidden');
|
||||
input.classList.add('input-no-btn');
|
||||
}
|
||||
}
|
||||
|
||||
function togglePasswordInput(e) {
|
||||
const unlockInput = document.getElementById('unlock-input');
|
||||
const boxChecked = e.target.checked;
|
||||
document
|
||||
.querySelector('.setPassword')
|
||||
.classList.toggle('hidden', !boxChecked);
|
||||
if (boxChecked) {
|
||||
unlockInput.focus();
|
||||
} else {
|
||||
unlockInput.value = '';
|
||||
}
|
||||
inputChanged();
|
||||
}
|
||||
|
||||
function setPassword(event) {
|
||||
event.preventDefault();
|
||||
const password = document.getElementById('unlock-input').value;
|
||||
if (password.length > 0) {
|
||||
document.getElementById('copy').classList.remove('wait-password');
|
||||
document.getElementById('copy-btn').disabled = false;
|
||||
emit('password', { password, file });
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return div;
|
||||
};
|
||||
@@ -1,62 +0,0 @@
|
||||
const html = require('choo/html');
|
||||
const assets = require('../../common/assets');
|
||||
const fileList = require('./fileList');
|
||||
const { fadeOut } = require('../utils');
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
const div = html`
|
||||
<div id="page-one" class="fadeIn">
|
||||
<div class="title">${state.translate('uploadPageHeader')}</div>
|
||||
<div class="description">
|
||||
<div>${state.translate('uploadPageExplainer')}</div>
|
||||
<a href="https://testpilot.firefox.com/experiments/send" class="link">${state.translate(
|
||||
'uploadPageLearnMore'
|
||||
)}</a>
|
||||
</div>
|
||||
<div class="${state.config
|
||||
.uploadWindowStyle}" ondragover=${dragover} ondragleave=${dragleave}>
|
||||
<div id="upload-img"><img src="${assets.get(
|
||||
'upload.svg'
|
||||
)}" title="${state.translate('uploadSvgAlt')}"/></div>
|
||||
<div id="upload-text">${state.translate('uploadPageDropMessage')}</div>
|
||||
<span id="file-size-msg"><em>${state.translate(
|
||||
'uploadPageSizeMessage'
|
||||
)}</em></span>
|
||||
<form method="post" action="upload" enctype="multipart/form-data">
|
||||
<label for="file-upload" id="browse" class="${state.config
|
||||
.uploadButtonStyle}" title="${state.translate(
|
||||
'uploadPageBrowseButton1'
|
||||
)}">${state.translate('uploadPageBrowseButton1')}</label>
|
||||
<input id="file-upload" type="file" name="fileUploaded" onchange=${upload} />
|
||||
</form>
|
||||
</div>
|
||||
${fileList(state, emit)}
|
||||
</div>
|
||||
`;
|
||||
|
||||
function dragover(event) {
|
||||
const div = document.querySelector('.upload-window');
|
||||
div.classList.add('ondrag');
|
||||
}
|
||||
|
||||
function dragleave(event) {
|
||||
const div = document.querySelector('.upload-window');
|
||||
div.classList.remove('ondrag');
|
||||
}
|
||||
|
||||
async function upload(event) {
|
||||
event.preventDefault();
|
||||
const target = event.target;
|
||||
const file = target.files[0];
|
||||
if (file.size === 0) {
|
||||
return;
|
||||
}
|
||||
await fadeOut('page-one');
|
||||
emit('upload', { file, type: 'click' });
|
||||
}
|
||||
|
||||
if (state.layout) {
|
||||
return state.layout(state, div);
|
||||
}
|
||||
return div;
|
||||
};
|
||||
115
app/utils.js
@@ -1,38 +1,18 @@
|
||||
function arrayToHex(iv) {
|
||||
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 notify(str) {
|
||||
return str;
|
||||
/* TODO: enable once we have an opt-in ui element
|
||||
if (!('Notification' in window)) {
|
||||
return;
|
||||
} else if (Notification.permission === 'granted') {
|
||||
new Notification(str);
|
||||
} else if (Notification.permission !== 'denied') {
|
||||
Notification.requestPermission(function(permission) {
|
||||
if (permission === 'granted') new Notification(str);
|
||||
});
|
||||
}
|
||||
*/
|
||||
function b64ToArray(str) {
|
||||
str = (str + '==='.slice((str.length + 3) % 4))
|
||||
.replace(/-/g, '+')
|
||||
.replace(/_/g, '/');
|
||||
return b64.toByteArray(str);
|
||||
}
|
||||
|
||||
function loadShim(polyfill) {
|
||||
@@ -105,21 +85,36 @@ const LOCALIZE_NUMBERS = !!(
|
||||
|
||||
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
|
||||
? n.toLocaleString(navigator.languages, {
|
||||
let nStr = n.toFixed(1);
|
||||
if (LOCALIZE_NUMBERS) {
|
||||
try {
|
||||
const locale = document.querySelector('html').lang;
|
||||
nStr = n.toLocaleString(locale, {
|
||||
minimumFractionDigits: 1,
|
||||
maximumFractionDigits: 1
|
||||
})
|
||||
: n.toFixed(1);
|
||||
});
|
||||
} catch (e) {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
return `${nStr}${UNITS[exponent]}`;
|
||||
}
|
||||
|
||||
function percent(ratio) {
|
||||
return LOCALIZE_NUMBERS
|
||||
? ratio.toLocaleString(navigator.languages, { style: 'percent' })
|
||||
: `${Math.floor(ratio * 100)}%`;
|
||||
if (LOCALIZE_NUMBERS) {
|
||||
try {
|
||||
const locale = document.querySelector('html').lang;
|
||||
return ratio.toLocaleString(locale, { style: 'percent' });
|
||||
} catch (e) {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
return `${Math.floor(ratio * 100)}%`;
|
||||
}
|
||||
|
||||
function allowedCopy() {
|
||||
@@ -138,7 +133,37 @@ function fadeOut(id) {
|
||||
return delay(300);
|
||||
}
|
||||
|
||||
const ONE_DAY_IN_MS = 86400000;
|
||||
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;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fadeOut,
|
||||
@@ -147,10 +172,10 @@ module.exports = {
|
||||
bytes,
|
||||
percent,
|
||||
copyToClipboard,
|
||||
arrayToHex,
|
||||
hexToArray,
|
||||
notify,
|
||||
arrayToB64,
|
||||
b64ToArray,
|
||||
canHasSend,
|
||||
isFile,
|
||||
ONE_DAY_IN_MS
|
||||
saveFile,
|
||||
openLinksInNewTab
|
||||
};
|
||||
|
||||
1
assets/check-16-blue.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path fill="#0A84FF " d="M6 14a1 1 0 0 1-.707-.293l-3-3a1 1 0 0 1 1.414-1.414l2.157 2.157 6.316-9.023a1 1 0 0 1 1.639 1.146l-7 10a1 1 0 0 1-.732.427A.863.863 0 0 1 6 14z"/></svg>
|
||||
|
After Width: | Height: | Size: 238 B |
BIN
assets/favicon-120.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
assets/favicon-128.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
assets/favicon-144.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
assets/favicon-152.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
assets/favicon-167.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
assets/favicon-180.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
assets/favicon-195.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
assets/favicon-196.png
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
assets/favicon-228.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
assets/favicon-32.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
BIN
assets/favicon-96.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 239 KiB After Width: | Height: | Size: 16 KiB |
369
assets/main.css
@@ -8,7 +8,6 @@ html {
|
||||
background-repeat: no-repeat;
|
||||
background-position: center top;
|
||||
height: 100%;
|
||||
max-width: 1440px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
@@ -130,18 +129,27 @@ body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
max-width: 630px;
|
||||
max-width: 650px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
box-sizing: border-box;
|
||||
width: 96%;
|
||||
}
|
||||
|
||||
pre,
|
||||
input,
|
||||
select,
|
||||
textarea,
|
||||
button {
|
||||
font-family: inherit;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
font-family: monospace;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
a {
|
||||
@@ -205,8 +213,8 @@ a {
|
||||
}
|
||||
|
||||
.upload-window {
|
||||
border: 1px dashed rgba(0, 148, 251, 0.5);
|
||||
margin: 0 auto;
|
||||
border: 3px dashed rgba(0, 148, 251, 0.5);
|
||||
margin: 0 auto 10px;
|
||||
height: 255px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
@@ -219,8 +227,7 @@ a {
|
||||
}
|
||||
|
||||
.upload-window.ondrag {
|
||||
border: 3px dashed rgba(0, 148, 251, 0.5);
|
||||
margin: 0 auto;
|
||||
border: 5px dashed rgba(0, 148, 251, 0.5);
|
||||
height: 251px;
|
||||
transform: scale(1.04);
|
||||
border-radius: 4.2px;
|
||||
@@ -231,12 +238,8 @@ a {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.upload-window-b {
|
||||
border: 3px dashed rgba(0, 148, 251, 0.5);
|
||||
}
|
||||
|
||||
.upload-window-b.ondrag {
|
||||
border: 5px dashed rgba(0, 148, 251, 0.5);
|
||||
.upload-window.ondrag * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.link {
|
||||
@@ -258,10 +261,10 @@ a {
|
||||
.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;
|
||||
@@ -273,13 +276,18 @@ a {
|
||||
background-color: #0287e8;
|
||||
}
|
||||
|
||||
.browse-b {
|
||||
height: 60px;
|
||||
font-size: 20px;
|
||||
input[type='file'] {
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
input[type='file'] {
|
||||
display: none;
|
||||
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 {
|
||||
@@ -334,12 +342,16 @@ tbody {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
#expiry-file-list {
|
||||
width: 21%;
|
||||
#expiry-time-file-list {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
#expiry-downloads-file-list {
|
||||
width: 8%;
|
||||
}
|
||||
|
||||
#delete-file-list {
|
||||
width: 12%;
|
||||
width: 7%;
|
||||
}
|
||||
|
||||
.overflow-col {
|
||||
@@ -410,6 +422,8 @@ tbody {
|
||||
border-radius: 0 0 5px;
|
||||
border-right: 1px solid #d7d7db;
|
||||
border-bottom: 1px solid #d7d7db;
|
||||
border-left: 1px solid #fff;
|
||||
border-top: 1px solid #fff;
|
||||
}
|
||||
|
||||
.popup .show {
|
||||
@@ -539,7 +553,6 @@ tbody {
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-width: 640px;
|
||||
@@ -556,6 +569,11 @@ tbody {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#copy.wait-password #link,
|
||||
#copy.wait-password #copy-btn {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
#copy-text {
|
||||
align-self: flex-start;
|
||||
margin-top: 60px;
|
||||
@@ -593,23 +611,27 @@ tbody {
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
height: 60px;
|
||||
padding-bottom: 4px;
|
||||
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;
|
||||
}
|
||||
|
||||
#delete-file {
|
||||
align-self: center;
|
||||
width: 176px;
|
||||
height: 44px;
|
||||
background: #fff;
|
||||
@@ -622,10 +644,35 @@ tbody {
|
||||
color: #313131;
|
||||
}
|
||||
|
||||
#deletePopup {
|
||||
position: relative;
|
||||
align-self: center;
|
||||
bottom: 50px;
|
||||
}
|
||||
|
||||
#delete-file:hover {
|
||||
background: #efeff1;
|
||||
}
|
||||
|
||||
#resetButton {
|
||||
width: 80px;
|
||||
height: 30px;
|
||||
background: #fff;
|
||||
border: 1px solid rgba(12, 12, 13, 0.3);
|
||||
border-radius: 5px;
|
||||
font-size: 15px;
|
||||
margin-top: 5px;
|
||||
margin-left: 15px;
|
||||
margin-bottom: 12px;
|
||||
line-height: 24px;
|
||||
cursor: pointer;
|
||||
color: #313131;
|
||||
}
|
||||
|
||||
#resetButton:hover {
|
||||
background: #efeff1;
|
||||
}
|
||||
|
||||
.send-new {
|
||||
font-size: 15px;
|
||||
margin: auto;
|
||||
@@ -641,6 +688,37 @@ tbody {
|
||||
color: #0287e8;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.selectPassword {
|
||||
padding: 10px 0;
|
||||
align-self: left;
|
||||
max-width: 100%;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.passwordOriginal {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selectPassword :hover .passwordOriginal {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.selectPassword :hover .passwordMask {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.setPassword {
|
||||
align-self: left;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
width: 80%;
|
||||
padding: 10px 5px;
|
||||
}
|
||||
|
||||
/* upload-error */
|
||||
#upload-error {
|
||||
display: flex;
|
||||
@@ -679,6 +757,10 @@ tbody {
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.firefox-logo-small {
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
#dl-firefox,
|
||||
#update-firefox {
|
||||
margin-bottom: 181px;
|
||||
@@ -688,7 +770,7 @@ tbody {
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
box-shadow: 0 5px 3px rgb(234, 234, 234);
|
||||
font-family: 'Fira Sans';
|
||||
font-family: 'Fira Sans', 'segoe ui', sans-serif;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
font-size: 26px;
|
||||
@@ -705,7 +787,7 @@ tbody {
|
||||
}
|
||||
|
||||
.unsupported-button-text > span {
|
||||
font-family: 'Fira Sans';
|
||||
font-family: 'Fira Sans', 'segoe ui', sans-serif;
|
||||
font-weight: 300;
|
||||
font-size: 18px;
|
||||
letter-spacing: -0.69px;
|
||||
@@ -736,7 +818,7 @@ tbody {
|
||||
}
|
||||
|
||||
#download {
|
||||
margin: 0 auto;
|
||||
margin: 0 auto 30px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -769,6 +851,68 @@ 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,
|
||||
#unlock-reset-btn {
|
||||
flex: 0 1 165px;
|
||||
background: #0297f8;
|
||||
border-radius: 0 6px 6px 0;
|
||||
border: 1px solid #0297f8;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
|
||||
/* Force flat button look */
|
||||
-webkit-appearance: none;
|
||||
font-size: 15px;
|
||||
padding-bottom: 3px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#unlock-btn:hover,
|
||||
#unlock-reset-btn:hover {
|
||||
background-color: #0287e8;
|
||||
}
|
||||
|
||||
.btn-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.input-no-btn {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* footer */
|
||||
.footer {
|
||||
right: 0;
|
||||
@@ -833,6 +977,139 @@ tbody {
|
||||
margin-bottom: -5px;
|
||||
}
|
||||
|
||||
#addPasswordWrapper {
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
#addPassword {
|
||||
position: absolute;
|
||||
visibility: collapse;
|
||||
}
|
||||
|
||||
#addPasswordWrapper:hover label::before {
|
||||
border: 1px solid #0297f8;
|
||||
}
|
||||
|
||||
#addPasswordWrapper label {
|
||||
line-height: 23px;
|
||||
cursor: pointer;
|
||||
color: #737373;
|
||||
}
|
||||
|
||||
#addPassword:checked + label {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
.banner {
|
||||
padding: 0 15px;
|
||||
height: 48px;
|
||||
background-color: #efeff1;
|
||||
color: #4a4a4f;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.banner > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.banner > div > span {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.banner-blue {
|
||||
background: linear-gradient(-180deg, #45a1ff 0%, #00feff 94%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.banner-blue a {
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.banner-blue a:hover {
|
||||
color: #eee;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.banner-pink {
|
||||
background: linear-gradient(-180deg, #ff9400 0%, #ff1ad9 94%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.banner-pink a {
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.banner-pink a:hover {
|
||||
color: #eee;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.selectbox {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selectSelected {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selectOptions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selectOptions.active {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 0;
|
||||
margin: 40px 0;
|
||||
background-color: white;
|
||||
border: 1px solid rgba(12, 12, 13, 0.3);
|
||||
border-radius: 4px;
|
||||
box-shadow: 1px 2px 4px rgba(12, 12, 13, 0.3);
|
||||
}
|
||||
|
||||
.selectOption {
|
||||
color: #737373;
|
||||
font-size: 12pt;
|
||||
list-style: none;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
padding: 0 60px;
|
||||
border-bottom: 1px solid rgba(12, 12, 13, 0.3);
|
||||
}
|
||||
|
||||
.selectOption:hover {
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
|
||||
@media (max-device-width: 992px), (max-width: 992px) {
|
||||
.popup .popuptext {
|
||||
left: auto;
|
||||
@@ -903,22 +1180,42 @@ 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,
|
||||
#unlock-reset-input {
|
||||
font-size: 22px;
|
||||
padding: 15px 10px;
|
||||
border-radius: 6px 6px 0 0;
|
||||
}
|
||||
|
||||
#copy-btn {
|
||||
#copy-btn,
|
||||
#unlock-btn,
|
||||
#unlock-reset-btn {
|
||||
border-radius: 0 0 6px 6px;
|
||||
flex: 0 1 65px;
|
||||
}
|
||||
|
||||
#copy-text {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
th {
|
||||
font-size: 14px;
|
||||
padding: 0 5px;
|
||||
@@ -928,4 +1225,12 @@ tbody {
|
||||
font-size: 13px;
|
||||
padding: 17px 5px 0;
|
||||
}
|
||||
|
||||
.popup .popuptext::after {
|
||||
left: 125px;
|
||||
}
|
||||
|
||||
#deletePopup {
|
||||
left: 83px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
<svg width="30" height="27" viewBox="0 0 30 27" xmlns="http://www.w3.org/2000/svg"><title>send logo</title><g stroke="#3E3D40" fill="none" fill-rule="evenodd"><path d="M22.364 19.989l-2.153-2.103a2.046 2.046 0 0 0-2.665-.151l3.402 3.323a.531.531 0 0 1 0 .766l-2.466 2.408a.563.563 0 0 1-.784 0l-3.398-3.32a1.932 1.932 0 0 0 .188 2.564l2.153 2.103c.788.77 2.066.77 2.855 0l2.868-2.802a1.94 1.94 0 0 0 0-2.788M8.77 14.745a.534.534 0 0 0 0 .766l3.399 3.32a2.05 2.05 0 0 1-2.625-.184l-2.153-2.102a1.94 1.94 0 0 1 0-2.79l2.869-2.801a2.052 2.052 0 0 1 2.854 0l2.153 2.103c.73.713.775 1.83.154 2.603l-3.401-3.323a.565.565 0 0 0-.784 0L8.77 14.745zm9.464 5.682a.777.777 0 0 1 0 1.118.822.822 0 0 1-1.144 0l-5.6-5.47a.777.777 0 0 1 0-1.118.822.822 0 0 1 1.144 0l5.6 5.47z" stroke-width=".618" fill="#3E3D40"/><path d="M6.065 20.606c-2.913-1.586-3.988-3.656-3.988-6.468 0-2.81 2.265-6.425 5.786-6.289.1.004.55-.006.649 0 .895-3.27 2.508-6.353 6.898-6.353 4.557 0 7.336 3.716 6.75 7.785.08-.005 1.232.17 1.31.186 3.096.644 4.915 3.275 4.915 5.18 0 1.905-.107 3.029-2.023 4.947" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></g></svg>
|
||||
<svg width="30" height="27" viewBox="0 0 30 27" xmlns="http://www.w3.org/2000/svg"><title>send logo</title><g stroke="#3E3D40" fill="none" fill-rule="evenodd" transform="translate(-0.231,0.11948695)"><path d="M22.364 19.989l-2.153-2.103a2.046 2.046 0 0 0-2.665-.151l3.402 3.323a.531.531 0 0 1 0 .766l-2.466 2.408a.563.563 0 0 1-.784 0l-3.398-3.32a1.932 1.932 0 0 0 .188 2.564l2.153 2.103c.788.77 2.066.77 2.855 0l2.868-2.802a1.94 1.94 0 0 0 0-2.788M8.77 14.745a.534.534 0 0 0 0 .766l3.399 3.32a2.05 2.05 0 0 1-2.625-.184l-2.153-2.102a1.94 1.94 0 0 1 0-2.79l2.869-2.801a2.052 2.052 0 0 1 2.854 0l2.153 2.103c.73.713.775 1.83.154 2.603l-3.401-3.323a.565.565 0 0 0-.784 0L8.77 14.745zm9.464 5.682a.777.777 0 0 1 0 1.118.822.822 0 0 1-1.144 0l-5.6-5.47a.777.777 0 0 1 0-1.118.822.822 0 0 1 1.144 0l5.6 5.47z" stroke-width=".618" fill="#3E3D40"/><path d="M6.065 20.606c-2.913-1.586-3.988-3.656-3.988-6.468 0-2.81 2.265-6.425 5.786-6.289.1.004.55-.006.649 0 .895-3.27 2.508-6.353 6.898-6.353 4.557 0 7.336 3.716 6.75 7.785.08-.005 1.232.17 1.31.186 3.096.644 4.915 3.275 4.915 5.18 0 1.905-.107 3.029-2.023 4.947" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.2 KiB |
10
browserconfig.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square70x70logo src=“favicon-76.png”/>
|
||||
<square150x150logo src="favicon-228.png"/>
|
||||
<TileColor>#0297F8</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
||||
@@ -1,26 +1,45 @@
|
||||
const { MessageContext } = require('fluent');
|
||||
const fs = require('fs');
|
||||
|
||||
function toJSON(map) {
|
||||
return JSON.stringify(Array.from(map));
|
||||
}
|
||||
|
||||
function merge(m1, m2) {
|
||||
const result = new Map(m1);
|
||||
for (const [k, v] of m2) {
|
||||
result.set(k, v);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = function(source) {
|
||||
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}`);
|
||||
}
|
||||
// load default language and "merge" contexts
|
||||
// TODO: make this configurable
|
||||
const en_ftl = fs.readFileSync(
|
||||
require.resolve('../public/locales/en-US/send.ftl'),
|
||||
'utf8'
|
||||
);
|
||||
const en = new MessageContext('en-US');
|
||||
en.addMessages(en_ftl);
|
||||
// pre-parse the ftl
|
||||
const context = new MessageContext(locale);
|
||||
context.addMessages(source);
|
||||
|
||||
const merged = merge(en._messages, context._messages);
|
||||
return `
|
||||
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)});
|
||||
ctx._messages = new Map(${toJSON(merged)});
|
||||
function translate(id, data) {
|
||||
var msg = ctx.getMessage(id);
|
||||
if (typeof(msg) !== 'string' && !msg.val && msg.attrs) {
|
||||
|
||||
5
build/version_loader.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const version = require('../package.json').version;
|
||||
|
||||
module.exports = function(source) {
|
||||
return source.replace('VERSION', version);
|
||||
};
|
||||
@@ -8,6 +8,5 @@ services:
|
||||
- "1443:1443"
|
||||
environment:
|
||||
- REDIS_HOST=redis
|
||||
- NODE_ENV=production
|
||||
redis:
|
||||
image: redis:alpine
|
||||
|
||||
@@ -27,6 +27,7 @@ Data will be collected with Google Analytics and follow [Test Pilot standards](h
|
||||
- `cm5` - the number of files the user has ever uploaded.
|
||||
- `cm6` - the number of unexpired files the user has uploaded.
|
||||
- `cm7` - the number of files the user has ever downloaded.
|
||||
- `cm8` - the number of downloads permitted by the uploader.
|
||||
|
||||
### Custom Dimensions
|
||||
- `cd1` - the method by which the user initiated an upload. One of `drag`, `click`.
|
||||
@@ -67,6 +68,25 @@ Triggered whenever a user stops uploading a file. Includes:
|
||||
- `cd2`
|
||||
- `cd6`
|
||||
|
||||
#### `download-limit-changed`
|
||||
Triggered whenever the sender changes the download limit. Includes:
|
||||
|
||||
- `ec` - `sender`
|
||||
- `ea` - `download-limit-changed`
|
||||
- `cm1`
|
||||
- `cm5`
|
||||
- `cm6`
|
||||
- `cm7`
|
||||
- `cm8`
|
||||
|
||||
#### `password-added`
|
||||
Triggered whenever a password is added to a file. Includes:
|
||||
|
||||
- `cm1`
|
||||
- `cm5`
|
||||
- `cm6`
|
||||
- `cm7`
|
||||
|
||||
#### `download-started`
|
||||
Triggered whenever a user begins downloading a file. Includes:
|
||||
|
||||
|
||||
19
docs/takedowns.md
Normal file
@@ -0,0 +1,19 @@
|
||||
## Take-down process
|
||||
|
||||
In cases of a DMCA notice, or other abuse yet to be determined, a file has to be removed from the service.
|
||||
|
||||
Files can be delisted and made inaccessible by removing their record from Redis.
|
||||
|
||||
Send share links contain the `id` of the file, for example `https://send.firefox.com/download/3d9d2bb9a1`
|
||||
|
||||
From a host with access to the Redis server run a `DEL` command with the file id.
|
||||
|
||||
For example:
|
||||
|
||||
```sh
|
||||
redis-cli DEL 3d9d2bb9a1
|
||||
```
|
||||
|
||||
Other redis-cli parameters like `-h` may also be required. See [redis-cli docs](https://redis.io/topics/rediscli) for more info.
|
||||
|
||||
The encrypted file resides on S3 as the same `id` under the bucket that the app was configured with as `S3_BUCKET`. The file can be managed if it has not already expired with the [AWS cli](https://docs.aws.amazon.com/cli/latest/reference/s3/index.html) or AWS web console.
|
||||
4661
package-lock.json
generated
83
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "firefox-send",
|
||||
"description": "File Sharing Experiment",
|
||||
"version": "1.2.0",
|
||||
"version": "2.3.1",
|
||||
"author": "Mozilla (https://mozilla.org)",
|
||||
"repository": "mozilla/send",
|
||||
"homepage": "https://github.com/mozilla/send/",
|
||||
@@ -43,75 +43,77 @@
|
||||
"node": ">=8.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^7.1.4",
|
||||
"autoprefixer": "^7.2.5",
|
||||
"babel-core": "^6.26.0",
|
||||
"babel-loader": "^7.1.2",
|
||||
"babel-plugin-yo-yoify": "^0.7.0",
|
||||
"babel-plugin-yo-yoify": "^1.0.2",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"babel-preset-env": "^1.6.0",
|
||||
"babel-preset-env": "^1.6.1",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"babel-preset-stage-2": "^6.24.1",
|
||||
"choo-log": "^7.2.1",
|
||||
"copy-webpack-plugin": "^4.0.1",
|
||||
"cross-env": "^5.0.5",
|
||||
"css-loader": "^0.28.7",
|
||||
"css-mqpacker": "^6.0.1",
|
||||
"base64-js": "^1.2.1",
|
||||
"copy-webpack-plugin": "^4.3.1",
|
||||
"cross-env": "^5.1.3",
|
||||
"css-loader": "^0.28.9",
|
||||
"css-mqpacker": "^6.0.2",
|
||||
"cssnano": "^3.10.0",
|
||||
"eslint": "^4.6.1",
|
||||
"eslint": "^4.16.0",
|
||||
"eslint-plugin-mocha": "^4.11.0",
|
||||
"eslint-plugin-node": "^5.1.1",
|
||||
"eslint-plugin-node": "^5.2.1",
|
||||
"eslint-plugin-security": "^1.4.0",
|
||||
"expose-loader": "^0.7.3",
|
||||
"extract-loader": "^1.0.1",
|
||||
"file-loader": "^0.11.2",
|
||||
"expose-loader": "^0.7.4",
|
||||
"extract-loader": "^1.0.2",
|
||||
"file-loader": "^1.1.6",
|
||||
"fluent-intl-polyfill": "^0.1.0",
|
||||
"git-rev-sync": "^1.9.1",
|
||||
"github-changes": "^1.1.0",
|
||||
"html-loader": "^0.5.1",
|
||||
"github-changes": "^1.1.2",
|
||||
"html-loader": "^0.5.5",
|
||||
"husky": "^0.14.3",
|
||||
"lint-staged": "^4.1.3",
|
||||
"lint-staged": "^4.3.0",
|
||||
"mocha": "^3.5.3",
|
||||
"nanobus": "^4.2.0",
|
||||
"npm-run-all": "^4.1.1",
|
||||
"postcss-loader": "^2.0.6",
|
||||
"prettier": "^1.6.1",
|
||||
"nanobus": "^4.3.2",
|
||||
"npm-run-all": "^4.1.2",
|
||||
"postcss-loader": "^2.0.10",
|
||||
"prettier": "^1.10.2",
|
||||
"proxyquire": "^1.8.0",
|
||||
"raven-js": "^3.17.0",
|
||||
"raven-js": "^3.22.1",
|
||||
"redis-mock": "^0.20.0",
|
||||
"require-from-string": "^1.2.1",
|
||||
"require-from-string": "^2.0.1",
|
||||
"rimraf": "^2.6.2",
|
||||
"selenium-webdriver": "^3.5.0",
|
||||
"sinon": "^3.2.1",
|
||||
"selenium-webdriver": "^3.6.0",
|
||||
"sinon": "^4.2.2",
|
||||
"string-hash": "^1.1.3",
|
||||
"stylelint-config-standard": "^17.0.0",
|
||||
"stylelint-no-unsupported-browser-features": "^1.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.5.6",
|
||||
"webpack-dev-server": "^2.8.0",
|
||||
"webpack-manifest-plugin": "^1.3.1",
|
||||
"val-loader": "^1.1.0",
|
||||
"webpack": "^3.10.0",
|
||||
"webpack-dev-server": "2.9.1",
|
||||
"webpack-manifest-plugin": "^1.3.2",
|
||||
"webpack-unassert-loader": "^1.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"aws-sdk": "^2.114.0",
|
||||
"body-parser": "^1.18.1",
|
||||
"choo": "^6.0.1",
|
||||
"aws-sdk": "^2.188.0",
|
||||
"body-parser": "^1.18.2",
|
||||
"choo": "^6.7.0",
|
||||
"cldr-core": "^32.0.0",
|
||||
"connect-busboy": "0.0.2",
|
||||
"convict": "^4.0.0",
|
||||
"express": "^4.15.4",
|
||||
"express-request-language": "^1.1.12",
|
||||
"convict": "^4.0.1",
|
||||
"express": "^4.16.2",
|
||||
"fluent": "^0.4.1",
|
||||
"fluent-langneg": "^0.1.0",
|
||||
"helmet": "^3.8.1",
|
||||
"helmet": "^3.10.0",
|
||||
"mkdirp": "^0.5.1",
|
||||
"mozlog": "^2.1.1",
|
||||
"raven": "^2.1.2",
|
||||
"mozlog": "^2.2.0",
|
||||
"raven": "^2.4.0",
|
||||
"redis": "^2.8.0"
|
||||
},
|
||||
"availableLanguages": [
|
||||
"en-US",
|
||||
"ast",
|
||||
"az",
|
||||
"bs",
|
||||
"ca",
|
||||
"cak",
|
||||
"cs",
|
||||
@@ -123,6 +125,7 @@
|
||||
"es-CL",
|
||||
"es-ES",
|
||||
"es-MX",
|
||||
"et",
|
||||
"fa",
|
||||
"fr",
|
||||
"fy-NL",
|
||||
@@ -131,6 +134,7 @@
|
||||
"id",
|
||||
"it",
|
||||
"ja",
|
||||
"ka",
|
||||
"kab",
|
||||
"ko",
|
||||
"ms",
|
||||
@@ -144,6 +148,7 @@
|
||||
"sl",
|
||||
"sr",
|
||||
"sv-SE",
|
||||
"tl",
|
||||
"tr",
|
||||
"uk",
|
||||
"vi",
|
||||
|
||||
127
public/locales/ar/send.ftl
Normal file
@@ -0,0 +1,127 @@
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
title = فَيَرفُكس سِنْد
|
||||
siteSubtitle = تجربة وِبّيّة
|
||||
siteFeedback = الانطباعات
|
||||
uploadPageHeader = شارِك ملفاتك بخصوصية وتعمية
|
||||
uploadPageExplainer = أرسل الملفات عبر رابط آمن خاص ومعمّى تنتهي صلاحيته تلقائيا لتضمن عدم بقاء ما ترسله إلى الأبد.
|
||||
uploadPageLearnMore = اطّلع على المزيد
|
||||
uploadPageDropMessage = أسقِط ملفّك هنا لبدء الرفع
|
||||
uploadPageSizeMessage = لتتحصل على أفضل تجربة، من المستحسن أن يكون الملف أصغر من 1 غ.بايت
|
||||
uploadPageBrowseButton = اختر ملفّا على حاسوبك
|
||||
.title = اختر ملفّا على حاسوبك
|
||||
uploadPageBrowseButton1 = اختر ملفّا لرفعه
|
||||
uploadPageMultipleFilesAlert = رفع عدة ملفات (أو رفع مجلد) ليس مدعوما حاليا.
|
||||
uploadPageBrowseButtonTitle = ارفع ملفًا
|
||||
uploadingPageProgress = يرفع { $filename } ({ $size })
|
||||
importingFile = يستورد…
|
||||
verifyingFile = يتحقق…
|
||||
encryptingFile = يعمّي…
|
||||
decryptingFile = يفك التعمية…
|
||||
notifyUploadDone = انتهى الرفع.
|
||||
uploadingPageMessage = ما إن يُرفع الملف سيُتاح ضبط خيارات انتهاء صلاحيته.
|
||||
uploadingPageCancel = ألغِ الرفع
|
||||
.title = ألغِ الرفع
|
||||
uploadCancelNotification = أُلغي الرفع.
|
||||
uploadingPageLargeFileMessage = هذا الملف كبير الحجم وسيأخذ رفعه وقتا. انتظر رجاءً.
|
||||
uploadingFileNotification = أعلِمني عندما يكتمل الرفع.
|
||||
uploadSuccessConfirmHeader = جاهز للإرسال
|
||||
uploadSvgAlt
|
||||
.alt = ارفع
|
||||
uploadSuccessTimingHeader = ستنتهي صلاحية الرابط الذي يشير إلى الملف في حال: نُزِّل لأول مرة، أو مرّ ٢٤ ساعة على رفعه.
|
||||
expireInfo = ستنتهي صلاحية رابط الملف بعد { $downloadCount } أو { $timespan }.
|
||||
downloadCount = { $num ->
|
||||
[zero] لا تنزيلات
|
||||
[one] تنزيل واحد
|
||||
[two] تنزيلين
|
||||
[few] { $num } تنزيلات
|
||||
[many] { $num } تنزيلًا
|
||||
*[other] { $num } تنزيل
|
||||
}
|
||||
timespanHours = { $num ->
|
||||
[zero] أقل من ساعة
|
||||
[one] ساعة
|
||||
[two] ساعتين
|
||||
[few] { $num } ساعات
|
||||
[many] { $num } ساعة
|
||||
*[other] { $num } ساعة
|
||||
}
|
||||
copyUrlFormLabelWithName = انسخ الرابط وشاركه لإرسال الملف: { $filename }
|
||||
copyUrlFormButton = انسخ إلى الحافظة
|
||||
.title = انسخ إلى الحافظة
|
||||
copiedUrl = نُسخ!
|
||||
deleteFileButton = احذف الملف
|
||||
.title = احذف الملف
|
||||
sendAnotherFileLink = أرسل ملفّا آخر
|
||||
.title = أرسل ملفّا آخر
|
||||
// Alternative text used on the download link/button (indicates an action).
|
||||
downloadAltText
|
||||
.alt = نزّل
|
||||
downloadsFileList = التنزيلات
|
||||
// Used as header in a column indicating the amount of time left before a
|
||||
// download link expires (e.g. "10h 5m")
|
||||
timeFileList = الوقت
|
||||
// Used as header in a column indicating the number of times a file has been
|
||||
// downloaded
|
||||
downloadFileName = نزّل { $filename }
|
||||
downloadFileSize = ({ $size })
|
||||
unlockInputLabel = أدخل كلمة السر
|
||||
unlockInputPlaceholder = كلمة السر
|
||||
unlockButtonLabel = افتح القفل
|
||||
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 = جرِّب «فَيَرفُكس سِنْد»
|
||||
downloadingPageProgress = ينزّل { $filename } ({ $size })
|
||||
downloadingPageMessage = رجاء أبقِ هذا اللسان مفتوحا حتى نجلب الملف ونفك تعميته.
|
||||
errorAltText
|
||||
.alt = خطأ أثناء الرفع
|
||||
errorPageHeader = حدث خطب ما.
|
||||
errorPageMessage = حدث خطب ما أثناء رفع الملف.
|
||||
errorPageLink = أرسل ملفا آخر
|
||||
fileTooBig = حجم الملف كبير للغاية لرفعه. يجب أن يكون أصغر من { $size }.
|
||||
linkExpiredAlt = انتهت صلاحية الرابط
|
||||
expiredPageHeader = انتهت صلاحية هذا الرابط أو لم يكن موجودا في المقام الأول!
|
||||
notSupportedHeader = متصفحك غير مدعوم.
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
notSupportedDetail = للأسف فإن متصفحك لا يدعم تقنية الوِب التي يعتمد عليها «فَيَرفُكس سِنْد». عليك تجربة متصفح آخر، ونحن ننصحك بِفَيَرفُكس!
|
||||
notSupportedLink = لماذا متصفحي غير مدعوم؟
|
||||
notSupportedOutdatedDetail = للأسف فإن إصدارة فَيَرفُكس هذه لا تدعم تقنية الوِب التي يعتمد عليها «فَيَرفُكس سِنْد». عليك تحديث متصفحك.
|
||||
updateFirefox = حدّث فَيَرفُكس
|
||||
downloadFirefoxButtonSub = تنزيل مجاني
|
||||
uploadedFile = ملف
|
||||
copyFileList = انسخ الرابط
|
||||
// expiryFileList is used as a column header
|
||||
expiryFileList = ينتهي في
|
||||
deleteFileList = احذف
|
||||
nevermindButton = لا بأس
|
||||
legalHeader = الشروط والخصوصية
|
||||
legalNoticeTestPilot = «فَيَرفُكس سِنْد» جزء من اختبار تجريبي حاليًا و يخضع <a>لبنود خدمة</a> الاختبار التجريبي و <a>تنويه الخصوصية</a>. يمكنك التعرف على مزيد من المعلومات حول هذه التجربة وجمع البيانات<a>هنا</a>.
|
||||
legalNoticeMozilla = يخضع استخدام موقع «فَيَرفُكس سِنْد» إلى<a>تنويه خصوصية المواقع</a> و <a>بنود خدمة المواقع</a>.
|
||||
deletePopupText = أأحذف هذا الملف؟
|
||||
deletePopupYes = نعم
|
||||
deletePopupCancel = ألغِ
|
||||
deleteButtonHover
|
||||
.title = احذف
|
||||
copyUrlHover
|
||||
.title = انسخ الرابط
|
||||
footerLinkLegal = القانونية
|
||||
// Test Pilot is a proper name and should not be localized.
|
||||
footerLinkAbout = حول الاختبار التجريبي
|
||||
footerLinkPrivacy = الخصوصية
|
||||
footerLinkTerms = الشروط
|
||||
footerLinkCookies = الكعكات
|
||||
requirePasswordCheckbox = اطلب كلمة سر لتنزيل هذا الملف
|
||||
addPasswordButton = أضِف كلمة سر
|
||||
changePasswordButton = غيّر
|
||||
passwordTryAgain = كلمة السر خاطئة. أعِد المحاولة.
|
||||
// This label is followed by the password needed to download a file
|
||||
passwordResult = كلمة السر: { $password }
|
||||
reportIPInfringement = أبلغ عن انتهاك للملكية الفكرية
|
||||
@@ -34,6 +34,10 @@ sendAnotherFileLink = Unviar otru ficheru
|
||||
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).
|
||||
@@ -80,3 +84,8 @@ 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.
|
||||
// This label is followed by the password needed to download a file
|
||||
passwordResult = Contraseña: { $password }
|
||||
|
||||
@@ -25,6 +25,15 @@ uploadingFileNotification = Yükləmə bitdiyində xəbər ver.
|
||||
uploadSuccessConfirmHeader = Göndərməyə hazır
|
||||
uploadSvgAlt = Yüklə
|
||||
uploadSuccessTimingHeader = Faylınızın keçidinin 1 endirmədən və ya 24 saatdan sonra vaxtı çıxacaq.
|
||||
expireInfo = Faylınız üçün keçidin vaxtı { $downloadCount } sonra və ya { $timespan } tarixində keçəcək.
|
||||
downloadCount = { $num ->
|
||||
[one] 1 endirmə
|
||||
*[other] { $num } endirmə
|
||||
}
|
||||
timespanHours = { $num ->
|
||||
[one] 1 saat
|
||||
*[other] { $num } saat
|
||||
}
|
||||
copyUrlFormLabelWithName = Faylınızı göndərmək üçün keçidi köçürün: { $filename }
|
||||
copyUrlFormButton = Buferə köçür
|
||||
copiedUrl = Köçürüldü!
|
||||
@@ -32,8 +41,18 @@ deleteFileButton = Faylı sil
|
||||
sendAnotherFileLink = Başqa fayl göndər
|
||||
// Alternative text used on the download link/button (indicates an action).
|
||||
downloadAltText = Endir
|
||||
downloadsFileList = Endirmələr
|
||||
// Used as header in a column indicating the amount of time left before a
|
||||
// download link expires (e.g. "10h 5m")
|
||||
timeFileList = Vaxt
|
||||
// Used as header in a column indicating the number of times a file has been
|
||||
// downloaded
|
||||
downloadFileName = { $filename } faylını endir
|
||||
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).
|
||||
@@ -80,3 +99,17 @@ 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
|
||||
changePasswordButton = Dəyişdir
|
||||
passwordTryAgain = Səhv parol. Təkrar yoxlayın.
|
||||
// This label is followed by the password needed to download a file
|
||||
passwordResult = Parol: { $password }
|
||||
reportIPInfringement = Əqli-mülkiyyət pozuntusu bildir
|
||||
javascriptRequired = Firefox Send üçün JavaScript lazımdır
|
||||
whyJavascript = Firefox Send niyə JavaScript tələb edir?
|
||||
enableJavascript = Lütfən JavaScript-i aktiv edib təkrar yoxlayın.
|
||||
// A short representation of a countdown timer containing the number of hours and minutes remaining as digits, example "13h 47m"
|
||||
expiresHoursMinutes = { $hours } saat { $minutes } dəq
|
||||
// A short representation of a countdown timer containing the number of minutes remaining as digits, example "56m"
|
||||
expiresMinutes = { $minutes } dəq
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
title = Firefox Send
|
||||
siteSubtitle = ওয়েব গবেষণা
|
||||
siteFeedback = প্রতিক্রিয়া
|
||||
uploadPageHeader = ব্যক্তিগত, এনক্রিপ্ট করা ফাইল শেয়ারিং
|
||||
uploadPageLearnMore = আরও জানুন
|
||||
uploadPageBrowseButton = আপনার কম্পিউটারে ফাইল নির্বাচন করুন
|
||||
uploadPageBrowseButtonTitle = ফাইল আপলোড
|
||||
@@ -14,14 +15,31 @@ uploadingPageCancel = আপলোড বাতিল করুন
|
||||
uploadCancelNotification = আপনার অাপলোড বাতিল করা হয়েছে।
|
||||
uploadSuccessConfirmHeader = পাঠানোর জন্য প্রস্তুত
|
||||
uploadSvgAlt = আপলোড
|
||||
downloadCount = { $num ->
|
||||
[one] 1 ডাউনলোড
|
||||
*[other] { $num } ডাউনলোডগুলো
|
||||
}
|
||||
timespanHours = { $num ->
|
||||
[one] 1 ঘন্টা
|
||||
*[other] { $num } ঘন্টা
|
||||
}
|
||||
copyUrlFormButton = ক্লিপবোর্ডে কপি করুন
|
||||
copiedUrl = কপি করা হয়েছে!
|
||||
deleteFileButton = ফাইল মুছুন
|
||||
sendAnotherFileLink = আরেকটি ফাইল পাঠান
|
||||
// Alternative text used on the download link/button (indicates an action).
|
||||
downloadAltText = ডাউনলোড
|
||||
downloadsFileList = ডাউনলোডগুলো
|
||||
// Used as header in a column indicating the amount of time left before a
|
||||
// download link expires (e.g. "10h 5m")
|
||||
timeFileList = সময়
|
||||
// Used as header in a column indicating the number of times a file has been
|
||||
// downloaded
|
||||
downloadFileName = ডাউনলোড { $filename }
|
||||
downloadFileSize = ({ $size })
|
||||
unlockInputLabel = পাসওয়ার্ড লিখুন
|
||||
unlockInputPlaceholder = পাসওয়ার্ড
|
||||
unlockButtonLabel = আনলক করুন
|
||||
// Text and title used on the download link/button (indicates an action).
|
||||
downloadButtonLabel = ডাউনলোড
|
||||
downloadNotification = আপনার ডাউনলোড সম্পন্ন হয়েছে।
|
||||
@@ -29,6 +47,8 @@ downloadFinish = ডাউনলোড সম্পন্ন
|
||||
errorAltText = আপালোডে ত্রুটি
|
||||
errorPageHeader = কোন সমস্যা হয়েছে!
|
||||
errorPageLink = আরেকটি ফাইল পাঠান
|
||||
expiredPageHeader = এই লিঙ্কটির মেয়াদ শেষ হয়ে গেছে বা কখনই প্রথম স্থানে নেই!
|
||||
notSupportedHeader = আপনার ব্রাউজার সমর্থিত নয়।
|
||||
updateFirefox = Firefox হালনাগাদ করুন
|
||||
downloadFirefoxButtonSub = বিনামূল্যে ডাউনলোড
|
||||
uploadedFile = ফাইল
|
||||
@@ -49,3 +69,4 @@ footerLinkAbout = Test Pilot পরিচিতি
|
||||
footerLinkPrivacy = গোপনীয়তা
|
||||
footerLinkTerms = শর্তাবলী
|
||||
footerLinkCookies = কুকি
|
||||
changePasswordButton = পরিবর্তন
|
||||
|
||||
117
public/locales/bs/send.ftl
Normal file
@@ -0,0 +1,117 @@
|
||||
// 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.
|
||||
expireInfo = Link za vašu datoteku će isteći nakon { $downloadCount } ili { $timespan }.
|
||||
downloadCount = { $num ->
|
||||
[one] 1 preuzimanja
|
||||
[few] { $num } preuzimanja
|
||||
*[other] { $num } preuzimanja
|
||||
}
|
||||
timespanHours = { $num ->
|
||||
[one] 1 sat
|
||||
[few] { $num } sata
|
||||
*[other] { $num } sati
|
||||
}
|
||||
copyUrlFormLabelWithName = Iskopirajte i podijelite vezu da biste poslali datoteku: { $filename }
|
||||
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
|
||||
downloadsFileList = Preuzimanja
|
||||
// Used as header in a column indicating the amount of time left before a
|
||||
// download link expires (e.g. "10h 5m")
|
||||
timeFileList = Vrijeme
|
||||
// Used as header in a column indicating the number of times a file has been
|
||||
// downloaded
|
||||
downloadFileName = Preuzmi { $filename }
|
||||
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
|
||||
changePasswordButton = Promijeni
|
||||
passwordTryAgain = Netačna lozinka. Pokušajte ponovo.
|
||||
// This label is followed by the password needed to download a file
|
||||
passwordResult = Lozinka: { $password }
|
||||
reportIPInfringement = Prijavite IP prekršaj
|
||||
javascriptRequired = Firefox Send zahtjeva JavaScript
|
||||
whyJavascript = Zašto Firefox Send zahtjeva JavaScript?
|
||||
enableJavascript = Molimo omogućite JavaScript i pokušajte ponovo.
|
||||
// A short representation of a countdown timer containing the number of hours and minutes remaining as digits, example "13h 47m"
|
||||
expiresHoursMinutes = { $hours }h { $minutes }m
|
||||
// A short representation of a countdown timer containing the number of minutes remaining as digits, example "56m"
|
||||
expiresMinutes = { $minutes }m
|
||||
@@ -25,6 +25,15 @@ uploadingFileNotification = Notifica'm quan s'acabi de pujar.
|
||||
uploadSuccessConfirmHeader = Llest per enviar
|
||||
uploadSvgAlt = Puja
|
||||
uploadSuccessTimingHeader = L'enllaç al fitxer caducarà quan es baixi una vegada o d'aquí 24 hores.
|
||||
expireInfo = L'enllaç al fitxer caducarà en fer { $downloadCount } o d'aquí { $timespan }.
|
||||
downloadCount = { $num ->
|
||||
[one] 1 baixada
|
||||
*[other] { $num } baixades
|
||||
}
|
||||
timespanHours = { $num ->
|
||||
[one] 1 hora
|
||||
*[other] { $num } hores
|
||||
}
|
||||
copyUrlFormLabelWithName = Copieu l'enllaç i compartiu-lo per enviar el fitxer: { $filename }
|
||||
copyUrlFormButton = Copia al porta-retalls
|
||||
copiedUrl = Copiat!
|
||||
@@ -34,6 +43,10 @@ sendAnotherFileLink = Envieu un altre fitxer
|
||||
downloadAltText = Baixa
|
||||
downloadFileName = Baixeu { $filename }
|
||||
downloadFileSize = ({ $size })
|
||||
unlockInputLabel = Introduïu la contrasenya
|
||||
unlockInputPlaceholder = Contrasenya
|
||||
unlockButtonLabel = Desbloca
|
||||
downloadFileTitle = Baixa el fitxer xifrat
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
downloadMessage = Un amic us ha enviat un fitxer amb el Firefox Send, un servei que permet compartir fitxers mitjançant un enllaç segur, privat i xifrat que caduca automàticament per tal que les vostres dades no es conservin a Internet per sempre.
|
||||
// Text and title used on the download link/button (indicates an action).
|
||||
@@ -80,3 +93,9 @@ footerLinkAbout = Quant al Test Pilot
|
||||
footerLinkPrivacy = Privadesa
|
||||
footerLinkTerms = Condicions d'ús
|
||||
footerLinkCookies = Galetes
|
||||
requirePasswordCheckbox = Sol·licita una contrasenya per baixar aquest fitxer
|
||||
addPasswordButton = Afegeix una contrasenya
|
||||
passwordTryAgain = La contrasenya és incorrecta. Torneu-ho a provar.
|
||||
// This label is followed by the password needed to download a file
|
||||
passwordResult = Contrasenya: { $password }
|
||||
reportIPInfringement = Denuncieu una infracció de propietat intel·lectual
|
||||
|
||||
@@ -25,6 +25,15 @@ 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.
|
||||
expireInfo = Ri ruximöy ayakb'al xtik'is ruq'ijul chi rij ri { $downloadCount } o { $timespan }.
|
||||
downloadCount = { $num ->
|
||||
[one] 1 qasanïk
|
||||
*[other] { $num } taq qasanïk
|
||||
}
|
||||
timespanHours = { $num ->
|
||||
[one] 1 ramaj
|
||||
*[other] { $num } taq ramaj
|
||||
}
|
||||
copyUrlFormLabelWithName = Tiwachib'ëx chuqa' tikomonïx ri ximonel richin nitaq ri ayakb'äl: { $filename }
|
||||
copyUrlFormButton = Tiwachib'ëx pa molwuj
|
||||
copiedUrl = ¡Xwachib'ëx!
|
||||
@@ -32,8 +41,18 @@ deleteFileButton = Tiyuj yakb'äl
|
||||
sendAnotherFileLink = Titaq jun chik yakb'äl
|
||||
// Alternative text used on the download link/button (indicates an action).
|
||||
downloadAltText = Tiqasäx
|
||||
downloadsFileList = Taq qasanïk
|
||||
// Used as header in a column indicating the amount of time left before a
|
||||
// download link expires (e.g. "10h 5m")
|
||||
timeFileList = Q'ijul
|
||||
// Used as header in a column indicating the number of times a file has been
|
||||
// downloaded
|
||||
downloadFileName = Tiqasäx { $filename }
|
||||
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).
|
||||
@@ -80,3 +99,17 @@ 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
|
||||
changePasswordButton = Tijalwachïx
|
||||
passwordTryAgain = Itzel ri ewan tzij. Tatojtob'ej chik.
|
||||
// This label is followed by the password needed to download a file
|
||||
passwordResult = Ewan tzij: { $password }
|
||||
reportIPInfringement = Tiya' rutzijol ri Ritzelanik Ajna'oj Ichinil
|
||||
javascriptRequired = K'atzinel JavaScript chi re ri Firefox Send
|
||||
whyJavascript =
|
||||
enableJavascript = Titz'ij JavaScript richin nitojtob'ëx chik.
|
||||
// A short representation of a countdown timer containing the number of hours and minutes remaining as digits, example "13h 47m"
|
||||
expiresHoursMinutes = { $hours }r { $minutes }ch
|
||||
// A short representation of a countdown timer containing the number of minutes remaining as digits, example "56m"
|
||||
expiresMinutes = { $minutes }ch
|
||||
|
||||
@@ -25,6 +25,17 @@ uploadingFileNotification = Upozornit, až bude nahrávání dokončeno.
|
||||
uploadSuccessConfirmHeader = Připraveno k odeslání
|
||||
uploadSvgAlt = Nahrát
|
||||
uploadSuccessTimingHeader = Platnost odkazu na váš soubor vyprší po jeho prvním stažení, nebo po 24 hodinách.
|
||||
expireInfo = Platnost odkazu na váš soubor vyprší po { $downloadCount } nebo { $timespan }.
|
||||
downloadCount = { $num ->
|
||||
[one] jednom stažení
|
||||
[few] { $num } staženích
|
||||
*[other] { $num } staženích
|
||||
}
|
||||
timespanHours = { $num ->
|
||||
[one] hodina
|
||||
[few] hodiny
|
||||
*[other] hodin
|
||||
}
|
||||
copyUrlFormLabelWithName = Zkopírujte a sdílejte odkaz na váš soubor: { $filename }
|
||||
copyUrlFormButton = Zkopírovat do schránky
|
||||
copiedUrl = Zkopírováno!
|
||||
@@ -32,8 +43,14 @@ deleteFileButton = Smazat soubor
|
||||
sendAnotherFileLink = Poslat další soubor
|
||||
// Alternative text used on the download link/button (indicates an action).
|
||||
downloadAltText = Stáhnout
|
||||
downloadsFileList = Stažení
|
||||
timeFileList = Zbývá
|
||||
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.
|
||||
// Text and title used on the download link/button (indicates an action).
|
||||
@@ -80,3 +97,10 @@ 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
|
||||
changePasswordButton = Změnit
|
||||
passwordTryAgain = Špatné heslo. Zkuste to znovu.
|
||||
// This label is followed by the password needed to download a file
|
||||
passwordResult = Heslo: { $password }
|
||||
reportIPInfringement = Nahlásit porušení autorských práv
|
||||
|
||||
@@ -2,14 +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
|
||||
uploadPageBrowseButton1 = 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…
|
||||
@@ -17,14 +17,27 @@ 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
|
||||
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 = 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.
|
||||
expireInfo = Bydd y ddolen i'ch ffeil yn dod i ben ym mhen { $downloadCount } neu { $timespan }.
|
||||
downloadCount = { $num ->
|
||||
[one] Llwyth i lawr
|
||||
[two] Lwyth i lawr
|
||||
[few] Llwyth i lawr
|
||||
*[other] Llwyth i lawr
|
||||
}
|
||||
timespanHours = { $num ->
|
||||
[one] awr
|
||||
[two] awr
|
||||
[few] awr
|
||||
*[other] awr
|
||||
}
|
||||
copyUrlFormLabelWithName = Copïo a rhannu'r ddolen i anfon eich ffeil: { $filename }
|
||||
copyUrlFormButton = Copïo i'r clipfwrdd
|
||||
copiedUrl = Wedi eu copïo!
|
||||
@@ -32,8 +45,18 @@ deleteFileButton = Dileu ffeil
|
||||
sendAnotherFileLink = Anfon ffeil arall
|
||||
// Alternative text used on the download link/button (indicates an action).
|
||||
downloadAltText = Llwytho i lawr
|
||||
downloadsFileList = Llwythi
|
||||
// Used as header in a column indicating the amount of time left before a
|
||||
// download link expires (e.g. "10h 5m")
|
||||
timeFileList = Amser
|
||||
// Used as header in a column indicating the number of times a file has been
|
||||
// downloaded
|
||||
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).
|
||||
@@ -43,7 +66,7 @@ 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.
|
||||
sendYourFilesLink = Profwch Firefox Send
|
||||
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 = Gwall llwytho
|
||||
@@ -55,9 +78,9 @@ 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
|
||||
@@ -67,7 +90,7 @@ 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
|
||||
@@ -80,3 +103,17 @@ footerLinkAbout = Ynghylch Test Pilot
|
||||
footerLinkPrivacy = Preifatrwydd
|
||||
footerLinkTerms = Amodau
|
||||
footerLinkCookies = Cwcis
|
||||
requirePasswordCheckbox = Gosod angen cyfrinair i lwytho'r ffeil hon i lawr
|
||||
addPasswordButton = Ychwanegu Cyfrinair
|
||||
changePasswordButton = Newid
|
||||
passwordTryAgain = Cyfrinair anghywir. Ceisiwch eto.
|
||||
// This label is followed by the password needed to download a file
|
||||
passwordResult = Cyfrinair: { $password }
|
||||
reportIPInfringement = Adrodd ar Gamddefnydd o'r IP
|
||||
javascriptRequired = Mae Firefox Send angen JavaScript
|
||||
whyJavascript = Pam fod Firefox Send angen JavaScript?
|
||||
enableJavascript = Galluogwch JavaScript a cheisio eto.
|
||||
// A short representation of a countdown timer containing the number of hours and minutes remaining as digits, example "13h 47m"
|
||||
expiresHoursMinutes = { $hours }a { $minutes }m
|
||||
// A short representation of a countdown timer containing the number of minutes remaining as digits, example "56m"
|
||||
expiresMinutes = { $minutes }m
|
||||
|
||||
0
public/locales/da/send.ftl
Normal file
@@ -25,6 +25,15 @@ uploadingFileNotification = Mich benachrichtigen, wenn der Upload abgeschlossen
|
||||
uploadSuccessConfirmHeader = Bereit zum Senden
|
||||
uploadSvgAlt = Hochladen
|
||||
uploadSuccessTimingHeader = Der Link zu Ihrer Datei läuft nach einem Download oder in 24 Stunden ab.
|
||||
expireInfo = Der Link zu Ihrer Datei läuft nach { $downloadCount } oder { $timespan } ab.
|
||||
downloadCount = { $num ->
|
||||
[one] einem Download
|
||||
*[other] { $num } Downloads
|
||||
}
|
||||
timespanHours = { $num ->
|
||||
[one] einer Stunde
|
||||
*[other] { $num } Stunden
|
||||
}
|
||||
copyUrlFormLabelWithName = Kopieren und teilen Sie den Link, um Ihre Datei zu senden: { $filename }
|
||||
copyUrlFormButton = In Zwischenablage kopieren
|
||||
copiedUrl = Kopiert!
|
||||
@@ -32,8 +41,18 @@ deleteFileButton = Datei löschen
|
||||
sendAnotherFileLink = Eine weitere Datei senden
|
||||
// Alternative text used on the download link/button (indicates an action).
|
||||
downloadAltText = Herunterladen
|
||||
downloadsFileList = Downloads
|
||||
// Used as header in a column indicating the amount of time left before a
|
||||
// download link expires (e.g. "10h 5m")
|
||||
timeFileList = Zeit
|
||||
// Used as header in a column indicating the number of times a file has been
|
||||
// downloaded
|
||||
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).
|
||||
@@ -80,3 +99,17 @@ footerLinkAbout = Über Test Pilot
|
||||
footerLinkPrivacy = Datenschutz
|
||||
footerLinkTerms = Nutzungsbedingungen
|
||||
footerLinkCookies = Cookies
|
||||
requirePasswordCheckbox = Zum Herunterladen dieser Datei soll ein Passwort erforderlich sein
|
||||
addPasswordButton = Passwort hinzufügen
|
||||
changePasswordButton = Ändern
|
||||
passwordTryAgain = Falsches Passwort. Versuchen Sie es erneut.
|
||||
// This label is followed by the password needed to download a file
|
||||
passwordResult = Passwort: { $password }
|
||||
reportIPInfringement = IP-Verletzung melden
|
||||
javascriptRequired = Firefox Send benötigt JavaScript
|
||||
whyJavascript = Warum benötigt Firefox Send JavaScript?
|
||||
enableJavascript = Bitte akivieren Sie JavaScript und versuchen Sie es erneut.
|
||||
// A short representation of a countdown timer containing the number of hours and minutes remaining as digits, example "13h 47m"
|
||||
expiresHoursMinutes = { $hours }h { $minutes }m
|
||||
// A short representation of a countdown timer containing the number of minutes remaining as digits, example "56m"
|
||||
expiresMinutes = { $minutes }m
|
||||
|
||||
@@ -25,6 +25,19 @@ uploadingFileNotification = K wěsći daś, gaž nagraśe jo dokóńcone.
|
||||
uploadSuccessConfirmHeader = Gótowy za słanje
|
||||
uploadSvgAlt = Nagraś
|
||||
uploadSuccessTimingHeader = Wótkaz k wašej dataji pó 1 ześěgnjenju abo 24 góźinach spadnjo.
|
||||
expireInfo = Wótkaz k wašej dataji pó { $downloadCount } abo { $timespan } spadnjo.
|
||||
downloadCount = { $num ->
|
||||
[one] 1 ześěgnjenje
|
||||
[two] { $num } ześěgnjeni
|
||||
[few] { $num } ześěgnjenja
|
||||
*[other] { $num } ześěgnjenjow
|
||||
}
|
||||
timespanHours = { $num ->
|
||||
[one] 1 góźina
|
||||
[two] { $num } góźinje
|
||||
[few] { $num } góźiny
|
||||
*[other] { $num } góźin
|
||||
}
|
||||
copyUrlFormLabelWithName = Kopěrujśo a źělśo wótkaz, aby swóju dataju pósłał: { $filename }
|
||||
copyUrlFormButton = Do mjazywótkłada kopěrowaś
|
||||
copiedUrl = Kopěrowany!
|
||||
@@ -32,8 +45,18 @@ deleteFileButton = Dataju wulašowaś
|
||||
sendAnotherFileLink = Drugu dataju pósłaś
|
||||
// Alternative text used on the download link/button (indicates an action).
|
||||
downloadAltText = Ześěgnuś
|
||||
downloadsFileList = Ześěgnjenja
|
||||
// Used as header in a column indicating the amount of time left before a
|
||||
// download link expires (e.g. "10h 5m")
|
||||
timeFileList = Cas
|
||||
// Used as header in a column indicating the number of times a file has been
|
||||
// downloaded
|
||||
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).
|
||||
@@ -80,3 +103,17 @@ 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ś
|
||||
changePasswordButton = Změniś
|
||||
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 }
|
||||
reportIPInfringement = Pśekśiwjenje IP k wěsći daś
|
||||
javascriptRequired = Firefox Send JavaScript trjeba
|
||||
whyJavascript = Cogodla Firefox Send JavaScript trjeba?
|
||||
enableJavascript = Pšosym zmóžniśo JavaScript a wopytajśo hyšći raz.
|
||||
// A short representation of a countdown timer containing the number of hours and minutes remaining as digits, example "13h 47m"
|
||||
expiresHoursMinutes = { $hours } góź. { $minutes } min.
|
||||
// A short representation of a countdown timer containing the number of minutes remaining as digits, example "56m"
|
||||
expiresMinutes = { $minutes } min.
|
||||
|
||||
@@ -25,6 +25,15 @@ uploadingFileNotification = Ειδοποίηση όταν ολοκληρωθεί
|
||||
uploadSuccessConfirmHeader = Έτοιμο για αποστολή
|
||||
uploadSvgAlt = Μεταφόρτωση
|
||||
uploadSuccessTimingHeader = Ο σύνδεσμος του αρχείου σας θα λήξει έπειτα από 1 λήψη ή 24 ώρες.
|
||||
expireInfo = Ο σύνδεσμος για το αρχείο σας θα λήξει μετά από { $downloadCount } ή { $timespan }.
|
||||
downloadCount = { $num ->
|
||||
[one] 1 λήψη
|
||||
*[other] { $num } λήψεις
|
||||
}
|
||||
timespanHours = { $num ->
|
||||
[one] 1 ώρα
|
||||
*[other] { $num } ώρες
|
||||
}
|
||||
copyUrlFormLabelWithName = Αντιγράψτε και μοιραστείτε τον σύνδεσμο για αποστολή του αρχείου σας : { $filename }
|
||||
copyUrlFormButton = Αντιγραφή στο πρόχειρο
|
||||
copiedUrl = Αντιγράφτηκε!
|
||||
@@ -34,6 +43,10 @@ sendAnotherFileLink = Αποστολή άλλου αρχείου
|
||||
downloadAltText = Λήψη
|
||||
downloadFileName = Λήψη του { $filename }
|
||||
downloadFileSize = ({ $size })
|
||||
unlockInputLabel = Εισαγωγή κωδικού πρόσβασης
|
||||
unlockInputPlaceholder = Κωδικός πρόσβασης
|
||||
unlockButtonLabel = Ξεκλείδωμα
|
||||
downloadFileTitle = Λήψη κρυπτογραφημένου αρχείου
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
downloadMessage = Ο/Η φίλος/-η σας, σάς στέλνει ένα αρχείο με τη βοήθεια του Firefox Send, μιας υπηρεσίας που επιτρέπει τον διαμοιρασμό αρχείων μέσω ενός ασφαλούς, ιδιωτικού και κρυπτογραφημένου συνδέσμου που λήγει αυτόματα, ώστε να είστε σίγουροι ότι τα αρχεία σας δεν θα παραμείνουν στο διαδίκτυο για πάντα.
|
||||
// Text and title used on the download link/button (indicates an action).
|
||||
@@ -80,3 +93,9 @@ footerLinkAbout = Σχετικά με το Test Pilot
|
||||
footerLinkPrivacy = Απόρρητο
|
||||
footerLinkTerms = Όροι
|
||||
footerLinkCookies = Cookies
|
||||
requirePasswordCheckbox = Απαίτηση κωδικού πρόσβασης για λήψη του αρχείου
|
||||
addPasswordButton = Προσθήκη κωδικού πρόσβασης
|
||||
passwordTryAgain = Λάθος κωδικός πρόσβασης. Δοκιμάστε ξανά.
|
||||
// This label is followed by the password needed to download a file
|
||||
passwordResult = Κωδικός πρόσβασης: { $password }
|
||||
reportIPInfringement = Αναφορά παραβίασης IP
|
||||
|
||||
@@ -25,6 +25,15 @@ uploadingFileNotification = Notify me when the upload is complete.
|
||||
uploadSuccessConfirmHeader = Ready to Send
|
||||
uploadSvgAlt = Upload
|
||||
uploadSuccessTimingHeader = The link to your file will expire after 1 download or in 24 hours.
|
||||
expireInfo = The link to your file will expire after { $downloadCount } or { $timespan }.
|
||||
downloadCount = { $num ->
|
||||
[one] 1 download
|
||||
*[other] { $num } downloads
|
||||
}
|
||||
timespanHours = { $num ->
|
||||
[one] 1 hour
|
||||
*[other] { $num } hours
|
||||
}
|
||||
copyUrlFormLabelWithName = Copy and share the link to send your file: { $filename }
|
||||
copyUrlFormButton = Copy to clipboard
|
||||
copiedUrl = Copied!
|
||||
@@ -32,8 +41,18 @@ deleteFileButton = Delete file
|
||||
sendAnotherFileLink = Send another file
|
||||
// Alternative text used on the download link/button (indicates an action).
|
||||
downloadAltText = Download
|
||||
downloadsFileList = Downloads
|
||||
// Used as header in a column indicating the amount of time left before a
|
||||
// download link expires (e.g. "10h 5m")
|
||||
timeFileList = Time
|
||||
// Used as header in a column indicating the number of times a file has been
|
||||
// downloaded
|
||||
downloadFileName = Download { $filename }
|
||||
downloadFileSize = ({ $size })
|
||||
unlockInputLabel = Enter Password
|
||||
unlockInputPlaceholder = Password
|
||||
unlockButtonLabel = Unlock
|
||||
downloadFileTitle = Download Encrypted File
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
downloadMessage = Your friend is sending you a file with Firefox Send, a service that allows you to share files with a safe, private, and encrypted link that automatically expires to ensure your stuff does not remain online forever.
|
||||
// Text and title used on the download link/button (indicates an action).
|
||||
@@ -80,3 +99,17 @@ footerLinkAbout = About Test Pilot
|
||||
footerLinkPrivacy = Privacy
|
||||
footerLinkTerms = Terms
|
||||
footerLinkCookies = Cookies
|
||||
requirePasswordCheckbox = Require a password to download this file
|
||||
addPasswordButton = Add password
|
||||
changePasswordButton = Change
|
||||
passwordTryAgain = Incorrect password. Try again.
|
||||
// This label is followed by the password needed to download a file
|
||||
passwordResult = Password: { $password }
|
||||
reportIPInfringement = Report IP Infringement
|
||||
javascriptRequired = Firefox Send requires JavaScript
|
||||
whyJavascript = Why does Firefox Send require JavaScript?
|
||||
enableJavascript = Please enable JavaScript and try again.
|
||||
// A short representation of a countdown timer containing the number of hours and minutes remaining as digits, example "13h 47m"
|
||||
expiresHoursMinutes = { $hours }h { $minutes }m
|
||||
// A short representation of a countdown timer containing the number of minutes remaining as digits, example "56m"
|
||||
expiresMinutes = { $minutes }m
|
||||
@@ -25,6 +25,15 @@ uploadingFileNotification = Notificarme cuando la subida se complete.
|
||||
uploadSuccessConfirmHeader = Listo para enviar
|
||||
uploadSvgAlt = Subir
|
||||
uploadSuccessTimingHeader = El enlace al archivo expirará después de 1 descarga o en 24 horas.
|
||||
expireInfo = El enlace a tu archivo expirará después de { $downloadCount } o { $timespan }.
|
||||
downloadCount = { $num ->
|
||||
[one] 1 descarga
|
||||
*[other] { $num } descargas
|
||||
}
|
||||
timespanHours = { $num ->
|
||||
[one] 1 hora
|
||||
*[other] { $num } horas
|
||||
}
|
||||
copyUrlFormLabelWithName = Copiá y compartí el enlace para enviar tu archivo: { $filename }
|
||||
copyUrlFormButton = Copiar al portapapeles
|
||||
copiedUrl = ¡Copiado!
|
||||
@@ -32,8 +41,18 @@ deleteFileButton = Borrar archivo
|
||||
sendAnotherFileLink = Enviar otro archivo
|
||||
// Alternative text used on the download link/button (indicates an action).
|
||||
downloadAltText = Descargar
|
||||
downloadsFileList = Descargas
|
||||
// Used as header in a column indicating the amount of time left before a
|
||||
// download link expires (e.g. "10h 5m")
|
||||
timeFileList = Tiempo
|
||||
// Used as header in a column indicating the number of times a file has been
|
||||
// downloaded
|
||||
downloadFileName = Descargar { $filename }
|
||||
downloadFileSize = ({ $size })
|
||||
unlockInputLabel = Ingresar contraseña
|
||||
unlockInputPlaceholder = Contraseña
|
||||
unlockButtonLabel = Desbloquear
|
||||
downloadFileTitle = Descargar archivo cifrado
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
downloadMessage = Tu amigo te está enviando un archivo con Firefox Send, un servicio que permite compartir archivos con un enlace cifrado, seguro y privado que expira automáticamente para asegurar que tus datos no quedan en línea para siempre.
|
||||
// Text and title used on the download link/button (indicates an action).
|
||||
@@ -80,3 +99,17 @@ footerLinkAbout = Acerca de Test Pilot
|
||||
footerLinkPrivacy = Privacidad
|
||||
footerLinkTerms = Términos
|
||||
footerLinkCookies = Cookies
|
||||
requirePasswordCheckbox = Requerir contraseña para descargar este archivo
|
||||
addPasswordButton = Agregar contraseña
|
||||
changePasswordButton = Cambiar
|
||||
passwordTryAgain = Contraseña incorrecta. Intentá nuevamente.
|
||||
// This label is followed by the password needed to download a file
|
||||
passwordResult = Contraseña: { $password }
|
||||
reportIPInfringement = Informar violación de propiedad intelectual
|
||||
javascriptRequired = Firefox Send requiere JavaScript
|
||||
whyJavascript = ¿Por qué Firefox Send requiere Java Script?
|
||||
enableJavascript = Por favor habilite JavaScript y pruebe de nuevo.
|
||||
// A short representation of a countdown timer containing the number of hours and minutes remaining as digits, example "13h 47m"
|
||||
expiresHoursMinutes = h { $hours } m { $minutes }
|
||||
// A short representation of a countdown timer containing the number of minutes remaining as digits, example "56m"
|
||||
expiresMinutes = m { $minutes }
|
||||
|
||||
@@ -25,6 +25,15 @@ uploadingFileNotification = Notificarme cuando la subida sea completada.
|
||||
uploadSuccessConfirmHeader = Listo para enviar
|
||||
uploadSvgAlt = Subir
|
||||
uploadSuccessTimingHeader = El enlace a tu archivo expirará tras 1 descarga o en 24 horas.
|
||||
expireInfo = El enlace a tu archivo expirará después de { $downloadCount } o { $timespan }.
|
||||
downloadCount = { $num ->
|
||||
[one] 1 descarga
|
||||
*[other] { $num } descargas
|
||||
}
|
||||
timespanHours = { $num ->
|
||||
[one] 1 hora
|
||||
*[other] { $num } horas
|
||||
}
|
||||
copyUrlFormLabelWithName = Copia y comparte el enlace para enviar tu archivo: { $filename }
|
||||
copyUrlFormButton = Copiar al portapapeles
|
||||
copiedUrl = ¡Copiado!
|
||||
@@ -32,8 +41,18 @@ deleteFileButton = Eliminar archivo
|
||||
sendAnotherFileLink = Enviar otro archivo
|
||||
// Alternative text used on the download link/button (indicates an action).
|
||||
downloadAltText = Descargar
|
||||
downloadsFileList = Descargas
|
||||
// Used as header in a column indicating the amount of time left before a
|
||||
// download link expires (e.g. "10h 5m")
|
||||
timeFileList = Tiempo
|
||||
// Used as header in a column indicating the number of times a file has been
|
||||
// downloaded
|
||||
downloadFileName = Descargar { $filename }
|
||||
downloadFileSize = ({ $size })
|
||||
unlockInputLabel = Ingresar contraseña
|
||||
unlockInputPlaceholder = Contraseña
|
||||
unlockButtonLabel = Desbloquear
|
||||
downloadFileTitle = Bajar archivo cifrado
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
downloadMessage = Tu amigo te está enviando un archivo con Firefox Send, un servicio que te permite compartir archivos con un enlace seguro, privado y cifrado que expira automáticamente para asegurar que tus cosas no queden en línea de por vida.
|
||||
// Text and title used on the download link/button (indicates an action).
|
||||
@@ -80,3 +99,17 @@ footerLinkAbout = Acerca de Test Pilot
|
||||
footerLinkPrivacy = Privacidad
|
||||
footerLinkTerms = Términos
|
||||
footerLinkCookies = Cookies
|
||||
requirePasswordCheckbox = Requerir una contraseña para descargar este archivo
|
||||
addPasswordButton = Añadir contraseña
|
||||
changePasswordButton = Cambiar
|
||||
passwordTryAgain = Contraseña incorrecta. Vuelve a intentarlo.
|
||||
// This label is followed by the password needed to download a file
|
||||
passwordResult = Contraseña: { $password }
|
||||
reportIPInfringement = Reportar infracción de PI
|
||||
javascriptRequired = Firefox Send requiere JavaScript.
|
||||
whyJavascript = ¿Por qué Firefox Send requiere JavaScript?
|
||||
enableJavascript = Por favor, activa JavaScript y vuelve a intentarlo.
|
||||
// A short representation of a countdown timer containing the number of hours and minutes remaining as digits, example "13h 47m"
|
||||
expiresHoursMinutes = { $hours }h { $minutes }m
|
||||
// A short representation of a countdown timer containing the number of minutes remaining as digits, example "56m"
|
||||
expiresMinutes = { $minutes }m
|
||||
|
||||
@@ -25,6 +25,15 @@ uploadingFileNotification = Notificarme cuando se complete la subida.
|
||||
uploadSuccessConfirmHeader = Listo para enviar
|
||||
uploadSvgAlt = Subir
|
||||
uploadSuccessTimingHeader = El enlace al archivo caducará tras descargarlo una vez o en 24 horas.
|
||||
expireInfo = El enlace al archivo expirará tras { $downloadCount } o { $timespan }.
|
||||
downloadCount = { $num ->
|
||||
[one] 1 descarga
|
||||
*[other] { $num } descargas
|
||||
}
|
||||
timespanHours = { $num ->
|
||||
[one] 1 hora
|
||||
*[other] { $num } horas
|
||||
}
|
||||
copyUrlFormLabelWithName = Copiar y compartir el enlace para enviar tu archivo: { $filename }
|
||||
copyUrlFormButton = Copiar en el portapapeles
|
||||
copiedUrl = ¡Copiado!
|
||||
@@ -32,8 +41,18 @@ deleteFileButton = Eliminar archivo
|
||||
sendAnotherFileLink = Enviar otro archivo
|
||||
// Alternative text used on the download link/button (indicates an action).
|
||||
downloadAltText = Descargar
|
||||
downloadsFileList = Descargas
|
||||
// Used as header in a column indicating the amount of time left before a
|
||||
// download link expires (e.g. "10h 5m")
|
||||
timeFileList = Fecha
|
||||
// Used as header in a column indicating the number of times a file has been
|
||||
// downloaded
|
||||
downloadFileName = Descargar { $filename }
|
||||
downloadFileSize = ({ $size })
|
||||
unlockInputLabel = Introducir contraseña
|
||||
unlockInputPlaceholder = Contraseña
|
||||
unlockButtonLabel = Desbloquear
|
||||
downloadFileTitle = Descargar archivo encriptado
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
downloadMessage = Tu amigo te está enviando un archivo a través de Firefox Send, un servicio que te permite compartir archivos con un enlace seguro, privado y cifrado que caduca automáticamente para que tus cosas no sean accesibles en línea de por vida.
|
||||
// Text and title used on the download link/button (indicates an action).
|
||||
@@ -80,3 +99,17 @@ footerLinkAbout = Sobre Test Pilot
|
||||
footerLinkPrivacy = Privacidad
|
||||
footerLinkTerms = Términos
|
||||
footerLinkCookies = Cookies
|
||||
requirePasswordCheckbox = Requerir una contraseña para descargar este archivo
|
||||
addPasswordButton = Añadir contraseña
|
||||
changePasswordButton = Cambiar
|
||||
passwordTryAgain = Contraseña incorrecta. Inténtelo de nuevo.
|
||||
// This label is followed by the password needed to download a file
|
||||
passwordResult = Contraseña: { $password }
|
||||
reportIPInfringement = Denunciar vulneración de propiedad intelectual
|
||||
javascriptRequired = Firefox Send requiere JavaScript
|
||||
whyJavascript = ¿Por qué Firefox Send requiere JavaScript?
|
||||
enableJavascript = Por favor, activa JavaScript y vuelve a intentarlo.
|
||||
// A short representation of a countdown timer containing the number of hours and minutes remaining as digits, example "13h 47m"
|
||||
expiresHoursMinutes = { $hours }h { $minutes }m
|
||||
// A short representation of a countdown timer containing the number of minutes remaining as digits, example "56m"
|
||||
expiresMinutes = { $minutes }m
|
||||
|
||||
@@ -25,6 +25,14 @@ uploadingFileNotification = Avísame cuando la subida del archivo esté completa
|
||||
uploadSuccessConfirmHeader = Listo para enviar
|
||||
uploadSvgAlt = Subir
|
||||
uploadSuccessTimingHeader = El enlace a tu archivo expirará después de una descarga o en 24 horas.
|
||||
expireInfo = El enlace a tu archivo expirará después de { $downloadCount } o { $timespan }.
|
||||
downloadCount = { $num ->
|
||||
*[one] 1 descarga
|
||||
}
|
||||
timespanHours = { $num ->
|
||||
[one] 1 hora
|
||||
*[other] { $num } horas
|
||||
}
|
||||
copyUrlFormLabelWithName = Copiar y compartir el enlace para enviar tu archivo: ($filename)
|
||||
copyUrlFormButton = Copiar a portapapeles
|
||||
copiedUrl = ¡Copiado!
|
||||
@@ -32,8 +40,18 @@ deleteFileButton = Eliminar archivo
|
||||
sendAnotherFileLink = Enviar otro archivo
|
||||
// Alternative text used on the download link/button (indicates an action).
|
||||
downloadAltText = Descargar
|
||||
downloadsFileList = Descargas
|
||||
// Used as header in a column indicating the amount of time left before a
|
||||
// download link expires (e.g. "10h 5m")
|
||||
timeFileList = Hora
|
||||
// Used as header in a column indicating the number of times a file has been
|
||||
// downloaded
|
||||
downloadFileName = Descargar ($filename)
|
||||
downloadFileSize = ({ $size })
|
||||
unlockInputLabel = Ingresar contraseña
|
||||
unlockInputPlaceholder = Contraseña
|
||||
unlockButtonLabel = Desbloquear
|
||||
downloadFileTitle = Descargar archivo encriptado
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
downloadMessage = Tu amigo te está enviando un archivo a través de Firefox Send, un servicio que te permite compartir archivos con un enlace seguro, privado y encriptado que caduca automáticamente para que tus cosas no sean accesibles en línea de por vida.
|
||||
// Text and title used on the download link/button (indicates an action).
|
||||
@@ -80,3 +98,17 @@ footerLinkAbout = Acerca de Test Pilot
|
||||
footerLinkPrivacy = Privacidad
|
||||
footerLinkTerms = Términos
|
||||
footerLinkCookies = Cookies
|
||||
requirePasswordCheckbox = Se necesita una contraseña para descargar este archivo
|
||||
addPasswordButton = Agregar contraseña
|
||||
changePasswordButton = Cambiar
|
||||
passwordTryAgain = Contraseña incorrecta. Intenta de nuevo.
|
||||
// This label is followed by the password needed to download a file
|
||||
passwordResult = Contraseña: { $password }
|
||||
reportIPInfringement = Denunciar una infracción de PI
|
||||
javascriptRequired = Firefox Send requiere JavaScript
|
||||
whyJavascript = ¿Por qué Firefox Send requiere JavaScript?
|
||||
enableJavascript = Por favor, habilita JavaScript e intenta de nuevo.
|
||||
// A short representation of a countdown timer containing the number of hours and minutes remaining as digits, example "13h 47m"
|
||||
expiresHoursMinutes = { $hours }h { $minutes }m
|
||||
// A short representation of a countdown timer containing the number of minutes remaining as digits, example "56m"
|
||||
expiresMinutes = { $minutes }m
|
||||
|
||||
@@ -21,6 +21,7 @@ uploadingPageMessage = Aegumise sätteid saab muuta siis, kui faili üles laadit
|
||||
uploadingPageCancel = Katkesta üleslaadimine
|
||||
uploadCancelNotification = Üleslaadimine katkestati
|
||||
uploadingPageLargeFileMessage = Fail on suur ja selle üleslaadimine võib aega võtta.
|
||||
uploadingFileNotification = Teavita mind üleslaadimise lõppemisest.
|
||||
uploadSuccessConfirmHeader = Saatmiseks valmis
|
||||
uploadSvgAlt = Laadi üles
|
||||
uploadSuccessTimingHeader = Link failile aegub pärast 1. allalaadimist või 24 tunni möödumisel.
|
||||
@@ -33,6 +34,10 @@ sendAnotherFileLink = Saada järgmine fail
|
||||
downloadAltText = Laadi alla
|
||||
downloadFileName = Laadi fail { $filename } alla
|
||||
downloadFileSize = ({ $size })
|
||||
unlockInputLabel = Sisesta parool
|
||||
unlockInputPlaceholder = Parool
|
||||
unlockButtonLabel = Ava
|
||||
downloadFileTitle = Krüptitud faili allalaadimine
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
downloadMessage = Sulle on saadetud fail Firefox Sendiga - teenusega, mis lubab faile ohutult, privaatselt ja krüpteeritult jagada. Failid kustutatakse automaatselt, et need ei jääks internetti igaveseks.
|
||||
// Text and title used on the download link/button (indicates an action).
|
||||
@@ -64,6 +69,7 @@ copyFileList = Kopeeri URL
|
||||
// expiryFileList is used as a column header
|
||||
expiryFileList = Aegub
|
||||
deleteFileList = Kustuta
|
||||
nevermindButton = Ära pane tähele
|
||||
legalHeader = Tingimused ja privaatsusreeglid
|
||||
legalNoticeTestPilot = Firefox Send on praegu Test Piloti eksperiment ja sellele rakenduvad Test Piloti <a>teenusetingimused</a> ning <a>privaatsusreeglid</a>. Rohkem teavet selle eksperimendi ja kogutavate andmete kohta leiab <a>siit</a>.
|
||||
legalNoticeMozilla = Firefox Sendi veebilehe kasutamisele rakenduvad ka Mozilla <a>veebilehtede privaatsusreeglid</a> ja <a>veebilehtede teenusetingimused</a>.
|
||||
@@ -78,3 +84,8 @@ footerLinkAbout = Test Pilotist
|
||||
footerLinkPrivacy = Privaatsusest
|
||||
footerLinkTerms = Teenusetingimused
|
||||
footerLinkCookies = Küpsistest
|
||||
requirePasswordCheckbox = Selle faili allalaadimiseks nõutakse parooli
|
||||
addPasswordButton = Lisa parool
|
||||
passwordTryAgain = Vale parool. Palun proovi uuesti.
|
||||
// This label is followed by the password needed to download a file
|
||||
passwordResult = Parool: { $password }
|
||||
|
||||
@@ -25,6 +25,13 @@ uploadingFileNotification = هر وقت بارگذاری تمام شد به من
|
||||
uploadSuccessConfirmHeader = آماده برای ارسال
|
||||
uploadSvgAlt = بارگذاری
|
||||
uploadSuccessTimingHeader = پیوند به پرونده شما بعد از ۱ بار دانلود یا ۲۴ ساعت حذف خواهد شد.
|
||||
expireInfo = این پیوند به فایل شما پس از { $downloadCount } یا { $timespan } منقضی خواهد شد.
|
||||
downloadCount = { $num ->
|
||||
*[other] ۱ بارگذاری
|
||||
}
|
||||
timespanHours = { $num ->
|
||||
*[other] ۱ ساعت
|
||||
}
|
||||
copyUrlFormLabelWithName = برای ارسال پرونده پیوند آن را رونوشت و به اشتراک بگذارید: { $filename }
|
||||
copyUrlFormButton = رونوشت به کلیپبورد
|
||||
copiedUrl = رونوشت شد!
|
||||
@@ -32,8 +39,18 @@ deleteFileButton = حذف پرونده
|
||||
sendAnotherFileLink = ارسال پرونده دیگر
|
||||
// Alternative text used on the download link/button (indicates an action).
|
||||
downloadAltText = دریافت
|
||||
downloadsFileList = دریافتها
|
||||
// Used as header in a column indicating the amount of time left before a
|
||||
// download link expires (e.g. "10h 5m")
|
||||
timeFileList = زمان
|
||||
// Used as header in a column indicating the number of times a file has been
|
||||
// downloaded
|
||||
downloadFileName = بارگیری { $filename }
|
||||
downloadFileSize = ({ $size })
|
||||
unlockInputLabel = گذرواژه را وارد کنید
|
||||
unlockInputPlaceholder = گذرواژه
|
||||
unlockButtonLabel = باز کردن
|
||||
downloadFileTitle = دریافت پروندهٔ رمزنگاری شده
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
downloadMessage = دوست شما درحال ارسال پرونده ای به وسیله Firefox Send است، این سرویس این امکان را به شما میدهد تا پروندههای خود را به صورت ایمن،خصوصی و رمزنگاری شده به همراه پیوند انقضا خودکار همرسانی کنید تا اطمینان حاصل کنید چیزهای شما برای همیشه آنلاین باقی نخواهد ماند.
|
||||
// Text and title used on the download link/button (indicates an action).
|
||||
@@ -80,3 +97,17 @@ footerLinkAbout = درباره Test Pilot
|
||||
footerLinkPrivacy = حریمخصوصی
|
||||
footerLinkTerms = شرایط
|
||||
footerLinkCookies = کوکیها
|
||||
requirePasswordCheckbox = دریافت این پرونده نیاز به گذرواژه دارد
|
||||
addPasswordButton = افزودن گذرواژه
|
||||
changePasswordButton = تغییر
|
||||
passwordTryAgain = کلمه عبور اشتباه است. مجدد تلاش کنید.
|
||||
// This label is followed by the password needed to download a file
|
||||
passwordResult = گذرواژه: { $password }
|
||||
reportIPInfringement = گزارش تخلف IP
|
||||
javascriptRequired = Firefox Send نیازمند جاوااسکریپت است
|
||||
whyJavascript = چراFirefox Send جاوااسکریپت لازم دارد؟
|
||||
enableJavascript = لطفا جاوااسکریپت را فعال کنید و مجددا تلاش کنید.
|
||||
// A short representation of a countdown timer containing the number of hours and minutes remaining as digits, example "13h 47m"
|
||||
expiresHoursMinutes = { $hours }ساعت { $minutes }دقیقه
|
||||
// A short representation of a countdown timer containing the number of minutes remaining as digits, example "56m"
|
||||
expiresMinutes = { $minutes } دقیقه
|
||||
|
||||
@@ -25,6 +25,15 @@ uploadingFileNotification = M’envoyer une notification lorsque l’envoi est t
|
||||
uploadSuccessConfirmHeader = Paré à l’envoi
|
||||
uploadSvgAlt = Envoyer
|
||||
uploadSuccessTimingHeader = Le lien vers votre fichier expirera après le premier téléchargement ou au bout de 24 heures.
|
||||
expireInfo = Le lien vers votre fichier expirera après { $downloadCount } ou { $timespan }.
|
||||
downloadCount = { $num ->
|
||||
[one] 1 téléchargement
|
||||
*[other] { $num } téléchargements
|
||||
}
|
||||
timespanHours = { $num ->
|
||||
[one] 1 heure
|
||||
*[other] { $num } heures
|
||||
}
|
||||
copyUrlFormLabelWithName = Copiez et partagez le lien pour envoyer votre fichier : { $filename }
|
||||
copyUrlFormButton = Copier dans le presse-papiers
|
||||
copiedUrl = Lien copié !
|
||||
@@ -32,8 +41,18 @@ deleteFileButton = Supprimer le fichier
|
||||
sendAnotherFileLink = Envoyer un autre fichier
|
||||
// Alternative text used on the download link/button (indicates an action).
|
||||
downloadAltText = Télécharger
|
||||
downloadsFileList = Téléchargements
|
||||
// Used as header in a column indicating the amount of time left before a
|
||||
// download link expires (e.g. "10h 5m")
|
||||
timeFileList = Heure
|
||||
// Used as header in a column indicating the number of times a file has been
|
||||
// downloaded
|
||||
downloadFileName = Télécharger { $filename }
|
||||
downloadFileSize = ({ $size })
|
||||
unlockInputLabel = Saisissez le mot de passe
|
||||
unlockInputPlaceholder = Mot de passe
|
||||
unlockButtonLabel = Déverrouiller
|
||||
downloadFileTitle = Télécharger le fichier chiffré
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
downloadMessage = Votre ami⋅e vous a envoyé un fichier avec Firefox Send, un service qui permet d’envoyer des fichiers de façon sécurisée, confidentielle et chiffrée via un lien qui expire automatiquement pour que vos informations ne restent pas en ligne indéfiniment.
|
||||
// Text and title used on the download link/button (indicates an action).
|
||||
@@ -80,3 +99,17 @@ footerLinkAbout = À propos de Test Pilot
|
||||
footerLinkPrivacy = Confidentialité
|
||||
footerLinkTerms = Conditions d’utilisation
|
||||
footerLinkCookies = Cookies
|
||||
requirePasswordCheckbox = Exiger un mot de passe pour télécharger ce fichier
|
||||
addPasswordButton = Ajouter un mot de passe
|
||||
changePasswordButton = Changer
|
||||
passwordTryAgain = Mot de passe incorrect. Veuillez réessayer.
|
||||
// This label is followed by the password needed to download a file
|
||||
passwordResult = Mot de passe : { $password }
|
||||
reportIPInfringement = Signaler une violation de la p.i.
|
||||
javascriptRequired = Firefox Send nécessite JavaScript
|
||||
whyJavascript = Pourquoi Firefox Send nécessite-t-il JavaScript ?
|
||||
enableJavascript = Veuillez activer JavaScript puis réessayer.
|
||||
// A short representation of a countdown timer containing the number of hours and minutes remaining as digits, example "13h 47m"
|
||||
expiresHoursMinutes = { $hours } h { $minutes } min
|
||||
// A short representation of a countdown timer containing the number of minutes remaining as digits, example "56m"
|
||||
expiresMinutes = { $minutes } min
|
||||
|
||||
@@ -25,6 +25,15 @@ uploadingFileNotification = Jou in melding as de oplaad foltôge is.
|
||||
uploadSuccessConfirmHeader = Ree om te ferstjoeren
|
||||
uploadSvgAlt = Oplaad
|
||||
uploadSuccessTimingHeader = De keppeling nei jo bestân sil nei 1 download ferrinne of nei 24 oeren.
|
||||
expireInfo = De keppeling nei jo bestân sil nei { $downloadCount } of { $timespan } ferrinne.
|
||||
downloadCount = { $num ->
|
||||
[one] 1 download
|
||||
*[other] { $num } downloads
|
||||
}
|
||||
timespanHours = { $num ->
|
||||
[one] 1 oer
|
||||
*[other] { $num } oeren
|
||||
}
|
||||
copyUrlFormLabelWithName = Kopiearje en diel de keppeling om jo bestân te ferstjoeren: { $filename }
|
||||
copyUrlFormButton = Nei klamboerd kopiearje
|
||||
copiedUrl = Kopiearre!
|
||||
@@ -32,8 +41,18 @@ deleteFileButton = Bestân fuortsmite
|
||||
sendAnotherFileLink = Noch in bestân ferstjoere
|
||||
// Alternative text used on the download link/button (indicates an action).
|
||||
downloadAltText = Downloade
|
||||
downloadsFileList = Downloads
|
||||
// Used as header in a column indicating the amount of time left before a
|
||||
// download link expires (e.g. "10h 5m")
|
||||
timeFileList = Tiid
|
||||
// Used as header in a column indicating the number of times a file has been
|
||||
// downloaded
|
||||
downloadFileName = { $filename } downloade
|
||||
downloadFileSize = ({ $size })
|
||||
unlockInputLabel = Wachtwurd ynfiere
|
||||
unlockInputPlaceholder = Wachtwurd
|
||||
unlockButtonLabel = Deblokkearje
|
||||
downloadFileTitle = Fersifere bestân downloade
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
downloadMessage = Jo freon stjoert jo in best^n mei Firefox Send, in tsjinst dy't jo yn steat stelt bestannen te dielen mei in feilige, privee en fersifere keppeling dy't automatysk ferrint om wis te wêzen dat jo guod net foar altyd online bliuwt.
|
||||
// Text and title used on the download link/button (indicates an action).
|
||||
@@ -80,3 +99,17 @@ footerLinkAbout = Oer Test Pilot
|
||||
footerLinkPrivacy = Privacy
|
||||
footerLinkTerms = Betingsten
|
||||
footerLinkCookies = Cookies
|
||||
requirePasswordCheckbox = Om dit bestân te downloaden is in wachtwurd fereaske
|
||||
addPasswordButton = Wachtwurd tafoegje
|
||||
changePasswordButton = Wizigje
|
||||
passwordTryAgain = Net krekt wachtwurd. Probearje it opnij.
|
||||
// This label is followed by the password needed to download a file
|
||||
passwordResult = Wachtwurd: { $password }
|
||||
reportIPInfringement = IP-ynbrek melde
|
||||
javascriptRequired = Firefox Send fereasket JavaScript.
|
||||
whyJavascript = Werom hat Firefox Send JavaScript nedich?
|
||||
enableJavascript = Skeakelje JavaScript yn en probearje nochris.
|
||||
// A short representation of a countdown timer containing the number of hours and minutes remaining as digits, example "13h 47m"
|
||||
expiresHoursMinutes = { $hours }o { $minutes }m
|
||||
// A short representation of a countdown timer containing the number of minutes remaining as digits, example "56m"
|
||||
expiresMinutes = { $minutes }m
|
||||
|
||||
73
public/locales/he/send.ftl
Normal file
@@ -0,0 +1,73 @@
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
title = Firefox Send
|
||||
siteSubtitle = ניסוי אינטרנט
|
||||
siteFeedback = משוב
|
||||
uploadPageHeader = שיתוף קבצים פרטי, מוצפן
|
||||
uploadPageExplainer = לשלוח קבצים דרך קישור בטוח, פרטי ומוצפן שפג אוטומטית, כדי לוודא שהתכנים הפרטיים שלך לא יהיו ברשת לנצח.
|
||||
uploadPageLearnMore = מידע נוסף
|
||||
uploadPageDropMessage = יש לגרור קבצים לכאן כדי להתחיל בהעלאה
|
||||
uploadPageSizeMessage = להשגת ביצועים מיטביים, מוטב לשמור על הקובץ מתחת לגודל של 1 ג״ב
|
||||
uploadPageBrowseButton1 = נא לבחור קובץ להעלאה
|
||||
uploadPageMultipleFilesAlert = העלאה של מספר קבצים או ספריה אינה נתמכת כרגע.
|
||||
uploadPageBrowseButtonTitle = העלאת קובץ
|
||||
uploadingPageProgress = { $filename } ({ $size }) בהעלאה
|
||||
importingFile = מתבצע ייבוא...
|
||||
verifyingFile = מתבצע אימות…
|
||||
encryptingFile = מתבצעת הצפנה...
|
||||
decryptingFile = מתבצע פענוח...
|
||||
notifyUploadDone = ההעלאה שלך הסתיימה
|
||||
uploadingPageMessage = אחרי שהקובץ שלך יעלה, ניתן יהיה להגדיר אפשרויות תפוגה.
|
||||
uploadCancelNotification = ההעלאה שלך בוטלה.
|
||||
uploadingPageLargeFileMessage = קובץ זה גדול ועלול לקחת זמן להעלות אותו. סבלנות!
|
||||
uploadingFileNotification = נא להודיע לי כשתסתיים ההעלאה.
|
||||
uploadSuccessConfirmHeader = מוכן לשליחה
|
||||
uploadSvgAlt
|
||||
.alt = להעלות
|
||||
uploadSuccessTimingHeader = הקישור לקובץ שלך יפוג אחרי הורדה אחת או בעוד 24 שעות.
|
||||
copyUrlFormLabelWithName = ניתן להעתיק ולשתף את הקישור כדי לשלוח את הקובץ שלך: { $filename }
|
||||
copiedUrl = הועתק!
|
||||
deleteFileButton = מחיקת קובץ
|
||||
.title = מחיקת קובץ
|
||||
sendAnotherFileLink = שליחת קובץ נוסף
|
||||
.title = שליחת קובץ נוסף
|
||||
// Alternative text used on the download link/button (indicates an action).
|
||||
downloadAltText
|
||||
.alt = הורדה
|
||||
downloadFileName = ההורדה נכשלה
|
||||
downloadFileSize = ({ $size })
|
||||
// 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 })
|
||||
downloadingPageProgress = בהורדה: { $filename } ({ $size })
|
||||
errorAltText
|
||||
.alt = תקלה בהעלאה
|
||||
errorPageHeader = משהו השתבש!
|
||||
errorPageLink = שליחת קובץ נוסף
|
||||
fileTooBig = הקובץ הזה גדול מידי להעלאה. עליו להיות קטן מ־{ $size }.
|
||||
linkExpiredAlt
|
||||
.alt = קישור פג
|
||||
notSupportedHeader = הדפדפן שלך לא נתמך.
|
||||
notSupportedLink = למה אין תמיכה בדפדפן שלי?
|
||||
downloadFirefoxButtonSub = הורדה בחינם
|
||||
uploadedFile = קובץ
|
||||
copyFileList = העתקת כתובת
|
||||
// expiryFileList is used as a column header
|
||||
expiryFileList = יפוג בעוד
|
||||
deleteFileList = מחיקה
|
||||
nevermindButton = לא משנה
|
||||
legalHeader = תנאי שירות ופרטיות
|
||||
deletePopupText = למחוק דף זה?
|
||||
deletePopupYes = כן
|
||||
deletePopupCancel = ביטול
|
||||
deleteButtonHover
|
||||
.title = מחיקה
|
||||
copyUrlHover
|
||||
.title = העתקת קישור
|
||||
footerLinkLegal = מידע משפטי
|
||||
footerLinkPrivacy = פרטיות
|
||||
footerLinkTerms = תנאי שימוש
|
||||
footerLinkCookies = קובצי עוגיות
|
||||
@@ -25,6 +25,19 @@ uploadingFileNotification = Zdźělić, hdyž nahraće je dokónčene.
|
||||
uploadSuccessConfirmHeader = Hotowy za słanje
|
||||
uploadSvgAlt = Nahrać
|
||||
uploadSuccessTimingHeader = Wotkaz k wašej dataji po 1 sćehnjenju abo 24 hodźinach spadnje.
|
||||
expireInfo = Wotkaz k wašej dataji po { $downloadCount } abo { $timespan } spadnje.
|
||||
downloadCount = { $num ->
|
||||
[one] 1 sćehnjenje
|
||||
[two] { $num } sćehnjeni
|
||||
[few] { $num } sćehnjenja
|
||||
*[other] { $num } sćehnjenjow
|
||||
}
|
||||
timespanHours = { $num ->
|
||||
[one] 1 hodźina
|
||||
[two] { $num } hodźinje
|
||||
[few] { $num } hodźiny
|
||||
*[other] { $num } hodźin
|
||||
}
|
||||
copyUrlFormLabelWithName = Kopěrujće a dźělće wotkaz, zo byšće swoju dataju pósłał: { $filename }
|
||||
copyUrlFormButton = Do mjezyskłada kopěrować
|
||||
copiedUrl = Kopěrowany!
|
||||
@@ -32,8 +45,18 @@ deleteFileButton = Dataju zhašeć
|
||||
sendAnotherFileLink = Druhu dataju pósłać
|
||||
// Alternative text used on the download link/button (indicates an action).
|
||||
downloadAltText = Sćahnyć
|
||||
downloadsFileList = Sćehnjenja
|
||||
// Used as header in a column indicating the amount of time left before a
|
||||
// download link expires (e.g. "10h 5m")
|
||||
timeFileList = Čas
|
||||
// Used as header in a column indicating the number of times a file has been
|
||||
// downloaded
|
||||
downloadFileName = { $filename } sćahnyć
|
||||
downloadFileSize = ({ $size })
|
||||
unlockInputLabel = Hesło zapodać
|
||||
unlockInputPlaceholder = Hesło
|
||||
unlockButtonLabel = Wotewrěć
|
||||
downloadFileTitle = Zaklučowanu dataju sćahnyć
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
downloadMessage = Waš přećel wam dataju z Firefox Send sćele, słužba, kotraž wam zmóžnja, dataje přez wěsty, priwatny a zaklučowany wotkaz dźělić, kotryž awtomatisce spadnje, zo njebychu waše daty na přeco online wostawali.
|
||||
// Text and title used on the download link/button (indicates an action).
|
||||
@@ -80,3 +103,17 @@ footerLinkAbout = Wo Test Pilot
|
||||
footerLinkPrivacy = Priwatnosć
|
||||
footerLinkTerms = Wuměnjenja
|
||||
footerLinkCookies = Placki
|
||||
requirePasswordCheckbox = Žadajće sej hesło za sćehnjenje tuteje dataje
|
||||
addPasswordButton = Hesło přidać
|
||||
changePasswordButton = Změnić
|
||||
passwordTryAgain = Wopačne hesło. Prošu spytajće hišće raz.
|
||||
// This label is followed by the password needed to download a file
|
||||
passwordResult = Hesło: { $password }
|
||||
reportIPInfringement = Zranjenje IP zdźělić
|
||||
javascriptRequired = Firefox Send JavaScript trjeba
|
||||
whyJavascript = Čehodla Firefox Send JavaScript trjeba?
|
||||
enableJavascript = Prošu zmóžńće JavaScript a spytajće hišće raz.
|
||||
// A short representation of a countdown timer containing the number of hours and minutes remaining as digits, example "13h 47m"
|
||||
expiresHoursMinutes = { $hours } hodź. { $minutes } mjeń.
|
||||
// A short representation of a countdown timer containing the number of minutes remaining as digits, example "56m"
|
||||
expiresMinutes = { $minutes } mjeń.
|
||||
|
||||
@@ -25,6 +25,15 @@ uploadingFileNotification = Értesítsen, ha a feltöltés elkészült.
|
||||
uploadSuccessConfirmHeader = Küldésre kész
|
||||
uploadSvgAlt = Feltöltés
|
||||
uploadSuccessTimingHeader = A fájl hivatkozása lejár 1 letöltés vagy 24 óra múlva.
|
||||
expireInfo = A fájlhoz tartozó hivatkozás { $downloadCount } vagy { $timespan } múlva lejár.
|
||||
downloadCount = { $num ->
|
||||
[one] 1 letöltés
|
||||
*[other] { $num } letöltés
|
||||
}
|
||||
timespanHours = { $num ->
|
||||
[one] 1 óra
|
||||
*[other] { $num } óra
|
||||
}
|
||||
copyUrlFormLabelWithName = Másolja és ossza meg a hivatkozást a fájl küldéséhez: { $filename }
|
||||
copyUrlFormButton = Vágólapra másolás
|
||||
copiedUrl = Másolva!
|
||||
@@ -32,8 +41,18 @@ deleteFileButton = Fájl törlése
|
||||
sendAnotherFileLink = Még egy fájl küldése
|
||||
// Alternative text used on the download link/button (indicates an action).
|
||||
downloadAltText = Letöltés
|
||||
downloadsFileList = Letöltések
|
||||
// Used as header in a column indicating the amount of time left before a
|
||||
// download link expires (e.g. "10h 5m")
|
||||
timeFileList = Idő
|
||||
// Used as header in a column indicating the number of times a file has been
|
||||
// downloaded
|
||||
downloadFileName = { $filename } letöltése
|
||||
downloadFileSize = ({ $size })
|
||||
unlockInputLabel = Adja meg a jelszót
|
||||
unlockInputPlaceholder = Jelszó
|
||||
unlockButtonLabel = Feloldás
|
||||
downloadFileTitle = Titkosított fájl letöltése
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
downloadMessage = Az ismerőse egy fájlt küld a Firefox Senddel, egy olyan fájlmegosztó szolgáltatással, amely biztonságos, privát és titkosított hivatkozáson keresztül működik, amely automatikusan elévül, így biztosítva hogy a dolga ne maradjon örökre online.
|
||||
// Text and title used on the download link/button (indicates an action).
|
||||
@@ -80,3 +99,17 @@ footerLinkAbout = A Tesztpilóta névjegye
|
||||
footerLinkPrivacy = Adatvédelem
|
||||
footerLinkTerms = Feltételek
|
||||
footerLinkCookies = Sütik
|
||||
requirePasswordCheckbox = Jelszó megkövetelése a fájl letöltéséhez
|
||||
addPasswordButton = Jelszó hozzáadása
|
||||
changePasswordButton = Módosítás
|
||||
passwordTryAgain = Helytelen jelszó. Próbálja meg újra.
|
||||
// This label is followed by the password needed to download a file
|
||||
passwordResult = Jelszó: { $password }
|
||||
reportIPInfringement = Szellemi tulajdon megsértésének bejelentése
|
||||
javascriptRequired = A Firefox Sendhez JavaScript szükséges
|
||||
whyJavascript = Miért van szükség JavaScriptre a Firefox Sendhez?
|
||||
enableJavascript = Kérjük engedélyezze a JavaScriptet, majd próbálkozzon újra.
|
||||
// A short representation of a countdown timer containing the number of hours and minutes remaining as digits, example "13h 47m"
|
||||
expiresHoursMinutes = { $hours }ó { $minutes }p
|
||||
// A short representation of a countdown timer containing the number of minutes remaining as digits, example "56m"
|
||||
expiresMinutes = { $minutes }p
|
||||
|
||||
115
public/locales/ia/send.ftl
Normal file
@@ -0,0 +1,115 @@
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
title = Firefox Send
|
||||
siteSubtitle = un experimento web
|
||||
siteFeedback = Reaction
|
||||
uploadPageHeader = Compartimento de files private e cryptate
|
||||
uploadPageExplainer = Invia files per un ligamine secur, private e cryptate que automaticamente expira pro assecurar que tu datos non resta in linea per sempre.
|
||||
uploadPageLearnMore = Saper plus
|
||||
uploadPageDropMessage = Depone ci tu file pro comenciar a lo cargar
|
||||
uploadPageSizeMessage = Pro evitar problemas, mantene tu file sub 1GB
|
||||
uploadPageBrowseButton = Elige un file sur tu computator
|
||||
uploadPageBrowseButton1 = Elige un file a cargar
|
||||
uploadPageMultipleFilesAlert = Le cargamento de plure files o de un plica actualmente non es supportate.
|
||||
uploadPageBrowseButtonTitle = Cargar le file
|
||||
uploadingPageProgress = Cargamento de { $filename } ({ $size })
|
||||
importingFile = Importation…
|
||||
verifyingFile = Verifica…
|
||||
encryptingFile = Cryptation...
|
||||
decryptingFile = Decryptation…
|
||||
notifyUploadDone = Cargamento terminate
|
||||
uploadingPageMessage = Post cargate tu file, tu potera definir le optiones de expiration.
|
||||
uploadingPageCancel = Cancellar le cargamento
|
||||
uploadCancelNotification = Cargamento cancellate.
|
||||
uploadingPageLargeFileMessage = Iste file es grande e pote prender multe tempore pro le cargamento. Patientia!
|
||||
uploadingFileNotification = Notificar me quando le cargamento es complete.
|
||||
uploadSuccessConfirmHeader = Preste a inviar
|
||||
uploadSvgAlt = Cargamento
|
||||
uploadSuccessTimingHeader = Le ligamine a tu file expirara post un discargamento o in 24 horas.
|
||||
expireInfo = Le ligamine a tu file expirara post { $downloadCount } o { $timespan }
|
||||
downloadCount = { $num ->
|
||||
[one] discargamento
|
||||
*[other] discargamentos
|
||||
}
|
||||
timespanHours = { $num ->
|
||||
[one] hora
|
||||
*[other] horas
|
||||
}
|
||||
copyUrlFormLabelWithName = Copia e comparti le ligamine pro inviar tu file: { $filename }
|
||||
copyUrlFormButton = Copiar al area de transferentia
|
||||
copiedUrl = Copiate!
|
||||
deleteFileButton = Deler le file
|
||||
sendAnotherFileLink = Inviar un altere file
|
||||
// Alternative text used on the download link/button (indicates an action).
|
||||
downloadAltText = Discargar
|
||||
downloadsFileList = Discargamentos
|
||||
// Used as header in a column indicating the amount of time left before a
|
||||
// download link expires (e.g. "10h 5m")
|
||||
timeFileList = Tempore
|
||||
// Used as header in a column indicating the number of times a file has been
|
||||
// downloaded
|
||||
downloadFileName = Discargar { $filename }
|
||||
downloadFileSize = ({ $size })
|
||||
unlockInputLabel = Insere le contrasigno
|
||||
unlockInputPlaceholder = Contrasigno
|
||||
unlockButtonLabel = Disblocar
|
||||
downloadFileTitle = Discargar le file cryptate
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
downloadMessage = Tu amico te invia un file per Firefox Send, un servicio que te permitte de compartir files per un ligamine secur, private e cryptate, que expira automaticamente pro te assecurar que tu datos non resta online per sempre.
|
||||
// Text and title used on the download link/button (indicates an action).
|
||||
downloadButtonLabel = Discargar
|
||||
downloadNotification = Tu discargamento es completate.
|
||||
downloadFinish = Discargamento completate
|
||||
// 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.
|
||||
sendYourFilesLink = Proba Firefox Send
|
||||
downloadingPageProgress = Discargamento de { $filename } ({ $size })
|
||||
downloadingPageMessage = Per favor lassa iste scheda aperte durante que nos prende tu file e lo decifra.
|
||||
errorAltText = Error de cargamento.
|
||||
errorPageHeader = Un error occurreva!
|
||||
errorPageMessage = Un error occurreva durante le cargamento del file.
|
||||
errorPageLink = Inviar un altere file
|
||||
fileTooBig = Iste file es troppo grande pro lo cargar. Illo debe ser inferior a { $size }.
|
||||
linkExpiredAlt = Ligamine expirate
|
||||
expiredPageHeader = Iste ligamine expirava o illo non existeva jammais!
|
||||
notSupportedHeader = Tu navigator non es supportate
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
notSupportedDetail = Infelicemente iste navigator non supporta le nove technologias web que move Firefox Send. Tu besonia de probar un altere navigator. Nos recommenda Firefox!
|
||||
notSupportedLink = Perque iste navigator non es supportate?
|
||||
notSupportedOutdatedDetail = Infelicemente iste version de Firefox non supporta le nove technologias web que move Firefox Send. Tu besonia de actualisar tu navigator.
|
||||
updateFirefox = Actualisar Firefox
|
||||
downloadFirefoxButtonSub = Discargamento gratuite
|
||||
uploadedFile = File
|
||||
copyFileList = Copiar le URL
|
||||
// expiryFileList is used as a column header
|
||||
expiryFileList = Expira in
|
||||
deleteFileList = Deler
|
||||
nevermindButton = No, gratias
|
||||
legalHeader = Terminos & confidentialitate
|
||||
legalNoticeTestPilot = Firefox Send es actualmente un Experimento pilota, e subjecte a <a>Terminos de servicio</a> e <a>Notitia de confidentialitate</a> de Experimento pilota. Tu pote saper plus re iste experimento e su recolta de datos <a>hic</a>.
|
||||
legalNoticeMozilla = Le uso del sito web de Firefox Send es anque subjecte a <a>Notitia de confidentialitate de sito web</a> e <a>Terminos de servicio sito web</a>.
|
||||
deletePopupText = Deler iste file?
|
||||
deletePopupYes =
|
||||
deletePopupCancel = Cancellar
|
||||
deleteButtonHover = Deler
|
||||
copyUrlHover = Copiar le URL
|
||||
footerLinkLegal = Legal
|
||||
// Test Pilot is a proper name and should not be localized.
|
||||
footerLinkAbout = Re Test Pilot
|
||||
footerLinkPrivacy = Confidentialitate
|
||||
footerLinkTerms = Terminos
|
||||
footerLinkCookies = Cookies
|
||||
requirePasswordCheckbox = Requirer un contrasigno pro discargar iste file
|
||||
addPasswordButton = Adder contrasigno
|
||||
changePasswordButton = Cambiar
|
||||
passwordTryAgain = Contrasigno incorrecte. Retenta.
|
||||
// This label is followed by the password needed to download a file
|
||||
passwordResult = Contrasigno: { $password }
|
||||
reportIPInfringement = Reportar un violation de proprietate intellectual
|
||||
javascriptRequired = Firefox Send require JavaScript
|
||||
whyJavascript = Proque Firefox Send require JavaScript?
|
||||
enableJavascript = Por favor activa JavaScript e tenta novemente.
|
||||
// A short representation of a countdown timer containing the number of hours and minutes remaining as digits, example "13h 47m"
|
||||
expiresHoursMinutes = { $hours }h { $minutes }m
|
||||
// A short representation of a countdown timer containing the number of minutes remaining as digits, example "56m"
|
||||
expiresMinutes = { $minutes }m
|
||||
@@ -8,9 +8,11 @@ uploadPageLearnMore = Pelajari lebih lanjut
|
||||
uploadPageDropMessage = Lepas berkas Anda di sini untuk mulai mengunggah
|
||||
uploadPageSizeMessage = Untuk pengoperasian yang paling andal, sebaiknya jaga berkas Anda di bawah 1GB
|
||||
uploadPageBrowseButton = Pilih berkas pada komputer Anda
|
||||
uploadPageBrowseButton1 = Pilih berkas untuk diunggah
|
||||
.title = Pilih berkas untuk diunggah
|
||||
uploadPageMultipleFilesAlert = Saat ini belum mendukung pengunggahan beberapa berkas atau folder.
|
||||
uploadPageBrowseButtonTitle = Unggah berkas
|
||||
uploadingPageHeader = Mengunggah Berkas Anda
|
||||
uploadingPageProgress = Mengunggah { $filename } ({ $size })
|
||||
importingFile = Mengimpor…
|
||||
verifyingFile = Memverifikasi…
|
||||
encryptingFile = Mengenkripsi...
|
||||
@@ -24,6 +26,13 @@ uploadingFileNotification = Beri tahu saya ketika unggahan telah selesai.
|
||||
uploadSuccessConfirmHeader = Siap untuk Dikirim
|
||||
uploadSvgAlt = Unggah
|
||||
uploadSuccessTimingHeader = Tautan ke berkas Anda akan berakhir setelah 1 unduhan atau dalam 24 jam.
|
||||
expireInfo = Tautan ke berkas Anda akan kedaluwarsa setelah { $downloadCount } atau { $timespan }.
|
||||
downloadCount = { $num ->
|
||||
*[other] { $number } unduhan
|
||||
}
|
||||
timespanHours = { $num ->
|
||||
*[other] { $number } jam
|
||||
}
|
||||
copyUrlFormLabelWithName = Salin dan bagikan tautan untuk mengirim berkas Anda: { $filename }
|
||||
copyUrlFormButton = Salin ke papan klip
|
||||
copiedUrl = Tersalin!
|
||||
@@ -33,12 +42,18 @@ sendAnotherFileLink = Kirim berkas lain
|
||||
downloadAltText = Unduh
|
||||
downloadFileName = Unduh { $filename }
|
||||
downloadFileSize = ({ $size })
|
||||
unlockInputLabel = Masukkan Sandi
|
||||
unlockInputPlaceholder = Sandi
|
||||
unlockButtonLabel = Buka
|
||||
downloadFileTitle = Unduh Berkas Terenkripsi
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
downloadMessage = Teman Anda mengirimkan berkas dengan Firefox Send, layanan yang memungkinkan Anda berbagi berkas dengan tautan yang aman, pribadi, dan terenkripsi yang secara otomatis berakhir untuk memastikan berkas Anda tidak daring selamanya.
|
||||
// Text and title used on the download link/button (indicates an action).
|
||||
downloadButtonLabel = Unduh
|
||||
downloadNotification = Unduhan Anda telah selesai.
|
||||
downloadFinish = Unduhan Selesai
|
||||
// This message is displayed when uploading or downloading a file, e.g. "(1,3 MB of 10 MB)".
|
||||
fileSizeProgress = ({ $partialSize } dari { $totalSize })
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
sendYourFilesLink = Coba Firefox Send
|
||||
downloadingPageProgress = Mengunduh { $filename } ({ $size })
|
||||
@@ -77,3 +92,9 @@ footerLinkAbout = Tentang Test Pilot
|
||||
footerLinkPrivacy = Privasi
|
||||
footerLinkTerms = Ketentuan
|
||||
footerLinkCookies = Kuki
|
||||
requirePasswordCheckbox = Membutuhkan sandi untuk mengunduh berkas ini
|
||||
addPasswordButton = Tambahkan Sandi
|
||||
passwordTryAgain = Sandi salah. Silakan coba lagi.
|
||||
// This label is followed by the password needed to download a file
|
||||
passwordResult = Sandi: { $password }
|
||||
reportIPInfringement = Laporkan Pelanggaran IP
|
||||
|
||||
@@ -25,6 +25,15 @@ uploadingFileNotification = Invia una notifica quando il caricamento è completa
|
||||
uploadSuccessConfirmHeader = Pronto per l’invio
|
||||
uploadSvgAlt = Carica
|
||||
uploadSuccessTimingHeader = Il link al file scadrà dopo 1 download o in 24 ore.
|
||||
expireInfo = Il link a questo file scadrà dopo { $downloadCount } o { $timespan }.
|
||||
downloadCount = { $num ->
|
||||
[one] 1 download
|
||||
*[other] { $num } download
|
||||
}
|
||||
timespanHours = { $num ->
|
||||
[one] 1 ora
|
||||
*[other] { $num } ore
|
||||
}
|
||||
copyUrlFormLabelWithName = Copia e condividi il link per inviare il tuo file: { $filename }
|
||||
copyUrlFormButton = Copia negli appunti
|
||||
copiedUrl = Copiato
|
||||
@@ -32,8 +41,18 @@ deleteFileButton = Elimina file
|
||||
sendAnotherFileLink = Invia un altro file
|
||||
// Alternative text used on the download link/button (indicates an action).
|
||||
downloadAltText = Scarica
|
||||
downloadsFileList = Download
|
||||
// Used as header in a column indicating the amount of time left before a
|
||||
// download link expires (e.g. "10h 5m")
|
||||
timeFileList = Scadenza
|
||||
// Used as header in a column indicating the number of times a file has been
|
||||
// downloaded
|
||||
downloadFileName = Scarica { $filename }
|
||||
downloadFileSize = ({ $size })
|
||||
unlockInputLabel = Inserire la password
|
||||
unlockInputPlaceholder = Password
|
||||
unlockButtonLabel = Sblocca
|
||||
downloadFileTitle = Scarica il file crittato
|
||||
// Firefox Send is a brand name and should not be localized.
|
||||
downloadMessage = Qualcuno ha utilizzato Firefox Send per inviarti un file. Si tratta di un servizio che permette di condividere file in modo sicuro, riservato e crittato, utilizzando un link che smette di funzionare automaticamente dopo un certo periodo di tempo, garantendo così che i tuoi dati non rimangano online per sempre.
|
||||
// Text and title used on the download link/button (indicates an action).
|
||||
@@ -80,3 +99,17 @@ footerLinkAbout = Informazioni su Test Pilot
|
||||
footerLinkPrivacy = Privacy
|
||||
footerLinkTerms = Condizioni di utilizzo
|
||||
footerLinkCookies = Cookie
|
||||
requirePasswordCheckbox = Richiedi una password per poter scaricare questo file
|
||||
addPasswordButton = Aggiungi password
|
||||
changePasswordButton = Modifica
|
||||
passwordTryAgain = Password errata, riprovare.
|
||||
// This label is followed by the password needed to download a file
|
||||
passwordResult = Password: { $password }
|
||||
reportIPInfringement = Segnala violazione della proprietà intellettuale
|
||||
javascriptRequired = Firefox Send richiede JavaScript
|
||||
whyJavascript = Perché Firefox Send richiede JavaScript?
|
||||
enableJavascript = Attiva JavaScript e riprova.
|
||||
// A short representation of a countdown timer containing the number of hours and minutes remaining as digits, example "13h 47m"
|
||||
expiresHoursMinutes = { $hours }h { $minutes }m
|
||||
// A short representation of a countdown timer containing the number of minutes remaining as digits, example "56m"
|
||||
expiresMinutes = { $minutes }m
|
||||
|
||||