24
1
Fork 0

hook multifile to ui

This commit is contained in:
Emily 2018-07-31 11:09:18 -07:00
parent e42ad175db
commit c9ae76b209
77 changed files with 1528 additions and 1111 deletions

View File

@ -65,6 +65,14 @@ export async function fileInfo(id, owner_token) {
throw new Error(response.status); throw new Error(response.status);
} }
export async function hasPassword(id) {
const response = await fetch(`/api/exists/${id}`);
if (response.ok) {
return response.json();
}
throw new Error(response.status);
}
export async function metadata(id, keychain) { export async function metadata(id, keychain) {
const result = await fetchWithAuthAndRetry( const result = await fetchWithAuthAndRetry(
`/api/metadata/${id}`, `/api/metadata/${id}`,

View File

@ -17,6 +17,10 @@ export default class Archive {
return this.files.reduce((total, file) => total + file.size, 0); return this.files.reduce((total, file) => total + file.size, 0);
} }
get numFiles() {
return this.files.length;
}
get manifest() { get manifest() {
return { return {
files: this.files.map(file => ({ files: this.files.map(file => ({

View File

@ -1,14 +1,16 @@
:root { :root {
--pageBGColor: #fff; --pageBGColor: #fff;
--primaryControlBGColor: #0297f8; --lightControlBGColor: #e6e6e6;
--primaryControlBGColor: #0a84ff;
--primaryControlFGColor: #fff; --primaryControlFGColor: #fff;
--primaryControlHoverColor: #0287e8; --primaryControlHoverColor: #0473e2;
--inputTextColor: #737373; --inputTextColor: #737373;
--errorColor: #d70022; --errorColor: #d70022;
--linkColor: #0094fb; --linkColor: #0094fb;
--textColor: #0c0c0d; --textColor: #0c0c0d;
--lightBorderColor: rgba(12, 12, 12, 0.2);
--lightTextColor: #737373; --lightTextColor: #737373;
--successControlBGColor: #05a700; --successControlBGColor: #12bc00;
--successControlFGColor: #fff; --successControlFGColor: #fff;
} }
@ -41,17 +43,21 @@ a {
.main { .main {
display: flex; display: flex;
flex-direction: row;
flex: auto; flex: auto;
padding: 0 20px; padding: 0 25px;
box-sizing: border-box; box-sizing: border-box;
min-height: 500px;
max-height: 630px;
height: 100px;
} }
.stripedBox { .stripedBox {
width: 480px; flex: none;
position: relative;
width: 400px;
background-color: white; background-color: white;
border-radius: 6px; border-radius: 6px;
box-shadow: 0 0 0 3px rgba(155, 155, 155, 0.4); box-shadow: 0 0 0 3px rgba(12, 12, 13, 0.2);
background-image: repeating-linear-gradient( background-image: repeating-linear-gradient(
45deg, 45deg,
white, white,
@ -68,8 +74,11 @@ a {
.mainContent { .mainContent {
height: 100%; height: 100%;
background-color: white; background-color: white;
box-sizing: border-box;
margin: 0 10px; margin: 0 10px;
padding: 1px 10px 0 10px; /* top wtf? */ padding: 10px 10px 28px;
display: flex;
flex-direction: column;
} }
.spacer { .spacer {
@ -77,7 +86,8 @@ a {
} }
.uploads { .uploads {
flex: auto; flex: 0 0 262px;
position: relative;
} }
.noscript { .noscript {
@ -87,13 +97,19 @@ a {
} }
.btn { .btn {
font-size: 15px; display: block;
width: 100%;
height: 70px;
line-height: 70px;
font-size: 21px;
font-weight: 500; font-weight: 500;
color: var(--primaryControlFGColor); text-transform: uppercase;
cursor: pointer;
text-align: center; text-align: center;
letter-spacing: 0.56px;
color: var(--primaryControlFGColor);
background: var(--primaryControlBGColor); background: var(--primaryControlBGColor);
border: 1px solid var(--primaryControlBGColor); cursor: pointer;
border: 0;
border-radius: 5px; border-radius: 5px;
} }
@ -101,75 +117,52 @@ a {
background-color: var(--primaryControlHoverColor); background-color: var(--primaryControlHoverColor);
} }
.btn--cancel { .btn--stripes {
color: var(--errorColor); background: repeating-linear-gradient(
background: var(--pageBGColor); -65deg,
font-size: 15px; #7c7c7c 0,
border: 0; #7c7c7c 17px,
cursor: pointer; #737373 17px,
text-decoration: underline; #737373 30px
);
background-size: 300% 300%;
animation: barberpole 12s linear infinite;
} }
.btn--cancel:disabled { @keyframes barberpole {
text-decoration: none; 0% {
cursor: auto; background-position: 100% 0%;
} }
.btn--cancel:hover { 100% {
background-color: var(--pageBGColor); background-position: 0% 0%;
}
} }
.input { .input {
flex: 2 0 auto; border: 1px solid var(--lightBorderColor);
border: 1px solid var(--primaryControlBGColor);
border-radius: 6px 0 0 6px;
font-size: 20px; font-size: 20px;
color: var(--inputTextColor); color: var(--inputTextColor);
font-family: 'SF Pro Text', sans-serif; font-family: 'SF Pro Text', sans-serif;
letter-spacing: 0;
line-height: 23px;
font-weight: 300; font-weight: 300;
height: 46px;
padding-left: 10px; padding-left: 10px;
padding-right: 10px; padding-right: 10px;
} }
.input--error {
border-color: var(--errorColor);
}
.input--noBtn { .input--noBtn {
border-radius: 6px; border-radius: 6px;
} }
.inputBtn { .input--error {
flex: auto; border-color: var(--errorColor);
background: var(--primaryControlBGColor);
border-radius: 0 6px 6px 0;
border: 1px solid var(--primaryControlBGColor);
color: var(--primaryControlFGColor);
cursor: pointer;
/* Force flat button look */
/* stylelint-disable-next-line plugin/no-unsupported-browser-features */
appearance: none;
font-size: 15px;
padding-bottom: 3px;
padding-left: 10px;
padding-right: 10px;
white-space: nowrap;
} }
.inputBtn:disabled { .inputBtn.inputError {
cursor: auto; background-color: var(--errorColor);
} }
.inputBtn:hover { .inputBtn.inputError:hover {
background-color: var(--primaryControlHoverColor); background-color: var(--errorColor);
}
.inputBtn--hidden {
display: none;
} }
.cursor--pointer { .cursor--pointer {
@ -188,15 +181,15 @@ a {
} }
.link--action { .link--action {
text-decoration: underline; font-weight: 500;
font-size: 14px;
text-align: center; text-align: center;
} }
.page { .page {
margin: 0 auto 30px; height: 100%;
margin: 0;
display: flex; display: flex;
justify-content: center;
align-items: center;
flex-direction: column; flex-direction: column;
text-align: center; text-align: center;
} }
@ -223,6 +216,13 @@ a {
animation: fadeout 200ms linear; animation: fadeout 200ms linear;
} }
.goBackButton {
position: absolute;
top: 0;
left: 0;
margin: 18px;
}
@keyframes fadeout { @keyframes fadeout {
0% { 0% {
opacity: 1; opacity: 1;
@ -250,6 +250,7 @@ a {
.error { .error {
color: var(--errorColor); color: var(--errorColor);
font-weight: 600;
} }
.title { .title {
@ -264,34 +265,50 @@ a {
} }
.description { .description {
font-size: 15px; font-size: 13px;
line-height: 23px; text-align: left;
max-width: 630px; margin: 14px auto;
text-align: center; color: var(--lightTextColor);
margin: 0 auto 60px; width: 95%;
color: var(--textColor);
width: 92%;
} }
@media (max-device-width: 768px), (max-width: 768px) { .visible {
visibility: visible !important;
}
.noDisplay {
display: none;
}
.flexible {
flex: 1;
}
@media (max-device-width: 750px), (max-width: 750px) {
.description { .description {
margin: 0 auto 25px; margin: 0 auto 25px;
} }
}
@media (max-device-width: 520px), (max-width: 520px) { .main {
.input { flex-direction: column;
font-size: 22px; min-height: 700px;
padding: 10px 10px;
border-radius: 6px 6px 0 0;
} }
.inputBtn { .spacer {
border-radius: 0 0 6px 6px; flex: none;
flex: 0 1 65px; height: 0;
} }
.input--noBtn { .stripedBox {
border-radius: 6px; max-height: 550px;
flex: 1;
}
.uploads {
flex: none;
}
.footer {
margin: 15px;
} }
} }

View File

@ -1,12 +1,12 @@
/* global MAXFILESIZE */ import { checkSize } from './utils';
import Archive from './archive';
import { bytes } from './utils';
export default function(state, emitter) { export default function(state, emitter) {
emitter.on('DOMContentLoaded', () => { emitter.on('DOMContentLoaded', () => {
document.body.addEventListener('dragover', event => { document.body.addEventListener('dragover', event => {
if (state.route === '/') { if (state.route === '/') {
event.preventDefault(); event.preventDefault();
const files = document.querySelector('.uploadedFilesWrapper');
files.classList.add('uploadArea--noEvents');
} }
}); });
document.body.addEventListener('drop', event => { document.body.addEventListener('drop', event => {
@ -15,21 +15,12 @@ export default function(state, emitter) {
document document
.querySelector('.uploadArea') .querySelector('.uploadArea')
.classList.remove('uploadArea--dragging'); .classList.remove('uploadArea--dragging');
const target = event.dataTransfer;
if (target.files.length === 0) {
return;
}
const file = new Archive(target.files);
if (file.size === 0) { const target = event.dataTransfer;
return;
} checkSize(target.files, state.files);
if (file.size > MAXFILESIZE) {
// eslint-disable-next-line no-alert emitter.emit('addFiles', { files: target.files });
alert(state.translate('fileTooBig', { size: bytes(MAXFILESIZE) }));
return;
}
emitter.emit('upload', { file, type: 'drop' });
} }
}); });
}); });

View File

@ -1,17 +1,14 @@
import FileSender from './fileSender'; import FileSender from './fileSender';
import FileReceiver from './fileReceiver'; import FileReceiver from './fileReceiver';
import { import { copyToClipboard, delay, openLinksInNewTab, percent } from './utils';
copyToClipboard,
delay,
fadeOut,
openLinksInNewTab,
percent
} from './utils';
import * as metrics from './metrics'; import * as metrics from './metrics';
import { hasPassword } from './api';
import Archive from './archive';
export default function(state, emitter) { export default function(state, emitter) {
let lastRender = 0; let lastRender = 0;
let updateTitle = false; let updateTitle = false;
state.files = [];
function render() { function render() {
emitter.emit('render'); emitter.emit('render');
@ -64,6 +61,16 @@ export default function(state, emitter) {
metrics.changedDownloadLimit(file); metrics.changedDownloadLimit(file);
}); });
emitter.on('removeUpload', async ({ file }) => {
for (let i = 0; i < state.files.length; i++) {
if (state.files[i] === file) {
state.files.splice(i, 1);
render();
return;
}
}
});
emitter.on('delete', async ({ file, location }) => { emitter.on('delete', async ({ file, location }) => {
try { try {
metrics.deletedUpload({ metrics.deletedUpload({
@ -85,11 +92,22 @@ export default function(state, emitter) {
state.transfer.cancel(); state.transfer.cancel();
}); });
emitter.on('upload', async ({ file, type }) => { emitter.on('addFiles', async ({ files }) => {
for (let i = 0; i < files.length; i++) {
state.files.push(files[i]);
}
render();
});
//TODO: hook up to multi-file upload functionality
emitter.on('upload', async ({ files, type, dlCount, password }) => {
const file = new Archive(files);
const size = file.size; const size = file.size;
const sender = new FileSender(file); const sender = new FileSender(file);
sender.on('progress', updateProgress); sender.on('progress', updateProgress);
sender.on('encrypting', render); sender.on('encrypting', render);
sender.on('complete', render);
state.transfer = sender; state.transfer = sender;
state.uploading = true; state.uploading = true;
render(); render();
@ -98,19 +116,25 @@ export default function(state, emitter) {
await delay(200); await delay(200);
try { try {
metrics.startedUpload({ size, type }); metrics.startedUpload({ size, type });
const ownedFile = await sender.upload(); const ownedFile = await sender.upload();
ownedFile.type = type; ownedFile.type = type;
state.storage.totalUploads += 1; state.storage.totalUploads += 1;
metrics.completedUpload(ownedFile); metrics.completedUpload(ownedFile);
state.storage.addFile(ownedFile); state.storage.addFile(ownedFile);
if (password) {
emitter.emit('password', { password, file: ownedFile });
}
emitter.emit('changeLimit', { file: ownedFile, value: dlCount });
const cancelBtn = document.getElementById('cancel-upload'); const cancelBtn = document.getElementById('cancel-upload');
if (cancelBtn) { if (cancelBtn) {
cancelBtn.hidden = 'hidden'; cancelBtn.hidden = 'hidden';
} }
if (document.querySelector('.page')) { if (document.querySelector('.page')) {
await delay(1000); await delay(1000);
await fadeOut('.page');
} }
emitter.emit('pushState', `/share/${ownedFile.id}`); emitter.emit('pushState', `/share/${ownedFile.id}`);
} catch (err) { } catch (err) {
@ -127,6 +151,8 @@ export default function(state, emitter) {
} }
} finally { } finally {
openLinksInNewTab(links, false); openLinksInNewTab(links, false);
state.files = [];
state.password = '';
state.uploading = false; state.uploading = false;
state.transfer = null; state.transfer = null;
} }
@ -150,6 +176,17 @@ export default function(state, emitter) {
render(); render();
}); });
emitter.on('getPasswordExist', async ({ id }) => {
try {
state.fileInfo = await hasPassword(id);
render();
} catch (e) {
if (e.message === '404') {
return emitter.emit('pushState', '/404');
}
}
});
emitter.on('getMetadata', async () => { emitter.on('getMetadata', async () => {
const file = state.fileInfo; const file = state.fileInfo;
@ -172,6 +209,7 @@ export default function(state, emitter) {
emitter.on('download', async file => { emitter.on('download', async file => {
state.transfer.on('progress', updateProgress); state.transfer.on('progress', updateProgress);
state.transfer.on('decrypting', render); state.transfer.on('decrypting', render);
state.transfer.on('complete', render);
const links = openLinksInNewTab(); const links = openLinksInNewTab();
const size = file.size; const size = file.size;
try { try {
@ -186,12 +224,11 @@ export default function(state, emitter) {
const speed = size / (time / 1000); const speed = size / (time / 1000);
if (document.querySelector('.page')) { if (document.querySelector('.page')) {
await delay(1000); await delay(1000);
await fadeOut('.page');
} }
state.storage.totalDownloads += 1; state.storage.totalDownloads += 1;
state.transfer.reset(); state.transfer.reset();
metrics.completedDownload({ size, time, speed }); metrics.completedDownload({ size, time, speed });
emitter.emit('pushState', '/completed'); //emitter.emit('pushState', '/completed');
} catch (err) { } catch (err) {
if (err.message === '0') { if (err.message === '0') {
// download cancelled // download cancelled

View File

@ -172,6 +172,7 @@ export default class FileReceiver extends Nanobus {
this.downloadRequest = null; this.downloadRequest = null;
this.msg = 'downloadFinish'; this.msg = 'downloadFinish';
this.emit('complete');
this.state = 'complete'; this.state = 'complete';
} catch (e) { } catch (e) {
this.downloadRequest = null; this.downloadRequest = null;

View File

@ -93,6 +93,7 @@ export default class FileSender extends Nanobus {
url: `${result.url}#${secretKey}`, url: `${result.url}#${secretKey}`,
name: this.file.name, name: this.file.name,
size: this.file.size, size: this.file.size,
manifest: this.file.manifest,
time: time, time: time,
speed: this.file.size / (time / 1000), speed: this.file.size / (time / 1000),
createdAt: Date.now(), createdAt: Date.now(),
@ -101,6 +102,7 @@ export default class FileSender extends Nanobus {
nonce: this.keychain.nonce, nonce: this.keychain.nonce,
ownerToken: result.ownerToken ownerToken: result.ownerToken
}); });
return ownedFile; return ownedFile;
} catch (e) { } catch (e) {
this.msg = 'errorPageHeader'; this.msg = 'errorPageHeader';

View File

@ -2,7 +2,6 @@
@import './templates/activeBackground/activeBackground.css'; @import './templates/activeBackground/activeBackground.css';
@import './templates/header/header.css'; @import './templates/header/header.css';
@import './templates/downloadButton/downloadButton.css'; @import './templates/downloadButton/downloadButton.css';
@import './templates/progress/progress.css';
@import './templates/passwordInput/passwordInput.css'; @import './templates/passwordInput/passwordInput.css';
@import './templates/downloadPassword/downloadPassword.css'; @import './templates/downloadPassword/downloadPassword.css';
@import './templates/setPasswordSection/setPasswordSection.css'; @import './templates/setPasswordSection/setPasswordSection.css';
@ -11,7 +10,14 @@
@import './templates/selectbox/selectbox.css'; @import './templates/selectbox/selectbox.css';
@import './templates/fileList/fileList.css'; @import './templates/fileList/fileList.css';
@import './templates/file/file.css'; @import './templates/file/file.css';
@import './templates/uploadedFile/uploadedFile.css';
@import './templates/uploadedFileList/uploadedFileList.css';
@import './templates/popup/popup.css'; @import './templates/popup/popup.css';
@import './templates/title/title.css';
@import './templates/fileIcon/fileIcon.css';
@import './templates/signupPromo/signupPromo.css';
@import './templates/userAccount/userAccount.css';
@import './pages/welcome/welcome.css'; @import './pages/welcome/welcome.css';
@import './pages/share/share.css'; @import './pages/share/share.css';
@import './pages/signin/signin.css';
@import './pages/unsupported/unsupported.css'; @import './pages/unsupported/unsupported.css';

View File

@ -9,6 +9,7 @@ export default class OwnedFile {
this.name = obj.name; this.name = obj.name;
this.size = obj.size; this.size = obj.size;
this.type = obj.type; this.type = obj.type;
this.manifest = obj.manifest;
this.time = obj.time; this.time = obj.time;
this.speed = obj.speed; this.speed = obj.speed;
this.createdAt = obj.createdAt; this.createdAt = obj.createdAt;
@ -70,6 +71,7 @@ export default class OwnedFile {
name: this.name, name: this.name,
size: this.size, size: this.size,
type: this.type, type: this.type,
manifest: this.manifest,
time: this.time, time: this.time,
speed: this.speed, speed: this.speed,
createdAt: this.createdAt, createdAt: this.createdAt,

View File

@ -1,26 +0,0 @@
const html = require('choo/html');
const progress = require('../../templates/progress');
const { fadeOut } = require('../../utils');
module.exports = function(state, emit) {
return html`
<div class="page effect--fadeIn">
<div class="title">
${state.translate('downloadFinish')}
</div>
<div class="description"></div>
${progress(1)}
<div class="progressSection">
<div class="progressSection__text"></div>
</div>
<a class="link link--action"
href="/"
onclick=${sendNew}>${state.translate('sendYourFilesLink')}</a>
</div>`;
async function sendNew(e) {
e.preventDefault();
await fadeOut('.page');
emit('pushState', '/');
}
};

View File

@ -1,42 +0,0 @@
const html = require('choo/html');
const progress = require('../../templates/progress');
const { bytes } = require('../../utils');
module.exports = function(state, emit) {
const transfer = state.transfer;
const cancelBtn = html`
<button
id="cancel"
class="btn btn--cancel"
title="${state.translate('deletePopupCancel')}"
onclick=${cancel}>
${state.translate('deletePopupCancel')}
</button>`;
return html`
<div class="page effect--fadeIn">
<div class="title">
${state.translate('downloadingPageProgress', {
filename: state.fileInfo.name,
size: bytes(state.fileInfo.size)
})}
</div>
<div class="description">
${state.translate('downloadingPageMessage')}
</div>
${progress(transfer.progressRatio, transfer.progressIndefinite)}
<div class="progressSection">
<div class="progressSection__text">
${state.translate(transfer.msg, transfer.sizes)}
</div>
${transfer.state === 'downloading' ? cancelBtn : null}
</div>
</div>
`;
function cancel() {
const btn = document.getElementById('cancel');
btn.remove();
emit('cancel');
}
};

View File

@ -1,10 +1,22 @@
const html = require('choo/html'); const html = require('choo/html');
const assets = require('../../../common/assets'); const assets = require('../../../common/assets');
const title = require('../../templates/title');
module.exports = function(state) { module.exports = function(state) {
return html` return html`
<div class="page"> <div class="page">
<div class="title">${state.translate('errorPageHeader')}</div>
<img src="${assets.get('illustration_error.svg')}"/> ${title(state)}
<div class="error">${state.translate('errorPageHeader')}</div>
<img class="flexible" src="${assets.get('illustration_error.svg')}"/>
<div class="description">
${state.translate('uploadPageExplainer')}
</div>
<a class="link link--action" href="/">
${state.translate('sendYourFilesLink')}
</a>
</div>`; </div>`;
}; };

View File

@ -1,11 +1,17 @@
const html = require('choo/html'); const html = require('choo/html');
const assets = require('../../../common/assets'); const assets = require('../../../common/assets');
const title = require('../../templates/title');
module.exports = function(state) { module.exports = function(state) {
return html` return html`
<div class="page"> <div class="page">
<div class="title">${state.translate('expiredPageHeader')}</div>
<img src="${assets.get('illustration_expired.svg')}" id="expired-img"> ${title(state)}
<div class="error">${state.translate('expiredPageHeader')}</div>
<img src="${assets.get(
'illustration_expired.svg'
)}" class="flexible" id="expired-img">
<div class="description"> <div class="description">
${state.translate('uploadPageExplainer')} ${state.translate('uploadPageExplainer')}
</div> </div>

View File

@ -0,0 +1,19 @@
const html = require('choo/html');
const titleSection = require('../../templates/title');
const downloadPassword = require('../../templates/downloadPassword');
module.exports = function(state, emit) {
return html`
<div class="page">
${titleSection(state)}
<div class="description">${state.translate('downloadMessage2')}</div>
${downloadPassword(state, emit)}
<a class="link link--action" href="/">
${state.translate('sendYourFilesLink')}
</a>
</div>
`;
};

View File

@ -1,40 +1,25 @@
const html = require('choo/html'); const html = require('choo/html');
const assets = require('../../../common/assets'); const titleSection = require('../../templates/title');
const { bytes } = require('../../utils'); const downloadButton = require('../../templates/downloadButton');
const downloadedFiles = require('../../templates/uploadedFileList');
module.exports = function(state, pageAction) { module.exports = function(state, emit) {
const fileInfo = state.fileInfo; const storageFile = state.storage.getFileById(state.params.id);
const size = fileInfo.size const multifiles = Array.from(storageFile.manifest.files);
? state.translate('downloadFileSize', { size: bytes(fileInfo.size) })
: '';
const title = fileInfo.name
? state.translate('downloadFileName', { filename: fileInfo.name })
: state.translate('downloadFileTitle');
const info = html`
<div id="dl-file"
data-nonce="${fileInfo.nonce}"
data-requires-password="${fileInfo.requiresPassword}"></div>`;
if (!pageAction) {
return info;
}
return html` return html`
<div class="page"> <div class="page">
<div class="title"> ${titleSection(state)}
<span>${title}</span>
<span>${' ' + size}</span> ${downloadedFiles(multifiles, state, emit)}
</div> <div class="description">${state.translate('downloadMessage2')}</div>
<div class="description">${state.translate('downloadMessage')}</div> ${downloadButton(state, emit)}
<img
src="${assets.get('illustration_download.svg')}"
title="${state.translate('downloadAltText')}"/>
${pageAction}
<a class="link link--action" href="/"> <a class="link link--action" href="/">
${state.translate('sendYourFilesLink')} ${state.translate('sendYourFilesLink')}
</a> </a>
${info}
</div> </div>
`; `;
}; };

View File

@ -3,42 +3,51 @@ const html = require('choo/html');
const raw = require('choo/html/raw'); const raw = require('choo/html/raw');
const assets = require('../../../common/assets'); const assets = require('../../../common/assets');
const notFound = require('../notFound'); const notFound = require('../notFound');
const setPasswordSection = require('../../templates/setPasswordSection');
const selectbox = require('../../templates/selectbox');
const deletePopup = require('../../templates/popup'); const deletePopup = require('../../templates/popup');
const uploadedFiles = require('../../templates/uploadedFileList');
const { allowedCopy, delay, fadeOut } = require('../../utils'); const { allowedCopy, delay, fadeOut } = require('../../utils');
module.exports = function(state, emit) { module.exports = function(state, emit) {
const file = state.storage.getFileById(state.params.id); const file = state.storage.getFileById(state.params.id);
if (!file) { if (!file) {
return notFound(state, emit); return notFound(state);
} }
const passwordReminderClass = file._hasPassword
? ''
: 'passwordReminder--hidden';
const multifiles = Array.from(file.manifest.files);
return html` return html`
<div id="shareWrapper" class="effect--fadeIn">
${expireInfo(file, state.translate, emit)} <div class="page effect--fadeIn" id="shareWrapper">
<div class="sharePage"> <a href="/" class="goBackButton">
<img src="${assets.get('back-arrow.svg')}"/>
</a>
${expireInfo(file, state.translate)}
${uploadedFiles(multifiles, state, emit)}
<div class="sharePage__copyText"> <div class="sharePage__copyText">
${state.translate('copyUrlFormLabelWithName', { filename: file.name })} ${state.translate('copyUrlFormLabelWithName', { filename: '' })}
<div class="sharePage__passwordReminder ${passwordReminderClass}">(don't forget the password too)</div>
</div> </div>
<div class="copySection">
<input <input
id="fileUrl" id="fileUrl"
class="copySection__url" class="copySection__url"
type="url" type="url"
value="${file.url}" value="${file.url}"
readonly="true"/> readonly="true"/>
<button id="copyBtn"
class="inputBtn inputBtn--copy" <button id="copyBtn"
title="${state.translate('copyUrlFormButton')}" class="btn copyBtn"
onclick=${copyLink}>${state.translate('copyUrlFormButton')}</button> title="${state.translate('copyUrlFormButton')}"
</div> onclick=${copyLink}>${state.translate('copyUrlFormButton')}
${setPasswordSection(state, emit)}
<button
class="btn btn--delete"
title="${state.translate('deleteFileButton')}"
onclick=${showPopup}>${state.translate('deleteFileButton')}
</button> </button>
<div class="sharePage__deletePopup"> <div class="sharePage__deletePopup">
${deletePopup( ${deletePopup(
state.translate('deletePopupText'), state.translate('deletePopupText'),
@ -47,11 +56,15 @@ module.exports = function(state, emit) {
deleteFile deleteFile
)} )}
</div> </div>
<a class="link link--action"
href="/" <a
onclick=${sendNew}>${state.translate('sendAnotherFileLink')}</a> class="error btn--delete"
title="${state.translate('deleteFileButton')}"
onclick=${showPopup}>${state.translate('deleteFileButton')}
</a>
</div> </div>
</div>
`; `;
function showPopup() { function showPopup() {
@ -60,30 +73,22 @@ module.exports = function(state, emit) {
popup.focus(); popup.focus();
} }
async function sendNew(e) {
e.preventDefault();
await fadeOut('#shareWrapper');
emit('pushState', '/');
}
async function copyLink() { async function copyLink() {
if (allowedCopy()) { if (allowedCopy()) {
emit('copy', { url: file.url, location: 'success-screen' }); emit('copy', { url: file.url, location: 'success-screen' });
const input = document.getElementById('fileUrl'); const input = document.getElementById('fileUrl');
input.disabled = true; input.disabled = true;
input.classList.add('input--copied');
const copyBtn = document.getElementById('copyBtn'); const copyBtn = document.getElementById('copyBtn');
copyBtn.disabled = true; copyBtn.disabled = true;
copyBtn.classList.add('inputBtn--copied'); copyBtn.classList.add('copyBtn--copied');
copyBtn.replaceChild( copyBtn.replaceChild(
html`<img src="${assets.get('check-16.svg')}" class="cursor--pointer">`, html`<label>${state.translate('copiedUrl')}</label>`,
copyBtn.firstChild copyBtn.firstChild
); );
await delay(2000); await delay(2000);
input.disabled = false; input.disabled = false;
input.classList.remove('input--copied');
copyBtn.disabled = false; copyBtn.disabled = false;
copyBtn.classList.remove('inputBtn--copied'); copyBtn.classList.remove('copyBtn--copied');
copyBtn.textContent = state.translate('copyUrlFormButton'); copyBtn.textContent = state.translate('copyUrlFormButton');
} }
} }
@ -95,18 +100,14 @@ module.exports = function(state, emit) {
} }
}; };
function expireInfo(file, translate, emit) { function expireInfo(file, translate) {
const hours = Math.floor(EXPIRE_SECONDS / 60 / 60); const hours = Math.floor(EXPIRE_SECONDS / 60 / 60);
const el = html`<div class="title">${raw( const el = html`<div class="shareTitle">${raw(
translate('expireInfo', { translate('expireInfo', {
downloadCount: '<select></select>', downloadCount: translate('downloadCount', { num: file.dlimit }),
timespan: translate('timespanHours', { num: hours }) timespan: translate('timespanHours', { num: hours })
}) })
)}</div>`; )}</div>`;
const select = el.querySelector('select');
const options = [1, 2, 3, 4, 5, 20].filter(i => i > (file.dtotal || 0));
const t = num => translate('downloadCount', { num });
const changed = value => emit('changeLimit', { file, value });
el.replaceChild(selectbox(file.dlimit || 1, options, t, changed), select);
return el; return el;
} }

View File

@ -1,112 +1,86 @@
.sharePage { .sharePage__copyText {
margin: 0 auto; margin: 8px 0 8px;
display: flex; word-wrap: break-word;
justify-content: center; font-size: 15px;
flex-direction: column; color: var(--lightTextColor);
width: 100%; text-align: center;
max-width: 640px;
} }
.sharePage__copyText { .sharePage__passwordReminder {
align-self: flex-start; font-size: 11px;
margin-top: 60px; font-style: italic;
margin-bottom: 10px; }
color: var(--textColor);
max-width: 614px; .passwordReminder--hidden {
word-wrap: break-word; display: none;
} }
.sharePage__deletePopup { .sharePage__deletePopup {
position: relative; position: relative;
align-self: center; margin-top: -70px;
bottom: 50px; pointer-events: none;
}
.shareTitle {
color: var(--textColor);
margin: 8px auto 15px;
text-align: center;
font-family: 'SF Pro Text', sans-serif;
font-size: 12px;
font-style: italic;
width: 280px;
} }
.copySection { .copySection {
display: flex;
flex-wrap: nowrap;
width: 100%; width: 100%;
} }
.copySection__url { .copySection__url {
flex: 1; box-sizing: border-box;
height: 56px; height: 30px;
border: 1px solid var(--primaryControlBGColor); border: 1px solid var(--lightBorderColor);
border-radius: 6px 0 0 6px; border-radius: 4px;
font-size: 20px; font-size: 15px;
color: var(--inputTextColor); color: var(--primaryControlBGColor);
margin: 0 0 6px;
padding: 6px;
font-family: 'SF Pro Text', sans-serif; font-family: 'SF Pro Text', sans-serif;
letter-spacing: 0; letter-spacing: 0;
line-height: 23px; line-height: 18px;
font-weight: 300; font-weight: 500;
padding-left: 10px;
} }
.copySection__url:disabled { .copySection__url:disabled {
border: 1px solid var(--successControlBGColor);
background: var(--successControlFGColor); background: var(--successControlFGColor);
} }
.inputBtn--copy {
flex: 0 1 165px;
padding-bottom: 4px;
}
.input--copied { .input--copied {
border-color: var(--successControlBGColor); border-color: var(--successControlBGColor);
} }
.inputBtn--copied, .copyBtn {
.inputBtn--copied:hover { transition: background 0.5s;
}
.copyBtn--copied,
.copyBtn--copied:hover {
background: var(--successControlBGColor); background: var(--successControlBGColor);
border: 1px solid var(--successControlBGColor);
color: var(--successControlFGColor); color: var(--successControlFGColor);
transition: background 0s;
} }
.btn--delete { .btn--delete {
border: none;
align-self: center; align-self: center;
width: 176px; width: 176px;
height: 44px;
background: #fff; background: #fff;
border-color: rgba(12, 12, 13, 0.3); margin: 10px 0 0;
margin-top: 50px; font-size: 14px;
margin-bottom: 12px; line-height: 16px;
color: #313131; color: var(--errorColor);
cursor: pointer;
} }
.btn--delete:hover { .btn--delete:hover {
background: #efeff1; text-decoration: underline;
}
@media (max-device-width: 768px), (max-width: 768px) {
.copySection {
width: 100%;
}
.copySection__url {
font-size: 18px;
}
}
@media (max-device-width: 520px), (max-width: 520px) {
.copySection {
width: 100%;
flex-direction: column;
padding-left: 0;
}
.copySection__url {
font-size: 22px;
padding: 15px 10px;
border-radius: 6px 6px 0 0;
}
.sharePage__copyText {
text-align: center;
}
.inputBtn--copy {
border-radius: 0 0 6px 6px;
flex: 0 1 65px;
}
} }

60
app/pages/signin/index.js Normal file
View File

@ -0,0 +1,60 @@
const html = require('choo/html');
const assets = require('../../../common/assets');
const title = require('../../templates/title');
// eslint-disable-next-line no-unused-vars
module.exports = function(state, emit) {
return html`
<div class="page signInPage">
<a href="/" class="goBackButton">
<img src="${assets.get('back-arrow.svg')}"/>
</a>
${title(state)}
<div class="signIn__info flexible">
${state.translate('accountBenefitTitle')}
<ul>
<li>${state.translate('accountBenefitMultiFile')}</li>
<li>${state.translate('accountBenefitLargeFiles')}</li>
<li>${state.translate('accountBenefitExpiry')}</li>
<li>${state.translate('accountBenefitSync')}</li>
<li>${state.translate('accountBenefitNotify')}</li>
<li>${state.translate('accountBenefitMore')}</li>
</ul>
</div>
<div class="signIn__form flexible">
<img class="signIn__firefoxLogo"
src="${assets.get('firefox_logo-only.svg')}"
width=56 height=56
alt="Firefox logo"/>
<div class="signIn__emailLabel">
${state.translate('signInEmailEnter')}
</div>
${state.translate('signInContinueMessage')}
<form
data-no-csrf>
<input
type="text"
class="signIn__emailInput"
placeholder=${state.translate('emailEntryPlaceholder')}/>
<input
class='noDisplay'
id="emailSubmit"
type="submit"/>
</form>
</div>
<label class="btn" for="emailSubmit">
${state.translate('signInContinueButton')}
</label>
</div>
`;
};

View File

@ -0,0 +1,33 @@
.signInPage {
font-size: 13px;
line-height: 18px;
color: var(--lightTextColor);
}
.signIn__info {
width: 308px;
margin: 16px auto 0;
text-align: left;
}
.signIn__firefoxLogo {
display: block;
margin: 0 auto;
}
.signIn__emailLabel {
font-size: 22px;
margin: 8px 0;
}
.signIn__emailInput {
box-sizing: border-box;
width: 310px;
height: 40px;
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 4px;
margin: 0;
padding: 0 8px;
font-size: 18px;
color: var(--lightTextColor);
}

View File

@ -1,11 +1,13 @@
const html = require('choo/html'); const html = require('choo/html');
const assets = require('../../../common/assets'); const assets = require('../../../common/assets');
const title = require('../../templates/title');
module.exports = function(state) { module.exports = function(state) {
let strings = {}; let strings = {};
let why = ''; let why = '';
let url = ''; let url = '';
let buttonAction = ''; let buttonAction = '';
if (state.params.reason !== 'outdated') { if (state.params.reason !== 'outdated') {
strings = unsupportedStrings(state); strings = unsupportedStrings(state);
why = html` why = html`
@ -28,20 +30,26 @@ module.exports = function(state) {
${strings.button} ${strings.button}
</div>`; </div>`;
} }
return html` return html`
<div class="unsupportedPage"> <div class="page unsupportedPage">
<div class="title">${strings.title}</div> ${title(state)}
<div class="description"> <div class="error unsupportedPage__error">${strings.header}</div>
<div class="description flexible">
${strings.description} ${strings.description}
${why}
</div> </div>
${why}
<a href="${url}" class="firefoxDownload"> <div class="flexible firefoxDownload">
<img <a href="${url}" class="firefoxDownload__button">
src="${assets.get('firefox_logo-only.svg')}" <img
class="firefoxDownload__logo" src="${assets.get('firefox_logo-only.svg')}"
alt="Firefox"/> class="firefoxDownload__logo"
${buttonAction} alt="Firefox"/>
</a> ${buttonAction}
</a>
</div>
<div class="unsupportedPage__info"> <div class="unsupportedPage__info">
${strings.explainer} ${strings.explainer}
</div> </div>
@ -50,7 +58,7 @@ module.exports = function(state) {
function outdatedStrings(state) { function outdatedStrings(state) {
return { return {
title: state.translate('notSupportedHeader'), header: state.translate('notSupportedHeader'),
description: state.translate('notSupportedOutdatedDetail'), description: state.translate('notSupportedOutdatedDetail'),
button: state.translate('updateFirefox'), button: state.translate('updateFirefox'),
explainer: state.translate('uploadPageExplainer') explainer: state.translate('uploadPageExplainer')
@ -59,7 +67,7 @@ function outdatedStrings(state) {
function unsupportedStrings(state) { function unsupportedStrings(state) {
return { return {
title: state.translate('notSupportedHeader'), header: state.translate('notSupportedHeader'),
description: state.translate('notSupportedDetail'), description: state.translate('notSupportedDetail'),
button: state.translate('downloadFirefoxButtonSub'), button: state.translate('downloadFirefoxButtonSub'),
explainer: state.translate('uploadPageExplainer') explainer: state.translate('uploadPageExplainer')

View File

@ -1,26 +1,30 @@
.unsupportedPage { .unsupportedPage {
display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
flex-direction: column; }
.unsupportedPage__error {
margin: 10px 0 20px;
} }
.unsupportedPage__info { .unsupportedPage__info {
font-size: 13px; font-size: 13px;
line-height: 23px;
text-align: center;
color: var(--lightTextColor); color: var(--lightTextColor);
margin: 0 auto 23px; margin: 0 auto 10px;
} }
.firefoxDownload { .firefoxDownload {
margin-bottom: 181px; flex: 2;
height: 80px; }
background: #98e02b;
.firefoxDownload__button {
margin: 0 auto 20px;
height: 70px;
width: 250px;
background: #12bc00;
border-radius: 3px; border-radius: 3px;
cursor: pointer; cursor: pointer;
border: 0; border: 0;
box-shadow: 0 5px 3px rgb(234, 234, 234);
font-family: 'Fira Sans', 'segoe ui', sans-serif; font-family: 'Fira Sans', 'segoe ui', sans-serif;
font-weight: 500; font-weight: 500;
color: var(--primaryControlFGColor); color: var(--primaryControlFGColor);
@ -29,21 +33,22 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
line-height: 1; line-height: 1;
padding: 0 25px; padding: 8px;
} }
.firefoxDownload__logo { .firefoxDownload__logo {
width: 70px; width: 55px;
} }
.firefoxDownload__action { .firefoxDownload__action {
text-align: left; text-align: left;
margin-left: 20.4px; text-transform: uppercase;
margin-left: 20px;
} }
.firefoxDownload__text { .firefoxDownload__text {
text-transform: none;
font-family: 'Fira Sans', 'segoe ui', sans-serif; font-family: 'Fira Sans', 'segoe ui', sans-serif;
font-weight: 300; font-weight: 300;
font-size: 18px; font-size: 17px;
letter-spacing: -0.69px;
} }

View File

@ -1,39 +0,0 @@
const html = require('choo/html');
const progress = require('../../templates/progress');
const { bytes } = require('../../utils');
module.exports = function(state, emit) {
const transfer = state.transfer;
return html`
<div class="page effect--fadeIn">
<div class="title">
${state.translate('uploadingPageProgress', {
filename: transfer.file.name,
size: bytes(transfer.file.size)
})}
</div>
<div class="description"></div>
${progress(transfer.progressRatio, transfer.progressIndefinite)}
<div class="progressSection">
<div class="progressSection__text">
${state.translate(transfer.msg, transfer.sizes)}
</div>
<button
id="cancel-upload"
class="btn btn--cancel"
title="${state.translate('uploadingPageCancel')}"
onclick=${cancel}>
${state.translate('uploadingPageCancel')}
</button>
</div>
</div>
`;
function cancel() {
const btn = document.getElementById('cancel-upload');
btn.disabled = true;
btn.textContent = state.translate('uploadCancelNotification');
emit('cancel');
}
};

View File

@ -1,50 +1,86 @@
/* global MAXFILESIZE */
const html = require('choo/html'); const html = require('choo/html');
const assets = require('../../../common/assets'); const assets = require('../../../common/assets');
const fileList = require('../../templates/fileList'); const { checkSize } = require('../../utils');
const { bytes, fadeOut } = require('../../utils'); const title = require('../../templates/title');
const setPasswordSection = require('../../templates/setPasswordSection');
const uploadBox = require('../../templates/uploadedFileList');
const expireInfo = require('../../templates/expireInfo');
module.exports = function(state, emit) { module.exports = function(state, emit) {
// the page flickers if both the server and browser set 'effect--fadeIn' // the page flickers if both the server and browser set 'effect--fadeIn'
const fade = state.layout ? '' : 'effect--fadeIn'; const fade = state.layout ? '' : 'effect--fadeIn';
const files = state.files ? state.files : [];
const optionClass = state.uploading ? 'uploadOptions--faded' : '';
const btnUploading = state.uploading ? 'btn--stripes' : '';
const faded = files.length > 0 ? 'uploadArea--faded' : '';
const selectFileClass = files.length > 0 ? 'btn--hidden' : '';
const sendFileClass = files.length > 0 ? '' : 'btn--hidden';
let btnText = '';
if (state.encrypting) {
btnText = state.translate('encryptingFile');
} else if (state.uploading) {
btnText = `sending... ${Math.floor(state.transfer.progressRatio * 100)}%`;
} else {
//default pre-upload text
btnText = state.translate('uploadSuccessConfirmHeader');
}
return html` return html`
<div id="page-one" class="${fade}"> <div id="page-one" class="page ${fade}">
<div class="title">${state.translate('uploadPageHeader')}</div> ${title(state)}
<div class="description">
<div>${state.translate('uploadPageExplainer')}</div> <label class="uploadArea"
<a
href="https://testpilot.firefox.com/experiments/send"
class="link">
${state.translate('uploadPageLearnMore')}
</a>
</div>
<div class="uploadArea"
ondragover=${dragover} ondragover=${dragover}
ondragleave=${dragleave}> ondragleave=${dragleave}>
<img
src="${assets.get('upload.svg')}" ${uploadBox(files, state, emit)}
title="${state.translate('uploadSvgAlt')}"/>
<div class="uploadArea__msg"> <div class="uploadedFilesWrapper ${faded}">
${state.translate('uploadPageDropMessage')} <img
class="uploadArea__icon"
src="${assets.get('addfile.svg')}"
title="${state.translate('uploadSvgAlt')}"/>
<div class="uploadArea__msg">
${state.translate('uploadDropDragMessage')}
</div>
<span class="uploadArea__clickMsg">
${state.translate('uploadDropClickMessage')}
</span>
</div> </div>
<span class="uploadArea__sizeMsg">
${state.translate('uploadPageSizeMessage')}
</span>
<input id="file-upload" <input id="file-upload"
class="inputFile" class="inputFile fileBox"
type="file" type="file"
multiple multiple
name="fileUploaded" name="fileUploaded"
onfocus=${onfocus} onfocus=${onfocus}
onblur=${onblur} onblur=${onblur}
onchange=${upload} /> onchange=${addFiles} />
<label for="file-upload"
class="btn btn--file" </label>
title="${state.translate('uploadPageBrowseButton1')}">
${state.translate('uploadPageBrowseButton1')} <div class="uploadOptions ${optionClass}">
</label> ${expireInfo(state)}
${setPasswordSection(state)}
</div> </div>
${fileList(state, emit)}
<label for="file-upload"
class="btn btn--file ${selectFileClass}"
title="${state.translate('uploadPageBrowseButton1')}">
${state.translate('uploadPageBrowseButton1')}
</label>
<button
class="btn ${btnUploading} ${sendFileClass}"
onclick=${upload}
title="${btnText}">
${btnText}
</button>
</div> </div>
`; `;
@ -66,22 +102,23 @@ module.exports = function(state, emit) {
event.target.classList.remove('inputFile--focused'); event.target.classList.remove('inputFile--focused');
} }
async function addFiles(event) {
event.preventDefault();
const target = event.target;
checkSize(target.files, state.files);
emit('addFiles', { files: target.files });
}
async function upload(event) { async function upload(event) {
event.preventDefault(); event.preventDefault();
const Archive = require('../../archive').default;
const target = event.target;
const file = new Archive(target.files);
if (file.size === 0) { if (files.length > 0) {
return; emit('upload', {
files,
type: 'click',
dlCount: state.downloadCount,
password: state.password
});
} }
if (file.size > MAXFILESIZE) {
// eslint-disable-next-line no-alert
alert(state.translate('fileTooBig', { size: bytes(MAXFILESIZE) }));
return;
}
await fadeOut('#page-one');
emit('upload', { file, type: 'click' });
} }
}; };

View File

@ -1,61 +1,64 @@
.uploadArea { .uploadArea {
border: 3px dashed rgba(0, 148, 251, 0.5); position: relative;
margin: 0 auto 10px;
height: 255px;
border-radius: 4px;
display: flex; display: flex;
justify-content: center;
align-items: center;
flex-direction: column; flex-direction: column;
justify-content: center;
text-align: center; text-align: center;
border: 1px dashed rgba(12, 12, 13, 0.2);
margin: 0 0 10px;
height: 400px;
border-radius: 4px;
overflow: scroll;
transition: transform 150ms; transition: transform 150ms;
padding: 15px; flex: 1;
} }
.uploadArea__msg { .uploadArea__msg {
font-size: 22px; font-size: 15px;
color: var(--lightTextColor); color: var(--lightTextColor);
margin: 20px 0 10px; margin: 12px 0 0;
font-family: 'SF Pro Text', sans-serif; font-family: 'SF Pro Text', sans-serif;
text-transform: uppercase;
font-weight: bold;
} }
.uploadArea__sizeMsg { .uploadArea__clickMsg {
font-style: italic; font-style: italic;
font-size: 12px; font-size: 12px;
line-height: 16px; line-height: 12px;
color: var(--lightTextColor); color: var(--lightTextColor);
margin-bottom: 22px; margin: 5px;
} }
.uploadArea--dragging { .uploadArea--dragging {
border: 5px dashed rgba(0, 148, 251, 0.5); border: 1px dashed rgba(12, 12, 13, 0.4);
height: 251px;
transform: scale(1.04); transform: scale(1.04);
border-radius: 4.2px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
text-align: center;
} }
.uploadArea--dragging * { .uploadArea--faded * {
opacity: 0.5;
}
.uploadArea--noEvents,
.uploadArea--noEvents * {
pointer-events: none; pointer-events: none;
} }
.btn--file { .btn--file {
font-size: 20px; display: inline-block;
min-width: 240px; background-color: #737373;
height: 60px; }
display: flex;
justify-content: center; .btn--file:hover {
align-items: center; background-color: #636363;
padding: 0 10px; }
.btn--hidden {
display: none;
} }
.inputFile { .inputFile {
opacity: 0; display: none;
position: absolute;
} }
.inputFile--focused + .btn--file { .inputFile--focused + .btn--file {
@ -63,3 +66,24 @@
outline: 1px dotted #000; outline: 1px dotted #000;
outline: -webkit-focus-ring-color auto 5px; outline: -webkit-focus-ring-color auto 5px;
} }
.uploadArea > .uploadedFiles {
position: absolute;
top: 0;
left: 0;
flex: none;
width: 100%;
border: none;
z-index: 1;
}
.uploadOptions {
text-align: left;
font-size: 13px;
color: var(--lightTextColor);
}
.uploadOptions--faded {
opacity: 0.5;
pointer-events: none;
}

View File

@ -1,60 +1,24 @@
const preview = require('../pages/preview'); const preview = require('../pages/preview');
const download = require('../pages/download'); const password = require('../pages/password');
const notFound = require('../pages/notFound');
const downloadPassword = require('../templates/downloadPassword');
const downloadButton = require('../templates/downloadButton');
function hasFileInfo() {
return !!document.getElementById('dl-file');
}
function getFileInfoFromDOM() {
const el = document.getElementById('dl-file');
if (!el) {
return null;
}
return {
nonce: el.getAttribute('data-nonce'),
requiresPassword: !!+el.getAttribute('data-requires-password')
};
}
function createFileInfo(state) {
const metadata = getFileInfoFromDOM();
return {
id: state.params.id,
secretKey: state.params.key,
nonce: metadata.nonce,
requiresPassword: metadata.requiresPassword
};
}
module.exports = function(state, emit) { module.exports = function(state, emit) {
if (!state.fileInfo) { if (!state.fileInfo) {
// This is a fresh page load emit('getPasswordExist', { id: state.params.id });
// We need to parse the file info from the server's html return;
if (!hasFileInfo()) { }
return notFound(state, emit);
} state.fileInfo.id = state.params.id;
state.fileInfo = createFileInfo(state); state.fileInfo.secretKey = state.params.key;
if (!state.fileInfo.requiresPassword) { if (!state.transfer && !state.fileInfo.requiresPassword) {
emit('getMetadata'); emit('getMetadata');
}
} }
let pageAction = null; //default state: we don't have file metadata
if (state.transfer) { if (state.transfer) {
const s = state.transfer.state; return preview(state, emit);
if (['downloading', 'decrypting', 'complete'].indexOf(s) > -1) { }
// Downloading is in progress
return download(state, emit); if (state.fileInfo.requiresPassword && !state.fileInfo.password) {
} return password(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);
}; };

View File

@ -1,9 +1,5 @@
const welcome = require('../pages/welcome'); const welcome = require('../pages/welcome');
const upload = require('../pages/upload');
module.exports = function(state, emit) { module.exports = function(state, emit) {
if (state.uploading) {
return upload(state, emit);
}
return welcome(state, emit); return welcome(state, emit);
}; };

View File

@ -5,7 +5,10 @@ const download = require('./download');
const header = require('../templates/header'); const header = require('../templates/header');
const footer = require('../templates/footer'); const footer = require('../templates/footer');
const fxPromo = require('../templates/fxPromo'); const fxPromo = require('../templates/fxPromo');
const signupPromo = require('../templates/signupPromo');
const activeBackground = require('../templates/activeBackground'); const activeBackground = require('../templates/activeBackground');
const fileList = require('../templates/fileList');
const profile = require('../templates/userAccount');
nanotiming.disabled = true; nanotiming.disabled = true;
const app = choo(); const app = choo();
@ -20,6 +23,7 @@ function body(template) {
return function(state, emit) { return function(state, emit) {
const b = html`<body class="background ${activeBackground(state)}"> const b = html`<body class="background ${activeBackground(state)}">
${banner(state, emit)} ${banner(state, emit)}
${signupPromo(state)}
${header(state)} ${header(state)}
<main class="main"> <main class="main">
<noscript> <noscript>
@ -35,11 +39,17 @@ function body(template) {
</noscript> </noscript>
<div class="stripedBox"> <div class="stripedBox">
<div class="mainContent"> <div class="mainContent">
${profile(state)}
${template(state, emit)} ${template(state, emit)}
</div> </div>
</div> </div>
<div class="spacer"></div> <div class="spacer"></div>
<div class="uploads"></div> <div class="uploads">
${fileList(state)}
</div>
</main> </main>
${footer(state)} ${footer(state)}
</body>`; </body>`;
@ -55,11 +65,11 @@ app.route('/', body(require('./home')));
app.route('/share/:id', body(require('../pages/share'))); app.route('/share/:id', body(require('../pages/share')));
app.route('/download/:id', body(download)); app.route('/download/:id', body(download));
app.route('/download/:id/:key', body(download)); app.route('/download/:id/:key', body(download));
app.route('/completed', body(require('../pages/completed')));
app.route('/unsupported/:reason', body(require('../pages/unsupported'))); app.route('/unsupported/:reason', body(require('../pages/unsupported')));
app.route('/legal', body(require('../pages/legal'))); app.route('/legal', body(require('../pages/legal')));
app.route('/error', body(require('../pages/error'))); app.route('/error', body(require('../pages/error')));
app.route('/blank', body(require('../pages/blank'))); app.route('/blank', body(require('../pages/blank')));
app.route('*', body(require('../pages/notFound'))); app.route('*', body(require('../pages/notFound')));
app.route('/signin', body(require('../pages/signin')));
module.exports = app; module.exports = app;

View File

@ -47,6 +47,7 @@ class Storage {
if (!f.id) { if (!f.id) {
f.id = f.fileId; f.id = f.fileId;
} }
fs.push(f); fs.push(f);
} catch (err) { } catch (err) {
// obviously you're not a golfer // obviously you're not a golfer

View File

@ -1,6 +1,20 @@
.btn--download { .btn--download {
width: 180px; margin: 0 0 13px;
height: 44px; }
margin-top: 20px;
margin-bottom: 30px; .btn--complete,
.btn--complete:hover {
background-color: var(--successControlBGColor);
}
.btn--blueStripes {
background: repeating-linear-gradient(
-65deg,
#3282f2 0,
#3282f2 17px,
#3c87eb 17px,
#3c87eb 30px
);
background-size: 300% 300%;
animation: barberpole 12s linear infinite;
} }

View File

@ -1,13 +1,36 @@
const html = require('choo/html'); const html = require('choo/html');
const percent = require('../../utils').percent;
module.exports = function(state, emit) { module.exports = function(state, emit) {
const downloadState = state.transfer.state;
const progress = percent(state.transfer.progressRatio);
let btnText = '';
let btnClass = '';
if (downloadState === 'complete') {
btnText = state.translate('downloadFinish');
btnClass = 'btn--complete';
} else if (downloadState === 'decrypting') {
btnText = state.translate('decryptingFile');
btnClass = 'btn--blueStripes';
} else if (downloadState === 'downloading') {
btnText = state.translate('downloadProgressButton', { progress });
btnClass = 'btn--blueStripes';
} else {
btnText = state.translate('downloadButtonLabel');
}
return html` return html`
<button class="btn btn--download" <button class="btn btn--download ${btnClass}"
onclick=${download}>${state.translate('downloadButtonLabel')} onclick=${download}>
${btnText}
</button>`; </button>`;
function download(event) { function download(event) {
event.preventDefault(); event.preventDefault();
emit('download', state.fileInfo); if (downloadState !== 'complete') {
emit('download', state.fileInfo);
}
} }
}; };

View File

@ -1,22 +1,31 @@
.passwordSection { .passwordSection {
text-align: left; margin: auto;
text-align: center;
padding: 40px 0; padding: 40px 0;
width: 80%; width: 80%;
} }
.passwordForm { .passwordForm {
display: flex; margin: 13px;
flex-wrap: nowrap; }
.passwordForm__input {
width: 100%; width: 100%;
padding: 10px 0; height: 40px;
box-sizing: border-box;
} }
@media (max-device-width: 520px), (max-width: 520px) { .unlockBtn {
.passwordSection { margin-top: 48px;
width: 100%; }
}
.unlockBtn--error,
.passwordForm { .unlockBtn--error:hover {
flex-direction: column; background-color: var(--errorColor);
} }
.passwordForm__error {
font-size: 13px;
font-weight: 600;
visibility: hidden;
} }

View File

@ -3,33 +3,33 @@ const html = require('choo/html');
module.exports = function(state, emit) { module.exports = function(state, emit) {
const fileInfo = state.fileInfo; const fileInfo = state.fileInfo;
const invalid = fileInfo.password === null; const invalid = fileInfo.password === null;
const label = invalid
? html` const visible = invalid ? 'visible' : '';
<label class="error" for="password-input"> const invalidBtn = invalid ? 'unlockBtn--error' : '';
${state.translate('passwordTryAgain')}
</label>`
: html`
<label for="password-input">
${state.translate('unlockInputLabel')}
</label>`;
const inputClass = invalid
? 'input input--noBtn input--error'
: 'input input--noBtn';
const div = html` const div = html`
<div class="passwordSection"> <div class="passwordSection">
${label}
<label
class="error passwordForm__error ${visible}"
for="password-input">
${state.translate('passwordTryAgain')}
</label>
<form class="passwordForm" onsubmit=${checkPassword} data-no-csrf> <form class="passwordForm" onsubmit=${checkPassword} data-no-csrf>
<input id="password-input" <input id="password-input"
class="${inputClass}" class="input passwordForm__input"
maxlength="64" maxlength="64"
autocomplete="off" autocomplete="off"
placeholder="${state.translate('unlockInputPlaceholder')}" placeholder="${state.translate('unlockInputPlaceholder')}"
oninput=${inputChanged} oninput=${inputChanged}
type="password" /> type="password" />
<input type="submit" <input type="submit"
id="password-btn" id="password-btn"
class="inputBtn inputBtn--hidden" class="btn unlockBtn ${invalidBtn}"
value="${state.translate('unlockButtonLabel')}"/> value="${state.translate('unlockInputLabel')}"/>
</form> </form>
</div>`; </div>`;
@ -38,16 +38,10 @@ module.exports = function(state, emit) {
} }
function inputChanged() { function inputChanged() {
const input = document.getElementById('password-input'); const input = document.querySelector('.passwordForm__error');
input.classList.remove('visible');
const btn = document.getElementById('password-btn'); const btn = document.getElementById('password-btn');
input.classList.remove('input--error'); btn.classList.remove('unlockBtn--error');
if (input.value.length > 0) {
btn.classList.remove('inputBtn--hidden');
input.classList.remove('input--noBtn');
} else {
btn.classList.add('inputBtn--hidden');
input.classList.add('input--noBtn');
}
} }
function checkPassword(event) { function checkPassword(event) {

View File

@ -0,0 +1,36 @@
const html = require('choo/html');
const raw = require('choo/html/raw');
const selectbox = require('../selectbox');
module.exports = function(state) {
const el = html`<div> ${raw(
state.translate('frontPageExpireInfo', {
downloadCount: '<select id=dlCount></select>',
timespan: state.translate('timespanHours', { num: 24 }) //'<select id=timespan></select>'
})
)}
</div>`;
const dlCountSelect = el.querySelector('#dlCount');
el.replaceChild(
selectbox(
state.downloadCount || 1,
[1, 2, 3, 4, 5, 20],
num => state.translate('downloadCount', { num }),
value => {
state.downloadCount = value;
}
),
dlCountSelect
);
/*
const timeSelect = el.querySelector('#timespan');
el.replaceChild(
selectbox(1, [1, 2, 3, 4, 5], num => num, () => {}),
timeSelect
);
*/
return el;
};

View File

@ -1,26 +1,94 @@
.fileData { .fileToast {
font-size: 15px; margin: 13px 0 0;
vertical-align: top;
color: var(--lightTextColor);
padding: 17px 19px 0;
line-height: 23px;
position: relative;
}
.fileData--overflow {
text-overflow: ellipsis;
max-width: 0;
overflow: hidden; overflow: hidden;
font-size: 11px;
line-height: 18px;
color: var(--lightTextColor);
background-color: var(--pageBGColor);
position: relative;
box-shadow: 0 0 0 3px rgba(12, 12, 12, 0.2);
box-sizing: border-box;
height: 53px;
border-radius: 4px;
}
.fileToast__content {
position: relative;
z-index: 2;
}
.fileToast::after {
position: absolute;
z-index: 1;
content: '';
transition: all 0.25s;
top: 0;
left: 50%;
width: 0;
height: 100%;
background-color: var(--primaryControlBGColor);
}
.fileToast:hover {
background-color: #eee;
}
.fileToast--active {
color: var(--primaryControlFGColor);
}
.fileToast--active::after {
left: 0%;
width: 100%;
}
.fileData {
margin: 8px 16px 8px 44px;
overflow: hidden;
}
.fileName {
margin: 0;
font-size: 13px;
font-weight: 500;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
} }
.fileData--center { .fileInfo {
text-align: center; margin: 0;
} }
@media (max-device-width: 520px), (max-width: 520px) { .fileToast .fileIcon {
margin: 2px 8px;
}
@media (max-device-width: 750px), (max-width: 750px) {
.fileToast {
height: 32px;
width: 400px;
}
.fileToast__content {
display: flex;
}
.fileData { .fileData {
font-size: 13px; flex: auto;
padding: 17px 5px 0; display: flex;
flex-wrap: nowrap;
margin-left: 8px;
}
.fileInfo {
flex-shrink: 0;
margin-left: auto;
}
.fileToast .fileIcon {
margin: 0;
transform: scale(0.5);
color: transparent;
} }
} }

View File

@ -1,73 +1,50 @@
const html = require('choo/html'); const html = require('choo/html');
const assets = require('../../../common/assets');
const number = require('../../utils').number; const number = require('../../utils').number;
const deletePopup = require('../popup'); const bytes = require('../../utils').bytes;
const fileIcon = require('../fileIcon');
module.exports = function(file, state, emit) { module.exports = function(file, state) {
const ttl = file.expiresAt - Date.now(); const ttl = file.expiresAt - Date.now();
const remainingTime = const remainingTime =
timeLeft(ttl, state) || state.translate('linkExpiredAlt'); timeLeft(ttl, state) || state.translate('linkExpiredAlt');
const downloadLimit = file.dlimit || 1; const downloadLimit = file.dlimit || 1;
const totalDownloads = file.dtotal || 0; const totalDownloads = file.dtotal || 0;
const multiFiles = file.manifest.files;
const fileName =
multiFiles.length > 1
? `${multiFiles[0].name} + ${state.translate('fileCount', {
num: multiFiles.length - 1
})}`
: file.name;
const activeClass = isOnSharePage() ? 'fileToast--active' : '';
return html` return html`
<tr id="${file.id}"> <a href=${toastClick()}>
<td class="fileData fileData--overflow" title="${file.name}"> <li class="fileToast ${activeClass}" id="${file.id}">
<a class="link" href="/share/${file.id}">${file.name}</a> <div class="fileToast__content">
</td> ${fileIcon(file.name, file._hasPassword)}
<td class="fileData fileData--center"> <div class="fileData">
<img <p class="fileName">${fileName}</p>
onclick=${copyClick} <p class="fileInfo">
src="${assets.get('copy-16.svg')}" <span>${bytes(file.size)}</span> ·
class="cursor--pointer" <span>${state.translate('downloadCount', {
title="${state.translate('copyUrlHover')}" num: `${number(totalDownloads)} / ${number(downloadLimit)}`
tabindex="0"> })}</span>
<span hidden="true"> <span>${remainingTime}</span>
${state.translate('copiedUrl')} </p>
</span> </div>
</td> </div>
<td class="fileData fileData--overflow">${remainingTime}</td> </li>
<td class="fileData fileData--center">${number(totalDownloads)} / ${number( </a>
downloadLimit
)}</td>
<td class="fileData fileData--center">
<img
onclick=${showPopup}
src="${assets.get('close-16.svg')}"
class="cursor--pointer"
title="${state.translate('deleteButtonHover')}"
tabindex="0">
${deletePopup(
state.translate('deletePopupText'),
state.translate('deletePopupYes'),
state.translate('deletePopupCancel'),
deleteFile
)}
</td>
</tr>
`; `;
function copyClick(e) { function toastClick() {
emit('copy', { url: file.url, location: 'upload-list' }); return isOnSharePage() ? '/' : `/share/${file.id}`;
const icon = e.target;
const text = e.target.nextSibling;
icon.hidden = true;
text.hidden = false;
setTimeout(() => {
icon.hidden = false;
text.hidden = true;
}, 500);
} }
function showPopup() { function isOnSharePage() {
const tr = document.getElementById(file.id); return state.href.includes('/share/') && state.params.id === file.id;
const popup = tr.querySelector('.popup');
popup.classList.add('popup--show');
popup.focus();
}
function deleteFile() {
emit('delete', { file, location: 'upload-list' });
emit('render');
} }
}; };

View File

@ -0,0 +1,28 @@
.fileIcon {
position: relative;
float: left;
pointer-events: none;
margin: 8px;
color: #fff;
background-image: url('../assets/red_file.svg');
width: 22px;
height: 32px;
overflow: hidden;
}
.fileIcon__lock {
margin: 7px 0 0 5px;
visibility: hidden;
}
.fileIcon__lock--visible {
visibility: visible;
}
.fileIcon__fileType {
position: absolute;
margin: 16px 0 0 2px;
font-size: 7px;
font-weight: 600;
text-transform: uppercase;
}

View File

@ -0,0 +1,17 @@
const html = require('choo/html');
const assets = require('../../../common/assets');
module.exports = function(name, hasPassword) {
let type = '';
if (name) {
type = name.split('.').pop();
}
const lockClass = hasPassword ? 'fileIcon__lock--visible' : '';
return html`
<div class="fileIcon">
<div class="fileIcon__fileType">${type}</div>
<img class="fileIcon__lock ${lockClass}"src="${assets.get(
'lock-white.svg'
)}"/>
</div>`;
};

View File

@ -1,52 +1,21 @@
.fileList { .fileList {
margin: 45.3px auto; position: absolute;
table-layout: fixed; bottom: 0;
border-collapse: collapse; list-style-type: none;
margin: 0;
padding: 3px;
font-family: 'Segoe UI', 'SF Pro Text', sans-serif; font-family: 'Segoe UI', 'SF Pro Text', sans-serif;
width: 262px;
max-height: 80%;
overflow-y: scroll;
overflow-x: hidden;
} }
.fileList__header { @media (max-device-width: 750px), (max-width: 750px) {
font-size: 16px; .fileList {
color: var(--lightTextColor); position: static;
font-weight: lighter; width: 400px;
text-align: left; max-height: 200px;
background: rgba(0, 148, 251, 0.05); margin: 6px 0 0 -3px;
height: 40px;
border-top: 1px solid rgba(0, 148, 251, 0.1);
padding: 0 19px;
white-space: nowrap;
}
.fileList__body {
word-wrap: break-word;
word-break: break-all;
}
.fileList__nameCol {
width: 35%;
}
.fileList__copyCol {
text-align: center;
width: 25%;
}
.fileList__expireCol {
width: 25%;
}
.fileList__dlCol {
width: 8%;
}
.fileList__delCol {
text-align: center;
width: 7%;
}
@media (max-device-width: 520px), (max-width: 520px) {
.fileList__header {
font-size: 14px;
padding: 0 5px;
} }
} }

View File

@ -1,33 +1,12 @@
const html = require('choo/html'); const html = require('choo/html');
const file = require('../file'); const file = require('../file');
module.exports = function(state, emit) { module.exports = function(state) {
if (state.storage.files.length) { if (state.storage.files.length) {
return html` return html`
<table class="fileList"> <ul class="fileList">
<thead> ${state.storage.files.map(f => file(f, state))}
<tr> </ul>
<th class="fileList__header fileList__nameCol">
${state.translate('uploadedFile')}
</th>
<th class="fileList__header fileList__copyCol">
${state.translate('copyFileList')}
</th>
<th class="fileList__header fileList__expireCol" >
${state.translate('timeFileList')}
</th>
<th class="fileList__header fileList__dlCol" >
${state.translate('downloadsFileList')}
</th>
<th class="fileList__header fileList__delCol">
${state.translate('deleteFileList')}
</th>
</tr>
</thead>
<tbody class="fileList__body">
${state.storage.files.map(f => file(f, state, emit))}
</tbody>
</table>
`; `;
} }
}; };

View File

@ -3,13 +3,14 @@
bottom: 0; bottom: 0;
left: 0; left: 0;
font-size: 13px; font-size: 13px;
font-weight: 600;
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
flex-direction: row; flex-direction: row;
justify-content: space-between;
padding: 50px 31px 41px; padding: 50px 31px 41px;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
justify-content: flex-end;
} }
.legalSection { .legalSection {
@ -20,18 +21,18 @@
} }
.legalSection__link { .legalSection__link {
color: var(--lightTextColor); color: #fff;
opacity: 0.9; text-shadow: 0 0 3px #000;
white-space: nowrap; white-space: nowrap;
margin-right: 2vw; margin-right: 2vw;
} }
.legalSection__link:hover { .legalSection__link:visited {
opacity: 1; color: #ededf0;
} }
.legalSection__link:visited { .legalSection__link:hover {
color: var(--lightTextColor); color: #d7d7db;
} }
.legalSection__mozLogo { .legalSection__mozLogo {
@ -60,12 +61,12 @@
margin-bottom: -5px; margin-bottom: -5px;
} }
@media (max-device-width: 768px), (max-width: 768px) { @media (max-device-width: 750px), (max-width: 750px) {
.footer { .footer {
flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
align-items: flex-start; align-items: flex-start;
max-width: 630px; max-width: 630px;
padding: 20px 31px;
margin: auto; margin: auto;
} }

View File

@ -38,8 +38,6 @@ module.exports = function(state) {
class="legalSection__link"> class="legalSection__link">
${state.translate('reportIPInfringement')} ${state.translate('reportIPInfringement')}
</a> </a>
</div>
<div class="socialSection">
<a <a
href="https://github.com/mozilla/send" href="https://github.com/mozilla/send"
class="socialSection__link"> class="socialSection__link">
@ -52,9 +50,9 @@ module.exports = function(state) {
href="https://twitter.com/FxTestPilot" href="https://twitter.com/FxTestPilot"
class="socialSection__link"> class="socialSection__link">
<img <img
class="socialSection__icon" class="legalSection__mozLogo"
src="${assets.get('twitter-icon.svg')}" src="${assets.get('mozilla-logo.svg')}"
alt="twitter"/> alt="mozilla"/>
</a> </a>
</div> </div>
</footer>`; </footer>`;

View File

@ -3,7 +3,7 @@
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
padding: 31px; padding: 20px;
width: 100%; width: 100%;
} }
@ -62,7 +62,7 @@
color: var(--primaryControlFGColor); color: var(--primaryControlFGColor);
cursor: pointer; cursor: pointer;
display: block; display: block;
float: right; float: left;
font-size: 12px; font-size: 12px;
line-height: 12px; line-height: 12px;
opacity: 0.9; opacity: 0.9;
@ -88,17 +88,10 @@
background-color: var(--primaryControlHoverColor); background-color: var(--primaryControlHoverColor);
} }
@media (max-device-width: 520px), (max-width: 520px) { @media (max-device-width: 750px), (max-width: 750px) {
.header { .header {
padding-top: 60px;
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
} }
.feedback {
margin-top: 10px;
min-width: 30px;
max-width: 300px;
text-indent: 2px;
padding: 5px 5px 5px 20px;
}
} }

View File

@ -6,8 +6,6 @@ module.exports = function(state) {
const feedbackUrl = `https://qsurvey.mozilla.com/s3/txp-firefox-send?ver=${version}&browser=${browser}`; const feedbackUrl = `https://qsurvey.mozilla.com/s3/txp-firefox-send?ver=${version}&browser=${browser}`;
const header = html` const header = html`
<header class="header"> <header class="header">
<div class="logo">
</div>
<a href="${feedbackUrl}" <a href="${feedbackUrl}"
rel="noreferrer noopener" rel="noreferrer noopener"
class="feedback" class="feedback"

View File

@ -1,76 +1,39 @@
const html = require('choo/html'); const html = require('choo/html');
const MAX_LENGTH = 32;
module.exports = function(file, state, emit) { module.exports = function(state) {
const loading = state.settingPassword; const placeholder =
const pwd = file.hasPassword; state.route === '/' ? '' : state.translate('unlockInputPlaceholder');
const sectionClass = const hasPassword = !!state.password;
pwd || state.passwordSetError const sectionClass = hasPassword
? 'passwordInput' ? 'passwordInput'
: 'passwordInput passwordInput--hidden'; : 'passwordInput passwordInput--hidden';
const inputClass = loading || pwd ? 'input' : 'input input--noBtn';
let btnClass = 'inputBtn inputBtn--password inputBtn--hidden';
if (loading) {
btnClass = 'inputBtn inputBtn--password inputBtn--loading';
} else if (pwd) {
btnClass = 'inputBtn inputBtn--password';
}
const action = pwd
? state.translate('changePasswordButton')
: state.translate('addPasswordButton');
return html` return html`
<div class="${sectionClass}"> <div class="${sectionClass}">
<form <form
class="passwordInput__form" onsubmit=${onSubmit}
onsubmit=${setPassword}
data-no-csrf> data-no-csrf>
<input id="password-input" <input id="password-input"
${loading ? 'disabled' : ''} class="input passwordInput__fill"
class="${inputClass}"
maxlength="${MAX_LENGTH}"
autocomplete="off" autocomplete="off"
type="password" type="password"
oninput=${inputChanged} oninput=${inputChanged}
onfocus=${focused} onfocus=${focused}
placeholder="${ placeholder="${
pwd && !state.passwordSetError hasPassword ? passwordPlaceholder(state.password) : placeholder
? passwordPlaceholder(file.password) }"
: state.translate('unlockInputPlaceholder') >
}">
<input type="submit"
id="password-btn"
${loading ? 'disabled' : ''}
class="${btnClass}"
value="${loading ? '' : action}">
</form> </form>
<label
class="passwordInput__msg ${
state.passwordSetError ? 'passwordInput__msg--error' : ''
}"
for="password-input">${message(state, pwd)}</label>
</div>`; </div>`;
function inputChanged() { function onSubmit() {
state.passwordSetError = null; event.preventDefault();
const resetInput = document.getElementById('password-input'); }
const resetBtn = document.getElementById('password-btn');
const pwdmsg = document.querySelector('.passwordInput__msg');
const length = resetInput.value.length;
if (length === MAX_LENGTH) { function inputChanged() {
pwdmsg.textContent = state.translate('maxPasswordLength', { const password = document.getElementById('password-input').value;
length: MAX_LENGTH state.password = password;
});
} else {
pwdmsg.textContent = '';
}
if (length > 0) {
resetBtn.classList.remove('inputBtn--hidden');
resetInput.classList.remove('input--noBtn');
} else {
resetBtn.classList.add('inputBtn--hidden');
resetInput.classList.add('input--noBtn');
}
} }
function focused(event) { function focused(event) {
@ -80,30 +43,8 @@ module.exports = function(file, state, emit) {
el.placeholder = ''; el.placeholder = '';
} }
} }
function setPassword(event) {
event.preventDefault();
const el = document.getElementById('password-input');
const password = el.value;
if (password.length > 0) {
emit('password', { password, file });
} else {
el.focus();
}
return false;
}
}; };
function passwordPlaceholder(password) { function passwordPlaceholder(password) {
return password ? password.replace(/./g, '●') : '●●●●●●●●●●●●'; return password ? password.replace(/./g, '•') : '••••••••••••';
}
function message(state, pwd) {
if (state.passwordSetError) {
return state.translate('passwordSetError');
}
if (state.settingPassword || !pwd) {
return '';
}
return state.translate('passwordIsSet');
} }

View File

@ -1,41 +1,31 @@
.passwordInput { .passwordInput {
width: 90%; display: inline;
height: 100px;
padding: 10px 5px 5px;
} }
.passwordInput--hidden { .passwordInput--hidden {
visibility: hidden; visibility: hidden;
} }
.passwordInput__form { .passwordInput__fill {
display: flex; height: 24px;
flex-wrap: nowrap; box-sizing: border-box;
padding-bottom: 5px; padding: 4px;
font-size: 18px;
border: none;
background-color: var(--lightControlBGColor);
outline: none;
}
.passwordInput__fill:focus {
border: 1px solid rgba(12, 12, 13, 0.2);
background-color: var(--pageBGColor);
} }
.passwordInput__msg { .passwordInput__msg {
font-size: 15px; font-size: 12px;
color: var(--lightTextColor); color: var(--lightTextColor);
} }
.passwordInput__msg--error { .passwordInput__msg--error {
color: var(--errorColor); color: var(--errorColor);
} }
.inputBtn--loading {
background-image: url('../assets/spinner.svg');
background-position: center;
background-size: 30px 30px;
background-repeat: no-repeat;
}
.inputBtn--password {
flex: 0 0 200px;
}
@media (max-device-width: 520px), (max-width: 520px) {
.passwordInput__form {
flex-direction: column;
}
}

View File

@ -2,19 +2,17 @@ const html = require('choo/html');
module.exports = function(msg, confirmText, cancelText, confirmCallback) { module.exports = function(msg, confirmText, cancelText, confirmCallback) {
return html` return html`
<div class="popup__wrapper">
<div class="popup" onblur=${hide} tabindex="-1"> <div class="popup" onblur=${hide} tabindex="-1">
<div class="popup__message">${msg}</div> <div class="popup__message">${msg}</div>
<div class="popup__action"> <div class="popup__action">
<span class="popup__no" onclick=${hide}> <div>
${cancelText} <span class="popup__no" onclick=${hide}>${cancelText}</span>
</span> </div>
<span class="popup__yes" onclick=${confirmCallback}> <div>
${confirmText} <span class="popup__yes" onclick=${confirmCallback}>${confirmText}</span>
</span> </div>
</div> </div>
</div> </div>`;
</div>`;
function hide(e) { function hide(e) {
e.stopPropagation(); e.stopPropagation();

View File

@ -1,122 +1,79 @@
.popup { .popup {
visibility: hidden; display: block;
min-width: 204px; width: 100%;
min-height: 105px; height: 70px;
background-color: var(--pageBGColor); background-color: var(--errorColor);
color: var(--textColor); color: var(--textColor);
border: 1px solid #d7d7db; padding: 0;
padding: 15px 24px; box-sizing: border-box;
box-sizing: content-box;
text-align: center; text-align: center;
border-radius: 5px; border-radius: 4px;
position: absolute;
z-index: 1;
bottom: 20px;
left: -40px;
transition: opacity 0.5s; transition: opacity 0.5s;
opacity: 0;
outline: 0; outline: 0;
box-shadow: 3px 3px 7px rgba(136, 136, 136, 0.3); opacity: 0;
visibility: hidden;
} }
.popup::after { .popup::after {
content: ''; content: '';
position: absolute; position: absolute;
bottom: -11px; top: 100%;
left: 20px; left: 50%;
background-color: #fff; width: 0;
display: block; height: 0;
width: 20px; border: 8px solid;
height: 20px; border-color: var(--errorColor) transparent transparent;
transform: rotate(45deg); margin-left: -8px;
border-radius: 0 0 5px; pointer-events: none;
border-right: 1px solid #d7d7db;
border-bottom: 1px solid #d7d7db;
border-left: 1px solid #fff;
border-top: 1px solid #fff;
}
.popup__wrapper {
position: absolute;
display: inline-block;
} }
.popup__message { .popup__message {
height: 40px; height: 40px;
display: flex; padding: 10px;
justify-content: center; box-sizing: border-box;
align-items: center; text-align: center;
border-bottom: 1px #ebebeb solid; color: var(--primaryControlFGColor);
color: var(--textColor);
font-size: 15px; font-size: 15px;
font-weight: normal; font-style: italic;
padding-bottom: 15px;
white-space: nowrap; white-space: nowrap;
width: calc(100% + 48px);
margin-left: -24px;
} }
.popup__action { .popup__action {
margin-top: 15px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; text-transform: uppercase;
justify-content: space-between; }
.popup__action > div {
flex: auto;
} }
.popup__no { .popup__no {
color: #4a4a4a; color: var(--primaryControlFGColor);
background-color: #fbfbfb; padding: 5px;
border: 1px #c1c1c1 solid; font-weight: bold;
border-radius: 5px;
padding: 5px 25px;
font-weight: normal;
min-width: 94px;
box-sizing: border-box;
cursor: pointer; cursor: pointer;
white-space: nowrap; white-space: nowrap;
} }
.popup__no:hover { .popup__no:hover {
background-color: #efeff1; text-decoration: underline;
} }
.popup__yes { .popup__yes {
color: var(--primaryControlFGColor); color: var(--primaryControlFGColor);
background-color: var(--primaryControlBGColor); padding: 5px;
border-radius: 5px;
padding: 5px 25px;
font-weight: normal; font-weight: normal;
cursor: pointer; cursor: pointer;
min-width: 94px;
box-sizing: border-box;
white-space: nowrap; white-space: nowrap;
margin-left: 12px;
} }
.popup__yes:hover { .popup__yes:hover {
background-color: var(--primaryControlHoverColor); text-decoration: underline;
} }
.popup--show { .popup--show {
visibility: visible; visibility: visible;
opacity: 1; opacity: 1;
} pointer-events: auto;
@media (max-device-width: 992px), (max-width: 992px) {
.popup {
left: auto;
right: -40px;
}
.popup::after {
left: auto;
right: 36px;
}
}
@media (max-device-width: 520px), (max-width: 520px) {
.popup::after {
left: 125px;
}
} }

View File

@ -1,56 +0,0 @@
const html = require('choo/html');
const percent = require('../../utils').percent;
const radius = 73;
const oRadius = radius + 10;
const oDiameter = oRadius * 2;
const circumference = 2 * Math.PI * radius;
module.exports = function(progressRatio, indefinite = false) {
// HACK - never indefinite for MS Edge
if (/edge/i.test(navigator.userAgent)) {
indefinite = false;
}
const p = indefinite ? 0.2 : progressRatio;
const dashOffset = (1 - p) * circumference;
const progressPercent = html`
<text class="progress__percent" text-anchor="middle" x="50%" y="98">
${percent(progressRatio)}
</text>`;
return html`
<div class="progress">
<svg
width="${oDiameter}"
height="${oDiameter}"
viewPort="0 0 ${oDiameter} ${oDiameter}"
version="1.1">
<circle
class="progress__bg"
r="${radius}"
cx="${oRadius}"
cy="${oRadius}"
fill="transparent"/>
<circle
class="progress__indefinite ${indefinite ? '' : 'progress--invisible'}"
r="${radius}"
cx="${oRadius}"
cy="${oRadius}"
fill="transparent"
transform="rotate(-90 ${oRadius} ${oRadius})"
stroke-dasharray="${circumference}"
stroke-dashoffset="${dashOffset}"/>
<circle
class="progress__bar ${indefinite ? 'progress--invisible' : ''}"
r="${radius}"
cx="${oRadius}"
cy="${oRadius}"
fill="transparent"
transform="rotate(-90 ${oRadius} ${oRadius})"
stroke-dasharray="${circumference}"
stroke-dashoffset="${dashOffset}"/>
${indefinite ? '' : progressPercent}
</svg>
</div>
`;
};

View File

@ -1,43 +0,0 @@
.progress {
margin-top: 3px;
}
.progress__bg {
stroke: #eee;
stroke-width: 0.75em;
}
.progress__bar {
stroke: #3b9dff;
stroke-width: 0.75em;
transition: stroke-dashoffset 300ms linear;
}
.progress__indefinite {
stroke: #3b9dff;
stroke-width: 0.75em;
animation: 1s linear infinite spin;
transform-origin: center;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.progress__percent {
font-family: 'Segoe UI', 'SF Pro Text', sans-serif;
font-size: 43.2px;
letter-spacing: -0.78px;
line-height: 58px;
user-select: none;
}
.progress--invisible {
display: none;
}

View File

@ -5,16 +5,14 @@ module.exports = function(selected, options, translate, changed) {
let x = selected; let x = selected;
return html` return html`
<div class="select"> <select class="selectBox" id="${id}" onchange=${choose}>
<select id="${id}" onchange=${choose}>
${options.map( ${options.map(
i => i =>
html`<option value="${i}" ${ html`<option value="${i}" ${
i === selected ? 'selected' : '' i === selected ? 'selected' : ''
}>${translate(i)}</option>` }>${translate(i)}</option>`
)} )}
</select> </select>`;
</div>`;
function choose(event) { function choose(event) {
const target = event.target; const target = event.target;

View File

@ -1,46 +1,22 @@
.select {
background-color: var(--pageBGColor);
overflow: hidden;
padding: 4px 2px 4px 2px;
border: 1px dotted #0094fb88;
border-radius: 4px;
display: inline;
position: relative;
}
.select::after {
color: #0094fb;
content: '\25BC';
pointer-events: none;
font-size: 20px;
margin-left: -30px;
padding-right: 10px;
}
option { option {
padding: 0; padding: 0;
} }
select { .selectBox {
appearance: none; appearance: none;
outline: 0; outline: 0;
box-shadow: none; box-shadow: none;
border: 0; border: none;
background: #fff; border-radius: 0;
background-image: none; background-color: #e6e6e6;
font-size: 1em; font-size: 1em;
font-weight: 200; font-weight: 200;
margin: 0; margin: 0;
color: #0094fb; padding: 4px 2px 4px 2px;
cursor: pointer; cursor: pointer;
padding-right: 40px;
} }
select:active { select:active {
background-color: var(--pageBGColor); background-color: var(--pageBGColor);
border: 0; border: 0;
} }
#arrow {
position: relative;
}

View File

@ -1,25 +1,26 @@
const html = require('choo/html'); const html = require('choo/html');
const passwordInput = require('../passwordInput'); const passwordInput = require('../passwordInput');
module.exports = function(state, emit) { module.exports = function(state) {
const file = state.storage.getFileById(state.params.id); const checked = state.password ? 'checked' : '';
const label = state.password ? 'addPasswordLabel' : 'addPasswordMessage';
return html` return html`
<div class="setPasswordSection"> <div class="setPasswordSection">
<div class="checkbox"> <div class="checkbox">
<input <input
${file.hasPassword ? 'disabled' : ''} class="checkbox__input" id="add-password"
${file.hasPassword || state.passwordSetError ? 'checked' : ''}
class="checkbox__input"
id="add-password"
type="checkbox" type="checkbox"
${checked}
autocomplete="off" autocomplete="off"
onchange=${togglePasswordInput}/> onchange=${togglePasswordInput}/>
<label class="checkbox__label" for="add-password"> <label class="checkbox__label" for="add-password">
${state.translate('requirePasswordCheckbox')} ${state.translate(label)}
</label> </label>
</div> </div>
${passwordInput(file, state, emit)}
${passwordInput(state)}
</div>`; </div>`;
function togglePasswordInput(e) { function togglePasswordInput(e) {
@ -28,9 +29,13 @@ module.exports = function(state, emit) {
document document
.querySelector('.passwordInput') .querySelector('.passwordInput')
.classList.toggle('passwordInput--hidden', !boxChecked); .classList.toggle('passwordInput--hidden', !boxChecked);
const label = document.querySelector('.checkbox__label');
if (boxChecked) { if (boxChecked) {
label.innerHTML = state.translate('addPasswordLabel');
unlockInput.focus(); unlockInput.focus();
} else { } else {
label.innerHTML = state.translate('addPasswordMessage');
unlockInput.value = ''; unlockInput.value = '';
} }
} }

View File

@ -1,11 +1,12 @@
.setPasswordSection { .setPasswordSection {
display: flex;
padding: 10px 0; padding: 10px 0;
max-width: 100%; max-width: 100%;
overflow-wrap: break-word;
} }
.checkbox { .checkbox {
min-height: 24px; flex: auto;
height: 24px;
} }
.checkbox__input { .checkbox__input {
@ -14,7 +15,8 @@
} }
.checkbox__label { .checkbox__label {
line-height: 23px; font-size: 13px;
line-height: 20px;
cursor: pointer; cursor: pointer;
color: var(--lightTextColor); color: var(--lightTextColor);
user-select: none; user-select: none;
@ -22,27 +24,21 @@
.checkbox__label::before { .checkbox__label::before {
content: ''; content: '';
height: 20px; height: 24px;
width: 20px; width: 24px;
margin-right: 10px; margin-right: 10px;
margin-left: 5px;
float: left; float: left;
border: 1px solid rgba(12, 12, 13, 0.3); background-color: #e6e6e6;
border-radius: 2px;
} }
.checkbox__input:focus + .checkbox__label::before, .checkbox__label:hover::before {
.checkbox:hover .checkbox__label::before { background-color: #d6d6d6;
border: 1px solid var(--primaryControlBGColor);
}
.checkbox__input:checked + .checkbox__label {
color: var(--textColor);
} }
.checkbox__input:checked + .checkbox__label::before { .checkbox__input:checked + .checkbox__label::before {
background-image: url('../assets/check-16-blue.svg'); background-image: url('../assets/lock.svg');
background-position: 2px 1px; background-position: 2px 2px;
background-repeat: no-repeat;
} }
.checkbox__input:disabled + .checkbox__label { .checkbox__input:disabled + .checkbox__label {
@ -50,20 +46,13 @@
} }
.checkbox__input:disabled + .checkbox__label::before { .checkbox__input:disabled + .checkbox__label::before {
background-image: url('../assets/check-16-blue.svg'); background-image: url('../assets/lock.svg');
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: 26px 26px; background-size: 26px 26px;
border: none; border: none;
cursor: auto; cursor: auto;
} }
@media (max-device-width: 520px), (max-width: 520px) { .setPasswordSection > .passwordInput--hidden {
.setPasswordSection { display: none;
align-self: center;
min-width: 95%;
}
.checkbox__label::before {
margin-left: 0;
}
} }

View File

@ -0,0 +1,15 @@
const html = require('choo/html');
module.exports = function(state) {
return html`
<div class="signupPromo">
<div class="signupPromo__title">${state.translate('signInPromoText')}</div>
<div class="signupPromo__info">${state.translate('signInExplanation')}</div>
<a href="/signin"
class="link signupPromo__link"
>
${state.translate('signInLearnMore')}
</a>
</div>
`;
};

View File

@ -0,0 +1,85 @@
.signupPromo {
display: flex;
flex-direction: column;
position: absolute;
right: 0;
height: 140px;
width: 150px;
background: #ffe900;
font-size: 13px;
font-weight: 500;
text-align: center;
justify-content: center;
}
.signupPromo::before {
content: '';
width: 0;
height: 0;
border-style: solid;
border-width: 0 35px 140px 0;
border-color: transparent #ffe900 transparent;
position: absolute;
right: 100%;
}
.signupPromo::after {
content: '';
width: 0;
height: 0;
border-style: solid;
border-width: 0 150px 35px 0;
border-color: transparent #ffe900 transparent;
position: absolute;
top: 100%;
left: 0%;
}
.signupPromo__title {
color: #0a84ff;
font-size: 22px;
font-style: italic;
font-weight: 600;
padding: 6px;
}
.signupPromo__info {
color: var(--lightTextColor);
padding: 6px;
}
.signupPromo__link {
z-index: 5;
}
.signupPromo__link:hover {
text-decoration: underline;
}
@media (max-device-width: 750px), (max-width: 750px) {
.signupPromo {
flex-direction: row;
align-items: center;
height: 40px;
width: 100%;
}
.signupPromo::before {
visibility: hidden;
}
.signupPromo::after {
border-width: 15px 50vw 0 50vw;
border-color: #ffe900 transparent transparent;
}
}
@media (max-device-width: 500px), (max-width: 500px) {
.signupPromo__link {
display: none;
}
.signupPromo {
overflow: hidden;
}
}

View File

@ -0,0 +1,11 @@
const html = require('choo/html');
module.exports = function(state) {
return html`
<div class="boxTitle">
${state.translate('uploadPageHeader')}
<div class="boxSubtitle">
${state.translate('pageHeaderCredits')}
</div>
</div>`;
};

View File

@ -0,0 +1,13 @@
.boxTitle {
font-size: 15px;
text-align: center;
font-weight: 500;
margin: 9px 0 19px 0;
color: var(--lightTextColor);
}
.boxSubtitle {
font-size: 12px;
font-weight: 300;
font-style: italic;
}

View File

@ -0,0 +1,64 @@
const html = require('choo/html');
const assets = require('../../../common/assets');
const bytes = require('../../utils').bytes;
const fileIcon = require('../fileIcon');
module.exports = function(file, state, emit) {
const transfer = state.transfer;
const transferState = transfer ? transfer.state : null;
const transferring = state.uploading || state.downloading;
const share = state.route.includes('share/');
const complete = share ? 'uploadedFile--completed' : '';
const cancelVisible =
transferring || state.route === '/' ? 'uploadedFile__cancel--visible' : '';
const stampClass =
share || transferState === 'complete' ? 'uploadedFile__stamp--visible' : '';
function cancel(event) {
event.preventDefault();
const btn = document.querySelector('.uploadedFile__cancel');
btn.disabled = true;
if (transferring) {
emit('cancel');
} else if (state.route === '/') {
emit('removeUpload', { file });
}
}
//const percent = share ? 100 : Math.floor(progressRatio * 100);
/*
style="
background: linear-gradient(to right,
#e8f2fe 0%,
#e8f2fe ${percent}%,
#fff ${percent}%,
#fff 100%);"
*/
return html`
<li class="uploadedFile ${complete}" id="${file.id}"
>
${fileIcon(file.name, file._hasPassword)}
<div class="uploadedFile__cancel ${cancelVisible}"
onclick=${cancel}>
<img
src="${assets.get('close-16.svg')}"
alt="cancel"/>
</div>
<div class="uploadedFile__fileData">
<p class="uploadedFile__fileName">${file.name}</p>
<p class="uploadedFile__fileInfo">
<span>${bytes(file.size)}</span>
</p>
</div>
<img src="${assets.get('sent-done.svg')}"
class="uploadedFile__stamp ${stampClass}"/>
</li>
`;
};

View File

@ -0,0 +1,70 @@
.uploadedFile {
margin: 11px;
list-style-type: none;
font-size: 11px;
line-height: 18px;
text-align: initial;
color: var(--lightTextColor);
background-color: var(--pageBGColor);
border: 1px solid #cececf;
box-sizing: border-box;
height: 53px;
border-radius: 4px;
position: relative;
}
.uploadedFile--completed {
background-color: #e8f2fe;
}
.uploadedFile__fileData {
margin: 8px 16px 8px 44px;
}
.uploadedFile__fileName {
margin: 0;
font-size: 13px;
font-weight: 500;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.uploadedFile__fileInfo {
margin: 0;
}
.uploadedFile__cancel {
float: right;
margin: 6px;
visibility: hidden;
}
.uploadedFile:hover .uploadedFile__cancel--visible {
visibility: visible;
}
.uploadedFile__stamp {
position: absolute;
top: -4px;
right: -8px;
visibility: hidden;
opacity: 0;
}
.uploadedFile__stamp--visible {
visibility: visible;
opacity: 1;
animation: stampDown 0.2s linear;
}
@keyframes stampDown {
0% {
opacity: 0;
transform: scale(1.5);
}
100% {
opacity: 1;
}
}

View File

@ -0,0 +1,12 @@
const html = require('choo/html');
const file = require('../uploadedFile');
module.exports = function(files, state, emit) {
//const progressRatio = state.transfer ? state.transfer.progressRatio : 0;
return html`
<ul class="uploadedFiles">
${files.map(f => file(f, state, emit))}
</ul>
`;
};

View File

@ -0,0 +1,10 @@
.uploadedFiles {
border: 1px solid rgba(12, 12, 13, 0.1);
border-radius: 4px;
box-sizing: border-box;
margin: 0;
padding: 0;
align-content: center;
flex: 1;
overflow-y: scroll;
}

View File

@ -0,0 +1,24 @@
const html = require('choo/html');
const assets = require('../../../common/assets');
// eslint-disable-next-line no-unused-vars
module.exports = function(state) {
return html`
<div class="account">
<img
src="${assets.get('user.svg')}"
onclick=${onclick}
alt="account"/>
<ul class=account_dropdown>
<li class=account_dropdown__item>Placeholder</li>
<li class=account_dropdown__item>Placeholder</li>
</ul>
</div>`;
function onclick(event) {
event.preventDefault();
const dropdown = document.querySelector('.account_dropdown');
dropdown.classList.toggle('visible');
}
};

View File

@ -0,0 +1,33 @@
.account {
position: absolute;
right: 0;
margin: 0 21px;
padding: 0;
}
.account_dropdown {
z-index: 1;
position: absolute;
top: 25px;
left: -10px;
width: 150px;
list-style-type: none;
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 4px;
background-color: var(--pageBGColor);
box-shadow: 0 5px 12px 0 rgba(0, 0, 0, 0.2);
padding: 11px 0;
visibility: hidden;
}
.account_dropdown__item {
padding: 0 14px;
color: var(--lightTextColor);
font-size: 13px;
line-height: 24px;
}
.account_dropdown__item:hover {
background-color: var(--primaryControlBGColor);
color: var(--primaryControlFGColor);
}

View File

@ -1,3 +1,4 @@
/* global MAXFILESIZE */
const b64 = require('base64-js'); const b64 = require('base64-js');
function arrayToB64(array) { function arrayToB64(array) {
@ -128,6 +129,27 @@ function openLinksInNewTab(links, should = true) {
return links; return links;
} }
function checkSize(files, oldfiles, translate) {
function size(arr) {
let total = 0;
for (let i = 0; i < arr.length; i++) {
total += arr[i].size;
}
return total;
}
const addSize = size(files);
if (addSize === 0) {
return;
}
const totalSize = addSize + size(oldfiles);
if (totalSize > MAXFILESIZE) {
// eslint-disable-next-line no-alert
alert(translate('fileTooBig', { size: bytes(MAXFILESIZE) }));
return;
}
}
module.exports = { module.exports = {
fadeOut, fadeOut,
delay, delay,
@ -140,5 +162,6 @@ module.exports = {
b64ToArray, b64ToArray,
loadShim, loadShim,
isFile, isFile,
openLinksInNewTab openLinksInNewTab,
checkSize
}; };

10
assets/addfile.svg Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="33px" height="33px" viewBox="0 0 33 33" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 51.1 (57501) - http://www.bohemiancoding.com/sketch -->
<title>Shape</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M13.799417,16.627417 L9.213417,21.213417 C8.45547258,21.9981748 8.46631225,23.2455866 9.23777985,24.0170541 C10.0092474,24.7885217 11.2566592,24.7993614 12.041417,24.041417 L16.627417,19.455417 L21.213417,24.041417 C21.9981748,24.7993614 23.2455866,24.7885217 24.0170541,24.0170541 C24.7885217,23.2455866 24.7993614,21.9981748 24.041417,21.213417 L19.455417,16.627417 L24.041417,12.041417 C24.5613302,11.5392681 24.7698422,10.7956612 24.5868113,10.0964024 C24.4037804,9.39714349 23.8576905,8.85105357 23.1584316,8.66802268 C22.4591728,8.4849918 21.7155659,8.69350383 21.213417,9.213417 L16.627417,13.799417 L12.041417,9.213417 C11.2566592,8.45547258 10.0092474,8.46631225 9.23777985,9.23777985 C8.46631225,10.0092474 8.45547258,11.2566592 9.213417,12.041417 L13.799417,16.627417 Z M16.627417,0.627416998 C25.463973,0.627416998 32.627417,7.790861 32.627417,16.627417 C32.627417,25.463973 25.463973,32.627417 16.627417,32.627417 C7.790861,32.627417 0.627416998,25.463973 0.627416998,16.627417 C0.627416998,7.790861 7.790861,0.627416998 16.627417,0.627416998 Z" id="Shape" fill="#737373" transform="translate(16.627417, 16.627417) rotate(45.000000) translate(-16.627417, -16.627417) "></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

4
assets/back-arrow.svg Normal file
View File

@ -0,0 +1,4 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M15.707 4.293a1 1 0 0 1 0 1.414L9.414 12l6.293 6.293a1 1 0 1 1-1.414 1.414l-7-7a1 1 0 0 1 0-1.414l7-7a1 1 0 0 1 1.414 0z" fill="#0A84FF" fill-opacity="1"></path></svg>

After

Width:  |  Height:  |  Size: 523 B

13
assets/blue_file.svg Normal file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="22px" height="32px" viewBox="0 0 22 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 51.1 (57501) - http://www.bohemiancoding.com/sketch -->
<title>Group 8</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" fill-opacity="0.5">
<g id="Group-8" fill="#0A84FF">
<path d="M2,0 L11.9428571,0 L21.665206,8.66172902 C21.8781869,8.85147558 22,9.12314407 22,9.40838886 L22,30 C22,31.1045695 21.1045695,32 20,32 L2,32 C0.8954305,32 1.3527075e-16,31.1045695 0,30 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z" id="Rectangle-14"></path>
<path d="M12,0 L22,9 L15,9 C13.3431458,9 12,7.65685425 12,6 L12,0 Z" id="Triangle"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 917 B

4
assets/lock-white.svg Normal file
View File

@ -0,0 +1,4 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M9 7a3 3 0 1 1 6 0v3H9V7zm-2 3V7a5 5 0 1 1 10 0v3h1a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h1z" fill="#FFFFFF" fill-opacity="1"></path></svg>

After

Width:  |  Height:  |  Size: 520 B

4
assets/lock.svg Normal file
View File

@ -0,0 +1,4 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M9 7a3 3 0 1 1 6 0v3H9V7zm-2 3V7a5 5 0 1 1 10 0v3h1a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h1z" fill="#0A84FF" fill-opacity="1"></path></svg>

After

Width:  |  Height:  |  Size: 520 B

13
assets/red_file.svg Normal file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="22px" height="32px" viewBox="0 0 22 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 51.1 (57501) - http://www.bohemiancoding.com/sketch -->
<title>Group 8</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" fill-opacity="0.5">
<g id="Group-8" fill="#D70022">
<path d="M2,0 L11.9428571,0 L21.665206,8.66172902 C21.8781869,8.85147558 22,9.12314407 22,9.40838886 L22,30 C22,31.1045695 21.1045695,32 20,32 L2,32 C0.8954305,32 1.3527075e-16,31.1045695 0,30 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z" id="Rectangle-14"></path>
<path d="M12,0 L22,9 L15,9 C13.3431458,9 12,7.65685425 12,6 L12,0 Z" id="Triangle"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 917 B

19
assets/sent-done.svg Normal file
View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="118px" height="59px" viewBox="0 0 118 59" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 51.1 (57501) - http://www.bohemiancoding.com/sketch -->
<title>Group</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group" transform="translate(2.000000, -5.000000)">
<g id="Group-5" transform="translate(86.841503, 34.526577) rotate(3.000000) translate(-86.841503, -34.526577) translate(54.341503, 2.526577)">
<circle id="Oval-2" stroke-opacity="0.5" stroke="#0C0C0D" stroke-width="3" transform="translate(32.499214, 31.954331) rotate(11.000000) translate(-32.499214, -31.954331) " cx="32.4992143" cy="31.9543312" r="27.0650466"></circle>
<path d="M28.020207,44.1875925 C27.4974152,44.1874763 26.9960763,43.9712589 26.6264624,43.5864984 L20.7124121,37.431951 C19.9653252,36.6269796 19.9760096,35.3474371 20.736426,34.5560981 C21.4968423,33.7647591 22.726385,33.7536403 23.4999012,34.5311077 L27.7521034,38.9562273 L40.2031506,20.4454003 C40.831247,19.5292815 42.051282,19.3107606 42.9375457,19.9556419 C43.8238095,20.6005231 44.0454042,21.868032 43.4341935,22.7964374 L29.6347427,43.3115953 C29.3022783,43.8117461 28.7737147,44.1326132 28.1917144,44.1875925 C28.1345816,44.1905933 28.0773398,44.1905933 28.020207,44.1875925 Z" id="Shape" fill="#12BC00" fill-rule="nonzero"></path>
</g>
<path d="M0,19.8728801 C3.80326861,19.2140466 7.96993528,19.2140466 12.5,19.8728801 C19.2950971,20.8611303 28.3355757,25.3728801 35,25.3728801 C41.6644243,25.3728801 47.8955852,19.5814393 55.5,18.3728801 C60.5696098,17.5671739 65.4029432,18.0671739 70,19.8728801" id="Line" stroke-opacity="0.5" stroke="#0C0C0D" stroke-width="3" stroke-linecap="square"></path>
<path d="M0,29.8728801 C3.80326861,29.2140466 7.96993528,29.2140466 12.5,29.8728801 C19.2950971,30.8611303 28.3355757,35.3728801 35,35.3728801 C41.6644243,35.3728801 47.8955852,29.5814393 55.5,28.3728801 C60.5696098,27.5671739 65.4029432,28.0671739 70,29.8728801" id="Line" stroke-opacity="0.5" stroke="#0C0C0D" stroke-width="3" stroke-linecap="square"></path>
<path d="M0,38.8728801 C3.80326861,38.2140466 7.96993528,38.2140466 12.5,38.8728801 C19.2950971,39.8611303 28.3355757,44.3728801 35,44.3728801 C41.6644243,44.3728801 47.8955852,38.5814393 55.5,37.3728801 C60.5696098,36.5671739 65.4029432,37.0671739 70,38.8728801" id="Line" stroke-opacity="0.5" stroke="#0C0C0D" stroke-width="3" stroke-linecap="square"></path>
<path d="M0,48.8728801 C3.80326861,48.2140466 7.96993528,48.2140466 12.5,48.8728801 C19.2950971,49.8611303 28.3355757,54.3728801 35,54.3728801 C41.6644243,54.3728801 47.8955852,48.5814393 55.5,47.3728801 C60.5696098,46.5671739 65.4029432,47.0671739 70,48.8728801" id="Line" stroke-opacity="0.5" stroke="#0C0C0D" stroke-width="3" stroke-linecap="square"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -1 +0,0 @@
<svg width="57" height="57" viewBox="0 0 57 57" xmlns="http://www.w3.org/2000/svg"><title>upload</title><g transform="translate(1 1)" stroke-width="2" stroke="#7FC9FD" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"><path d="M18 24l10-9 10 9M28 39.545V15"/><circle cx="27.5" cy="27.5" r="27.5"/></g></svg>

Before

Width:  |  Height:  |  Size: 336 B

1
assets/user.svg Normal file
View File

@ -0,0 +1 @@
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="194 -104 1000 1000"><style>.st0{fill:#C3CFD8;} .st1{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}</style><path class="st0" d="M694-104.3c276.1 0 500 223.9 500 500s-223.9 500-500 500-500-223.9-500-500c0-276.2 223.9-500 500-500z"/><path class="st1" d="M892.4 585.9c10 3.1 19.1 5.7 27.5 8.2 34.5 10 44.8 54.6 17.5 78.1-65.4 56.5-150.7 90.8-244 90.8-92.8 0-177.6-33.8-242.9-89.8-27.4-23.5-17.3-68.2 17.4-78.3 9.2-2.7 19.2-5.5 30.2-9 62.6-19.5 92.6-43.7 98.2-68.7 0-.1 0-.2.1-.2 3.6-16.1-2.8-32.9-15.5-43.5-26.4-22.1-37.1-59.8-44.1-87.5-.8-3.2-1.7-6.5-2.5-9.8-12.1-2.1-25.4-17.3-32.2-38.5-8.2-25.5-3.9-49.8 9.6-54.1 1.3-.4 2.6-.4 3.9-.5-3.1-18.2-6.9-45.4-7.3-69.3-.1-5.2-.2-10.9-.2-16.9 0-3 .1-6.1.1-9.3 0-1.6.1-3.2.2-4.8.1-1.6.2-3.2.3-4.9.9-13.1 2.9-26.8 7-40 7.4-23.7 21.6-45.4 47.4-57.3 5.8-2.7 11-6.4 15.1-11.3 22.4-26.4 49.1-39.6 74.2-45.4 6.9-1.6 13.6-2.6 20.1-3.2 3.2-.3 6.4-.5 9.5-.6 1.6-.1 3.1-.1 4.6-.1h4.5c11.7.3 22 1.8 29.6 3.7 50 12.3 89.2 38 116.4 69.5 13.5 15.8 23.9 33 30.7 50.7 3.4 8.9 5.9 17.9 7.4 26.9.8 4.5 1.3 9 1.6 13.5.3 4.5.3 8.9.1 13.4-1.5 27.1-4.4 45.9-7.3 60.1-2.3 11.1.1 22.2 5 32.4 4.9 10.3 5.3 26.7.2 43.9-6.1 20.3-18.3 35.3-29.8 38.7-2.2 8.1-3.8 13.5-3.9 13.5-3.8 29-10.7 59.8-35.3 82.9-10.5 9.8-15 24.5-13.1 38.7.5 3.5 1 6.6 1.6 9.2 5.6 25.1 35.5 49.3 98.1 68.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -26,6 +26,7 @@ uploadSuccessConfirmHeader = Ready to Send
uploadSvgAlt = Upload uploadSvgAlt = Upload
uploadSuccessTimingHeader = The link to your file will expire after 1 download or in 24 hours. 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 }. expireInfo = The link to your file will expire after { $downloadCount } or { $timespan }.
frontPageExpireInfo = Expires after { $downloadCount } or { $timespan }
downloadCount = { $num -> downloadCount = { $num ->
[one] 1 download [one] 1 download
*[other] { $num } downloads *[other] { $num } downloads
@ -34,6 +35,14 @@ timespanHours = { $num ->
[one] 1 hour [one] 1 hour
*[other] { $num } hours *[other] { $num } hours
} }
timespanMinutes = { $num ->
[one] 1 minute
*[other] { $num } minutes
}
timespanWeeks = { $num ->
[one] 1 week
*[other] { $num } weeks
}
copyUrlFormLabelWithName = Copy and share the link to send your file: { $filename } copyUrlFormLabelWithName = Copy and share the link to send your file: { $filename }
copyUrlFormButton = Copy to clipboard copyUrlFormButton = Copy to clipboard
copiedUrl = Copied! copiedUrl = Copied!
@ -117,3 +126,30 @@ passwordIsSet = Password set
maxPasswordLength = Maximum password length: { $length } maxPasswordLength = Maximum password length: { $length }
# A short status message shown when there was an error setting the password # A short status message shown when there was an error setting the password
passwordSetError = This password could not be set passwordSetError = This password could not be set
pageHeaderCredits = from the makers of Firefox
addFilesButton = Add file(s)
uploadFilesButton = Send
uploadFileProgress = Sending
uploadDropDragMessage = Drop files here
uploadDropClickMessage = or click to select a file
addPasswordMessage = Protect with password
addPasswordLabel = Password:
copyUrlLabel = Copy and share this link:
passwordReminder = don't forget the password too
signInPromoText = Sign In/Up!
signInExplanation = It's free and gives you many more Send options
signInLearnMore = Learn more!
downloadProgressButton = Downloading... { $progress }
downloadMessage2 = Firefox Send lets you share files with a safe, private, and encrypted link that automatically expires to ensure your stuff does not remain online forever.
signInEmailEnter = Enter your Email
emailEntryPlaceholder = Email
signInContinueMessage = to continue to Firefox Send
signInContinueButton = Continue
accountBenefitTitle = With a free Firefox Account with Send you can:
accountBenefitMultiFile = Send multiple files at once
accountBenefitLargeFiles = Upload larger files (up to { $size } GB)
accountBenefitExpiry = Have more expiry options
accountBenefitSync = Manage your uploads across devices
accountBenefitNotify = Be notified when your files are downloaded
accountBenefitMore = Do a lot more!

View File

@ -5,7 +5,7 @@ module.exports = async (req, res) => {
const meta = await storage.metadata(req.params.id); const meta = await storage.metadata(req.params.id);
res.set('WWW-Authenticate', `send-v1 ${meta.nonce}`); res.set('WWW-Authenticate', `send-v1 ${meta.nonce}`);
res.send({ res.send({
password: meta.pwd requiresPassword: meta.pwd
}); });
} catch (e) { } catch (e) {
res.sendStatus(404); res.sendStatus(404);

View File

@ -49,7 +49,7 @@ module.exports = function(app) {
next(); next();
}); });
app.use(express.json()); app.use(express.json());
app.get('/', language, pages.index); app.get('/', language, pages.blank);
app.get('/legal', language, pages.legal); app.get('/legal', language, pages.legal);
app.get('/jsconfig.js', require('./jsconfig')); app.get('/jsconfig.js', require('./jsconfig'));
app.get(`/share/:id${ID_REGEX}`, language, pages.blank); app.get(`/share/:id${ID_REGEX}`, language, pages.blank);

View File

@ -19,16 +19,15 @@ module.exports = {
download: async function(req, res, next) { download: async function(req, res, next) {
const id = req.params.id; const id = req.params.id;
try { try {
const { nonce, pwd } = await storage.metadata(id); const { nonce, pwd } = await storage.metadata(id);
res.set('WWW-Authenticate', `send-v1 ${nonce}`); res.set('WWW-Authenticate', `send-v1 ${nonce}`);
res.send( res.send(
stripEvents( stripEvents(
routes.toString( routes.toString(
`/download/${req.params.id}`, `/download/${id}`,
Object.assign(state(req), { Object.assign(state(req), {
fileInfo: { nonce, requiresPassword: +pwd } fileInfo: { nonce, requiresPassword: pwd }
}) })
) )
) )