${button}
{
console.error('ERROR ' + e + ' ' + e.stack);
}
diff --git a/app/api.js b/app/api.js
index ccc7854e..2d1238c2 100644
--- a/app/api.js
+++ b/app/api.js
@@ -11,6 +11,15 @@ if (!fileProtocolWssUrl) {
fileProtocolWssUrl = 'wss://send.firefox.com/api/ws';
}
+export class ConnectionError extends Error {
+ constructor(cancelled, duration, size) {
+ super(cancelled ? '0' : 'connection closed');
+ this.cancelled = cancelled;
+ this.duration = duration;
+ this.size = size;
+ }
+}
+
export function setFileProtocolWssUrl(url) {
localStorage && localStorage.setItem('wssURL', url);
fileProtocolWssUrl = url;
@@ -34,7 +43,7 @@ function post(obj, bearerToken) {
'Content-Type': 'application/json'
};
if (bearerToken) {
- h['Authentication'] = `Bearer ${bearerToken}`;
+ h['Authorization'] = `Bearer ${bearerToken}`;
}
return {
method: 'POST',
@@ -52,7 +61,10 @@ async function fetchWithAuth(url, params, keychain) {
const result = {};
params = params || {};
const h = await keychain.authHeader();
- params.headers = new Headers({ Authorization: h });
+ params.headers = new Headers({
+ Authorization: h,
+ 'Content-Type': 'application/json'
+ });
const response = await fetch(url, params);
result.response = response;
result.ok = response.ok;
@@ -137,17 +149,25 @@ export async function setPassword(id, owner_token, keychain) {
}
function asyncInitWebSocket(server) {
- return new Promise(resolve => {
- const ws = new WebSocket(server);
- ws.onopen = () => {
- resolve(ws);
- };
+ return new Promise((resolve, reject) => {
+ try {
+ const ws = new WebSocket(server);
+ ws.addEventListener('open', () => resolve(ws), { once: true });
+ } catch (e) {
+ reject(new ConnectionError(false));
+ }
});
}
function listenForResponse(ws, canceller) {
return new Promise((resolve, reject) => {
+ function handleClose(event) {
+ // a 'close' event before a 'message' event means the request failed
+ ws.removeEventListener('message', handleMessage);
+ reject(new ConnectionError(canceller.cancelled));
+ }
function handleMessage(msg) {
+ ws.removeEventListener('close', handleClose);
try {
const response = JSON.parse(msg.data);
if (response.error) {
@@ -156,13 +176,11 @@ function listenForResponse(ws, canceller) {
resolve(response);
}
} catch (e) {
- ws.close();
- canceller.cancelled = true;
- canceller.error = e;
reject(e);
}
}
ws.addEventListener('message', handleMessage, { once: true });
+ ws.addEventListener('close', handleClose, { once: true });
});
}
@@ -176,6 +194,8 @@ async function upload(
onprogress,
canceller
) {
+ let size = 0;
+ const start = Date.now();
const host = window.location.hostname;
const port = window.location.port;
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
@@ -203,31 +223,41 @@ async function upload(
const reader = stream.getReader();
let state = await reader.read();
- let size = 0;
while (!state.done) {
- const buf = state.value;
if (canceller.cancelled) {
- throw canceller.error;
+ ws.close();
}
-
+ if (ws.readyState !== WebSocket.OPEN) {
+ break;
+ }
+ const buf = state.value;
ws.send(buf);
-
onprogress(size);
size += buf.length;
state = await reader.read();
- while (ws.bufferedAmount > ECE_RECORD_SIZE * 2) {
+ while (
+ ws.bufferedAmount > ECE_RECORD_SIZE * 2 &&
+ ws.readyState === WebSocket.OPEN &&
+ !canceller.cancelled
+ ) {
await delay();
}
}
- const footer = new Uint8Array([0]);
- ws.send(footer);
+ if (ws.readyState === WebSocket.OPEN) {
+ ws.send(new Uint8Array([0])); //EOF
+ }
await completedResponse;
- ws.close();
+ uploadInfo.duration = Date.now() - start;
return uploadInfo;
} catch (e) {
- ws.close(4000);
+ e.size = size;
+ e.duration = Date.now() - start;
throw e;
+ } finally {
+ if (![WebSocket.CLOSED, WebSocket.CLOSING].includes(ws.readyState)) {
+ ws.close();
+ }
}
}
@@ -244,7 +274,6 @@ export function uploadWs(
return {
cancel: function() {
- canceller.error = new Error(0);
canceller.cancelled = true;
},
@@ -284,7 +313,7 @@ async function downloadS(id, keychain, signal) {
return response.body;
}
-async function tryDownloadStream(id, keychain, signal, tries = 1) {
+async function tryDownloadStream(id, keychain, signal, tries = 2) {
try {
const result = await downloadS(id, keychain, signal);
return result;
@@ -306,18 +335,19 @@ export function downloadStream(id, keychain) {
}
return {
cancel,
- result: tryDownloadStream(id, keychain, controller.signal, 2)
+ result: tryDownloadStream(id, keychain, controller.signal)
};
}
//////////////////
-function download(id, keychain, onprogress, canceller) {
+async function download(id, keychain, onprogress, canceller) {
+ const auth = await keychain.authHeader();
const xhr = new XMLHttpRequest();
canceller.oncancel = function() {
xhr.abort();
};
- return new Promise(async function(resolve, reject) {
+ return new Promise(function(resolve, reject) {
xhr.addEventListener('loadend', function() {
canceller.oncancel = function() {};
const authHeader = xhr.getResponseHeader('WWW-Authenticate');
@@ -337,7 +367,6 @@ function download(id, keychain, onprogress, canceller) {
onprogress(event.loaded);
}
});
- const auth = await keychain.authHeader();
xhr.open('get', getApiUrl(`/api/download/blob/${id}`));
xhr.setRequestHeader('Authorization', auth);
xhr.responseType = 'blob';
@@ -346,7 +375,7 @@ function download(id, keychain, onprogress, canceller) {
});
}
-async function tryDownload(id, keychain, onprogress, canceller, tries = 1) {
+async function tryDownload(id, keychain, onprogress, canceller, tries = 2) {
try {
const result = await download(id, keychain, onprogress, canceller);
return result;
@@ -367,7 +396,7 @@ export function downloadFile(id, keychain, onprogress) {
}
return {
cancel,
- result: tryDownload(id, keychain, onprogress, canceller, 2)
+ result: tryDownload(id, keychain, onprogress, canceller)
};
}
@@ -391,17 +420,6 @@ export async function setFileList(bearerToken, kid, data) {
return response.ok;
}
-export function sendMetrics(blob) {
- if (!navigator.sendBeacon) {
- return;
- }
- try {
- navigator.sendBeacon(getApiUrl('/api/metrics'), blob);
- } catch (e) {
- console.error(e);
- }
-}
-
export async function getConstants() {
const response = await fetch(getApiUrl('/config'));
diff --git a/app/archive.js b/app/archive.js
index 45517754..683cc370 100644
--- a/app/archive.js
+++ b/app/archive.js
@@ -14,11 +14,12 @@ function isDupe(newFile, array) {
}
export default class Archive {
- constructor(files = [], defaultTimeLimit = 86400) {
+ constructor(files = [], defaultTimeLimit = 86400, defaultDownloadLimit = 1) {
this.files = Array.from(files);
this.defaultTimeLimit = defaultTimeLimit;
+ this.defaultDownloadLimit = defaultDownloadLimit;
this.timeLimit = defaultTimeLimit;
- this.dlimit = 1;
+ this.dlimit = defaultDownloadLimit;
this.password = null;
}
@@ -76,7 +77,7 @@ export default class Archive {
clear() {
this.files = [];
- this.dlimit = 1;
+ this.dlimit = this.defaultDownloadLimit;
this.timeLimit = this.defaultTimeLimit;
this.password = null;
}
diff --git a/app/capabilities.js b/app/capabilities.js
index 6e3af90f..d43a6b10 100644
--- a/app/capabilities.js
+++ b/app/capabilities.js
@@ -1,5 +1,5 @@
-/* global AUTH_CONFIG LOCALE */
-import { browserName } from './utils';
+/* global AUTH_CONFIG */
+import { browserName, locale } from './utils';
async function checkCrypto() {
try {
@@ -76,8 +76,9 @@ async function polyfillStreams() {
}
export default async function getCapabilities() {
- const serviceWorker =
- 'serviceWorker' in navigator && browserName() !== 'edge';
+ const browser = browserName();
+ const isMobile = /mobi|android/i.test(navigator.userAgent);
+ const serviceWorker = 'serviceWorker' in navigator && browser !== 'edge';
let crypto = await checkCrypto();
const nativeStreams = checkStreams();
let polyStreams = false;
@@ -91,19 +92,23 @@ export default async function getCapabilities() {
account = false;
}
const share =
- typeof navigator.share === 'function' && LOCALE.startsWith('en'); // en until strings merge
+ isMobile &&
+ typeof navigator.share === 'function' &&
+ locale().startsWith('en'); // en until strings merge
const standalone =
window.matchMedia('(display-mode: standalone)').matches ||
navigator.standalone;
+ const mobileFirefox = browser === 'firefox' && isMobile;
+
return {
account,
crypto,
serviceWorker,
streamUpload: nativeStreams || polyStreams,
streamDownload:
- nativeStreams && serviceWorker && browserName() !== 'safari',
+ nativeStreams && serviceWorker && browser !== 'safari' && !mobileFirefox,
multifile: nativeStreams || polyStreams,
share,
standalone
diff --git a/app/controller.js b/app/controller.js
index 47ec3ecc..8c6945ac 100644
--- a/app/controller.js
+++ b/app/controller.js
@@ -1,12 +1,13 @@
-import FileSender from './fileSender';
import FileReceiver from './fileReceiver';
-import { copyToClipboard, delay, openLinksInNewTab, percent } from './utils';
-import * as metrics from './metrics';
-import { bytes } from './utils';
-import okDialog from './ui/okDialog';
+import FileSender from './fileSender';
import copyDialog from './ui/copyDialog';
+import faviconProgressbar from './ui/faviconProgressbar';
+import okDialog from './ui/okDialog';
import shareDialog from './ui/shareDialog';
import signupDialog from './ui/signupDialog';
+import surveyDialog from './ui/surveyDialog';
+import { bytes, locale } from './utils';
+import { copyToClipboard, delay, openLinksInNewTab, percent } from './utils';
export default function(state, emitter) {
let lastRender = 0;
@@ -28,6 +29,7 @@ export default function(state, emitter) {
if (updateTitle) {
emitter.emit('DOMTitleChange', percent(state.transfer.progressRatio));
}
+ faviconProgressbar.updateFavicon(state.transfer.progressRatio);
render();
}
@@ -35,7 +37,8 @@ export default function(state, emitter) {
document.addEventListener('blur', () => (updateTitle = true));
document.addEventListener('focus', () => {
updateTitle = false;
- emitter.emit('DOMTitleChange', 'Firefox Send');
+ emitter.emit('DOMTitleChange', 'Send');
+ faviconProgressbar.updateFavicon(0);
});
checkFiles();
});
@@ -48,9 +51,8 @@ export default function(state, emitter) {
state.user.login(email);
});
- emitter.on('logout', () => {
- state.user.logout();
- metrics.loggedOut({ trigger: 'button' });
+ emitter.on('logout', async () => {
+ await state.user.logout();
emitter.emit('pushState', '/');
});
@@ -64,24 +66,17 @@ export default function(state, emitter) {
emitter.on('delete', async ownedFile => {
try {
- metrics.deletedUpload({
- size: ownedFile.size,
- time: ownedFile.time,
- speed: ownedFile.speed,
- type: ownedFile.type,
- ttl: ownedFile.expiresAt - Date.now(),
- location
- });
state.storage.remove(ownedFile.id);
await ownedFile.del();
} catch (e) {
- state.raven.captureException(e);
+ state.sentry.captureException(e);
}
render();
});
emitter.on('cancel', () => {
state.transfer.cancel();
+ faviconProgressbar.updateFavicon(0);
});
emitter.on('addFiles', async ({ files }) => {
@@ -96,9 +91,6 @@ export default function(state, emitter) {
state.LIMITS.MAX_FILES_PER_ARCHIVE
);
} catch (e) {
- if (e.message === 'fileTooBig' && maxSize < state.LIMITS.MAX_FILE_SIZE) {
- return emitter.emit('signup-cta', 'size');
- }
state.modal = okDialog(
state.translate(e.message, {
size: bytes(maxSize),
@@ -118,7 +110,7 @@ export default function(state, emitter) {
source: query.utm_source,
term: query.utm_term
});
- state.modal = signupDialog(source);
+ state.modal = signupDialog();
render();
});
@@ -154,12 +146,10 @@ export default function(state, emitter) {
const links = openLinksInNewTab();
await delay(200);
- const start = Date.now();
try {
const ownedFile = await sender.upload(archive, state.user.bearerToken);
state.storage.totalUploads += 1;
- const duration = Date.now() - start;
- metrics.completedUpload(archive, duration);
+ faviconProgressbar.updateFavicon(0);
state.storage.addFile(ownedFile);
// TODO integrate password into /upload request
@@ -175,14 +165,21 @@ export default function(state, emitter) {
} catch (err) {
if (err.message === '0') {
//cancelled. do nothing
- const duration = Date.now() - start;
- metrics.cancelledUpload(archive, duration);
render();
+ } else if (err.message === '401') {
+ const refreshed = await state.user.refresh();
+ if (refreshed) {
+ return emitter.emit('upload');
+ }
+ emitter.emit('pushState', '/error');
} else {
// eslint-disable-next-line no-console
console.error(err);
- state.raven.captureException(err);
- metrics.stoppedUpload(archive);
+ state.sentry.withScope(scope => {
+ scope.setExtra('duration', err.duration);
+ scope.setExtra('size', err.size);
+ state.sentry.captureException(err);
+ });
emitter.emit('pushState', '/error');
}
} finally {
@@ -225,19 +222,20 @@ export default function(state, emitter) {
if (!file.requiresPassword) {
return emitter.emit('pushState', '/404');
}
+ } else {
+ console.error(e);
+ return emitter.emit('pushState', '/error');
}
}
render();
});
- emitter.on('download', async file => {
+ emitter.on('download', async () => {
state.transfer.on('progress', updateProgress);
state.transfer.on('decrypting', render);
state.transfer.on('complete', render);
const links = openLinksInNewTab();
- const size = file.size;
- const start = Date.now();
try {
const dl = state.transfer.download({
stream: state.capabilities.streamDownload
@@ -245,12 +243,7 @@ export default function(state, emitter) {
render();
await dl;
state.storage.totalDownloads += 1;
- const duration = Date.now() - start;
- metrics.completedDownload({
- size,
- duration,
- password_protected: file.requiresPassword
- });
+ faviconProgressbar.updateFavicon(0);
} catch (err) {
if (err.message === '0') {
// download cancelled
@@ -261,12 +254,11 @@ export default function(state, emitter) {
state.transfer = null;
const location = err.message === '404' ? '/404' : '/error';
if (location === '/error') {
- state.raven.captureException(err);
- const duration = Date.now() - start;
- metrics.stoppedDownload({
- size,
- duration,
- password_protected: file.requiresPassword
+ state.sentry.withScope(scope => {
+ scope.setExtra('duration', err.duration);
+ scope.setExtra('size', err.size);
+ scope.setExtra('progress', err.progress);
+ state.sentry.captureException(err);
});
}
emitter.emit('pushState', location);
@@ -278,7 +270,22 @@ export default function(state, emitter) {
emitter.on('copy', ({ url }) => {
copyToClipboard(url);
- // metrics.copiedLink({ location });
+ });
+
+ emitter.on('closeModal', () => {
+ if (
+ state.PREFS.surveyUrl &&
+ ['copy', 'share'].includes(state.modal.type) &&
+ locale().startsWith('en') &&
+ (state.storage.totalUploads > 1 || state.storage.totalDownloads > 0) &&
+ !state.user.surveyed
+ ) {
+ state.user.surveyed = true;
+ state.modal = surveyDialog();
+ } else {
+ state.modal = null;
+ }
+ render();
});
setInterval(() => {
diff --git a/app/experiments.js b/app/experiments.js
index 28b4e851..8e432e0a 100644
--- a/app/experiments.js
+++ b/app/experiments.js
@@ -1,6 +1,22 @@
import hash from 'string-hash';
+import Account from './ui/account';
-const experiments = {};
+const experiments = {
+ signin_button_color: {
+ eligible: function() {
+ return true;
+ },
+ variant: function() {
+ return ['white-primary', 'primary', 'white-violet', 'violet'][
+ Math.floor(Math.random() * 4)
+ ];
+ },
+ run: function(variant, state) {
+ const account = state.cache(Account, 'account');
+ account.buttonClass = variant;
+ }
+ }
+};
//Returns a number between 0 and 1
// eslint-disable-next-line no-unused-vars
@@ -25,23 +41,12 @@ export default function initialize(state, emitter) {
xp.run(+state.query.v, state, emitter);
}
});
-
- if (!state.storage.get('testpilot_ga__cid')) {
- // first ever visit. check again after cid is assigned.
- emitter.on('DOMContentLoaded', () => {
- checkExperiments(state, emitter);
- });
+ const enrolled = state.storage.enrolled;
+ // single experiment per session for now
+ const id = Object.keys(enrolled)[0];
+ if (Object.keys(experiments).includes(id)) {
+ experiments[id].run(enrolled[id], state, emitter);
} else {
- const enrolled = state.storage.enrolled.filter(([id, variant]) => {
- const xp = experiments[id];
- if (xp) {
- xp.run(variant, state, emitter);
- }
- return !!xp;
- });
- // single experiment per session for now
- if (enrolled.length === 0) {
- checkExperiments(state, emitter);
- }
+ checkExperiments(state, emitter);
}
}
diff --git a/app/fileReceiver.js b/app/fileReceiver.js
index 58182999..85065429 100644
--- a/app/fileReceiver.js
+++ b/app/fileReceiver.js
@@ -1,7 +1,7 @@
import Nanobus from 'nanobus';
import Keychain from './keychain';
import { delay, bytes, streamToArrayBuffer } from './utils';
-import { downloadFile, metadata, getApiUrl } from './api';
+import { downloadFile, metadata, getApiUrl, reportLink } from './api';
import { blobStream } from './streams';
import Zip from './zip';
@@ -53,6 +53,10 @@ export default class FileReceiver extends Nanobus {
this.state = 'ready';
}
+ async reportLink(reason) {
+ await reportLink(this.fileInfo.id, this.keychain, reason);
+ }
+
sendMessageToSw(msg) {
return new Promise((resolve, reject) => {
const channel = new MessageChannel();
@@ -112,6 +116,7 @@ export default class FileReceiver extends Nanobus {
}
async downloadStream(noSave = false) {
+ const start = Date.now();
const onprogress = p => {
this.progress = [p, this.fileInfo.size];
this.emit('progress');
@@ -153,9 +158,7 @@ export default class FileReceiver extends Nanobus {
const downloadPath = `/api/download/${this.fileInfo.id}`;
let downloadUrl = getApiUrl(downloadPath);
if (downloadUrl === downloadPath) {
- downloadUrl = `${location.protocol}//${location.host}/api/download/${
- this.fileInfo.id
- }`;
+ downloadUrl = `${location.protocol}//${location.host}${downloadPath}`;
}
const a = document.createElement('a');
a.href = downloadUrl;
@@ -164,11 +167,29 @@ export default class FileReceiver extends Nanobus {
}
let prog = 0;
+ let hangs = 0;
while (prog < this.fileInfo.size) {
const msg = await this.sendMessageToSw({
request: 'progress',
id: this.fileInfo.id
});
+ if (msg.progress === prog) {
+ hangs++;
+ } else {
+ hangs = 0;
+ }
+ if (hangs > 30) {
+ // TODO: On Chrome we don't get a cancel
+ // signal so one is indistinguishable from
+ // a hang. We may be able to detect
+ // which end is hung in the service worker
+ // to improve on this.
+ const e = new Error('hung download');
+ e.duration = Date.now() - start;
+ e.size = this.fileInfo.size;
+ e.progress = prog;
+ throw e;
+ }
prog = msg.progress;
onprogress(prog);
await delay(1000);
@@ -203,24 +224,6 @@ async function saveFile(file) {
if (navigator.msSaveBlob) {
navigator.msSaveBlob(blob, file.name);
return resolve();
- } else if (/iPhone|fxios/i.test(navigator.userAgent)) {
- // This method is much slower but createObjectURL
- // is buggy on iOS
- const reader = new FileReader();
- reader.addEventListener('loadend', function() {
- if (reader.error) {
- return reject(reader.error);
- }
- if (reader.result) {
- const a = document.createElement('a');
- a.href = reader.result;
- a.download = file.name;
- document.body.appendChild(a);
- a.click();
- }
- resolve();
- });
- reader.readAsDataURL(blob);
} else {
const downloadUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
diff --git a/app/fileSender.js b/app/fileSender.js
index 6985282e..e8bc6bea 100644
--- a/app/fileSender.js
+++ b/app/fileSender.js
@@ -44,7 +44,6 @@ export default class FileSender extends Nanobus {
}
async upload(archive, bearerToken) {
- const start = Date.now();
if (this.cancelled) {
throw new Error(0);
}
@@ -76,7 +75,6 @@ export default class FileSender extends Nanobus {
this.emit('progress'); // HACK to kick MS Edge
try {
const result = await this.uploadRequest.result;
- const time = Date.now() - start;
this.msg = 'notifyUploadEncryptDone';
this.uploadRequest = null;
this.progress = [1, 1];
@@ -87,8 +85,8 @@ export default class FileSender extends Nanobus {
name: archive.name,
size: archive.size,
manifest: archive.manifest,
- time: time,
- speed: archive.size / (time / 1000),
+ time: result.duration,
+ speed: archive.size / (result.duration / 1000),
createdAt: Date.now(),
expiresAt: Date.now() + archive.timeLimit * 1000,
secretKey: secretKey,
diff --git a/app/keychain.js b/app/keychain.js
index f82dd422..37951aa7 100644
--- a/app/keychain.js
+++ b/app/keychain.js
@@ -18,23 +18,6 @@ export default class Keychain {
false,
['deriveKey']
);
- this.encryptKeyPromise = this.secretKeyPromise.then(function(secretKey) {
- return crypto.subtle.deriveKey(
- {
- name: 'HKDF',
- salt: new Uint8Array(),
- info: encoder.encode('encryption'),
- hash: 'SHA-256'
- },
- secretKey,
- {
- name: 'AES-GCM',
- length: 128
- },
- false,
- ['encrypt', 'decrypt']
- );
- });
this.metaKeyPromise = this.secretKeyPromise.then(function(secretKey) {
return crypto.subtle.deriveKey(
{
diff --git a/app/locale.js b/app/locale.js
index 59d63442..23dfdb7c 100644
--- a/app/locale.js
+++ b/app/locale.js
@@ -1,8 +1,8 @@
-import { FluentBundle } from 'fluent';
+import { FluentBundle, FluentResource } from '@fluent/bundle';
function makeBundle(locale, ftl) {
const bundle = new FluentBundle(locale, { useIsolating: false });
- bundle.addMessages(ftl);
+ bundle.addResource(new FluentResource(ftl));
return bundle;
}
@@ -10,16 +10,16 @@ export async function getTranslator(locale) {
const bundles = [];
const { default: en } = await import('../public/locales/en-US/send.ftl');
if (locale !== 'en-US') {
- const {
- default: ftl
- } = await import(`../public/locales/${locale}/send.ftl`);
+ const { default: ftl } = await import(
+ `../public/locales/${locale}/send.ftl`
+ );
bundles.push(makeBundle(locale, ftl));
}
bundles.push(makeBundle('en-US', en));
return function(id, data) {
for (let bundle of bundles) {
if (bundle.hasMessage(id)) {
- return bundle.format(bundle.getMessage(id), data);
+ return bundle.formatPattern(bundle.getMessage(id).value, data);
}
}
};
diff --git a/app/main.css b/app/main.css
index d916ce90..6a42290e 100644
--- a/app/main.css
+++ b/app/main.css
@@ -1,13 +1,23 @@
-@tailwind preflight;
+@tailwind base;
+
+html {
+ line-height: 1.15;
+}
+
@tailwind components;
:not(input) {
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
user-select: none;
}
+:root {
+ --violet-gradient: linear-gradient(
+ -180deg,
+ rgb(144 89 255 / 80%) 0%,
+ rgb(144 89 255 / 40%) 100%
+ );
+}
+
a {
color: inherit;
text-decoration: none;
@@ -26,19 +36,20 @@ body {
}
.btn {
- @apply bg-blue-dark;
+ @apply bg-primary;
@apply text-white;
@apply cursor-pointer;
@apply py-4;
@apply px-6;
+ @apply font-semibold;
}
.btn:hover {
- @apply bg-blue-darker;
+ @apply bg-primary_accent;
}
.btn:focus {
- @apply bg-blue-darker;
+ @apply bg-primary_accent;
}
.checkbox {
@@ -56,8 +67,8 @@ body {
}
.checkbox > label::before {
- /* @apply bg-grey-lightest; */
- @apply border;
+ /* @apply bg-grey-10; */
+ @apply border-default;
@apply rounded-sm;
content: '';
@@ -68,16 +79,16 @@ body {
}
.checkbox > label:hover::before {
- @apply border-blue-dark;
+ @apply border-primary;
}
.checkbox > input:focus + label::before {
- @apply border-blue-dark;
+ @apply border-primary;
}
.checkbox > input:checked + label::before {
- @apply bg-blue-dark;
- @apply border-blue-dark;
+ @apply bg-primary;
+ @apply border-primary;
background-image: url('../assets/lock.svg');
background-position: center;
@@ -90,8 +101,8 @@ body {
}
.checkbox > input:disabled + label::before {
- @apply bg-blue-dark;
- @apply border-blue-dark;
+ @apply bg-primary;
+ @apply border-primary;
background-image: url('../assets/lock.svg');
background-position: center;
@@ -104,7 +115,7 @@ details {
overflow: hidden;
}
-details > summary::-webkit-details-marker {
+details > summary::marker {
display: none;
}
@@ -120,7 +131,7 @@ details[open] > summary > svg {
transform: rotate(90deg);
}
-footer li:hover {
+footer li a:hover {
text-decoration: underline;
}
@@ -139,9 +150,37 @@ footer li:hover {
white-space: nowrap;
}
-.intro {
- max-width: 100%;
- height: unset;
+.link-primary {
+ @apply text-primary;
+}
+
+.link-primary:hover {
+ @apply text-primary_accent;
+}
+
+.link-primary:focus {
+ @apply text-primary_accent;
+}
+
+.main-header img {
+ height: 32px;
+ width: auto;
+}
+
+.text-underline {
+ text-decoration: underline;
+}
+
+.d-block {
+ display: block;
+}
+
+.d-inline-block {
+ display: inline-block;
+}
+
+.align-middle {
+ vertical-align: middle;
}
.main {
@@ -149,39 +188,25 @@ footer li:hover {
position: relative;
max-width: 64rem;
width: 100%;
- height: 100%;
}
.main > section {
@apply bg-white;
}
-.mozilla-logo {
- background-image: url('../assets/mozilla-logo.svg');
- background-repeat: no-repeat;
- background-size: 100px, 32px;
- overflow: hidden;
- text-indent: 120%;
- white-space: nowrap;
- display: inline-block;
- height: 32px;
- width: 100px;
- flex-shrink: 0;
-}
-
#password-msg::after {
content: '\200b';
}
progress {
- @apply bg-grey-light;
+ @apply bg-grey-30;
@apply rounded-sm;
@apply w-full;
@apply h-1;
}
progress::-webkit-progress-bar {
- @apply bg-grey-light;
+ @apply bg-grey-30;
@apply rounded-sm;
@apply w-full;
@apply h-1;
@@ -192,19 +217,18 @@ progress::-webkit-progress-value {
background-image: -webkit-linear-gradient(
-45deg,
transparent 20%,
- rgba(255, 255, 255, 0.4) 20%,
- rgba(255, 255, 255, 0.4) 40%,
+ rgb(255 255 255 / 40%) 20%,
+ rgb(255 255 255 / 40%) 40%,
transparent 40%,
transparent 60%,
- rgba(255, 255, 255, 0.4) 60%,
- rgba(255, 255, 255, 0.4) 80%,
+ rgb(255 255 255 / 40%) 60%,
+ rgb(255 255 255 / 40%) 80%,
transparent 80%
),
- -webkit-linear-gradient(left, #0a84ff, #0a84ff);
+ -webkit-linear-gradient(left, var(--color-primary), var(--color-primary));
/* stylelint-enable */
border-radius: 2px;
background-size: 21px 20px, 100% 100%, 100% 100%;
- -webkit-animation: animate-stripes 1s linear infinite;
}
progress::-moz-progress-bar {
@@ -212,27 +236,21 @@ progress::-moz-progress-bar {
background-image: -moz-linear-gradient(
135deg,
transparent 20%,
- rgba(255, 255, 255, 0.4) 20%,
- rgba(255, 255, 255, 0.4) 40%,
+ rgb(255 255 255 / 40%) 20%,
+ rgb(255 255 255 / 40%) 40%,
transparent 40%,
transparent 60%,
- rgba(255, 255, 255, 0.4) 60%,
- rgba(255, 255, 255, 0.4) 80%,
+ rgb(255 255 255 / 40%) 60%,
+ rgb(255 255 255 / 40%) 80%,
transparent 80%
),
- -moz-linear-gradient(left, #0a84ff, #0a84ff);
+ -moz-linear-gradient(left, var(--color-primary), var(--color-primary));
/* stylelint-enable */
border-radius: 2px;
background-size: 21px 20px, 100% 100%, 100% 100%;
animation: animate-stripes 1s linear infinite;
}
-@-webkit-keyframes animate-stripes {
- 100% {
- background-position: -21px 0;
- }
-}
-
@keyframes animate-stripes {
100% {
background-position: -21px 0;
@@ -246,11 +264,9 @@ select {
}
@screen md {
- .intro {
- max-width: unset;
- height: unset;
- margin-bottom: -3rem;
- margin-right: -7rem;
+ .main-header img {
+ height: 48px;
+ width: auto;
}
.main {
@@ -260,22 +276,65 @@ select {
@apply m-auto;
@apply py-8;
- min-height: 36rem;
- max-height: 40rem;
+ max-height: 42rem;
width: calc(100% - 3rem);
}
}
+@screen dark {
+ body {
+ @apply text-grey-10;
+
+ background-image: unset;
+ }
+
+ .btn {
+ @apply bg-primary;
+ @apply text-white;
+ }
+
+ .btn:hover {
+ @apply bg-primary_accent;
+ }
+
+ .btn:focus {
+ @apply bg-primary_accent;
+ }
+
+ .link-primary {
+ @apply text-primary;
+ }
+
+ .link-primary:hover {
+ @apply text-primary_accent;
+ }
+
+ .link-primary:focus {
+ @apply text-primary_accent;
+ }
+
+ .main > section {
+ @apply bg-grey-90;
+ }
+
+ @screen md {
+ .main > section {
+ @apply border-default;
+ @apply border-grey-80;
+ }
+ }
+}
+
@tailwind utilities;
@responsive {
.shadow-light {
- box-shadow: 0 0 8px 0 rgba(12, 12, 13, 0.1);
+ box-shadow: 0 0 8px 0 rgb(12 12 13 / 10%);
}
.shadow-big {
- box-shadow: 0 0 32px 0 rgba(12, 12, 13, 0.1),
- 0 2px 16px 0 rgba(12, 12, 13, 0.05);
+ box-shadow: 0 12px 18px 2px rgb(34 0 51 / 4%),
+ 0 6px 22px 4px rgb(7 48 114 / 12%), 0 6px 10px -4px rgb(14 13 26 / 12%);
}
}
@@ -287,4 +346,67 @@ select {
.word-break-all {
word-break: break-all;
+ line-break: anywhere;
}
+
+.signin {
+ backface-visibility: hidden;
+ border-radius: 6px;
+ transition-property: transform, background-color;
+ transition-duration: 250ms;
+ transition-timing-function: cubic-bezier(0.07, 0.95, 0, 1);
+}
+
+.signin:hover,
+.signin:focus {
+ transform: scale(1.0625);
+}
+
+.signin:hover:active {
+ transform: scale(0.9375);
+}
+
+/* begin signin button color experiment */
+
+.white-primary {
+ @apply border-primary;
+ @apply border-2;
+ @apply text-primary;
+}
+
+.white-primary:hover,
+.white-primary:focus {
+ @apply bg-primary;
+ @apply text-white;
+}
+
+.primary {
+ @apply bg-primary;
+ @apply text-white;
+}
+
+.white-violet {
+ @apply border-violet;
+ @apply border-2;
+ @apply text-violet;
+}
+
+.white-violet:hover,
+.white-violet:focus {
+ @apply bg-violet;
+ @apply text-white;
+
+ background-image: var(--violet-gradient);
+}
+
+.violet {
+ @apply bg-violet;
+ @apply text-white;
+}
+
+.violet:hover,
+.violet:focus {
+ background-image: var(--violet-gradient);
+}
+
+/* end signin button color experiment */
diff --git a/app/main.js b/app/main.js
index 33a89c04..c6a89dce 100644
--- a/app/main.js
+++ b/app/main.js
@@ -1,7 +1,7 @@
-/* global DEFAULTS LIMITS LOCALE */
+/* global DEFAULTS LIMITS WEB_UI PREFS */
import 'core-js';
import 'fast-text-encoding'; // MS Edge support
-import 'fluent-intl-polyfill';
+import 'intl-pluralrules';
import choo from 'choo';
import nanotiming from 'nanotiming';
import routes from './routes';
@@ -10,17 +10,16 @@ import controller from './controller';
import dragManager from './dragManager';
import pasteManager from './pasteManager';
import storage from './storage';
-import metrics from './metrics';
import experiments from './experiments';
-import Raven from 'raven-js';
+import * as Sentry from '@sentry/browser';
import './main.css';
import User from './user';
import { getTranslator } from './locale';
import Archive from './archive';
-import { setTranslate } from './utils';
+import { setTranslate, locale } from './utils';
-if (navigator.doNotTrack !== '1' && window.RAVEN_CONFIG) {
- Raven.config(window.SENTRY_ID, window.RAVEN_CONFIG).install();
+if (navigator.doNotTrack !== '1' && window.SENTRY_CONFIG) {
+ Sentry.init(window.SENTRY_CONFIG);
}
if (process.env.NODE_ENV === 'production') {
@@ -45,27 +44,31 @@ if (process.env.NODE_ENV === 'production') {
}
}
- const translate = await getTranslator(LOCALE);
+ const translate = await getTranslator(locale());
setTranslate(translate);
+ // eslint-disable-next-line require-atomic-updates
window.initialState = {
LIMITS,
DEFAULTS,
- archive: new Archive([], DEFAULTS.EXPIRE_SECONDS),
+ WEB_UI,
+ PREFS,
+ archive: new Archive([], DEFAULTS.EXPIRE_SECONDS, DEFAULTS.DOWNLOADS),
capabilities,
translate,
storage,
- raven: Raven,
+ sentry: Sentry,
user: new User(storage, LIMITS, window.AUTH_CONFIG),
transfer: null,
- fileInfo: null
+ fileInfo: null,
+ locale: locale()
};
- const app = routes(choo());
+ const app = routes(choo({ hash: true }));
+ // eslint-disable-next-line require-atomic-updates
window.app = app;
- app.use(metrics);
+ app.use(experiments);
app.use(controller);
app.use(dragManager);
- app.use(experiments);
app.use(pasteManager);
app.mount('body');
})();
diff --git a/app/metrics.js b/app/metrics.js
deleted file mode 100644
index c256bf85..00000000
--- a/app/metrics.js
+++ /dev/null
@@ -1,178 +0,0 @@
-import storage from './storage';
-import { platform } from './utils';
-import { sendMetrics } from './api';
-
-let appState = null;
-// let experiment = null;
-const HOUR = 1000 * 60 * 60;
-const events = [];
-let session_id = Date.now();
-const lang = document.querySelector('html').lang;
-
-export default function initialize(state, emitter) {
- appState = state;
- if (!appState.user.firstAction) {
- appState.user.firstAction = appState.route === '/' ? 'upload' : 'download';
- }
- emitter.on('DOMContentLoaded', () => {
- // experiment = storage.enrolled[0];
- const query = appState.query;
- addEvent('client_visit', {
- entrypoint: appState.route === '/' ? 'upload' : 'download',
- referrer: document.referrer,
- utm_campaign: query.utm_campaign,
- utm_content: query.utm_content,
- utm_medium: query.utm_medium,
- utm_source: query.utm_source,
- utm_term: query.utm_term
- });
- });
- emitter.on('experiment', experimentEvent);
- window.addEventListener('unload', submitEvents);
-}
-
-function sizeOrder(n) {
- return Math.floor(Math.log10(n));
-}
-
-function submitEvents() {
- if (navigator.doNotTrack === '1') {
- return;
- }
- sendMetrics(
- new Blob(
- [
- JSON.stringify({
- now: Date.now(),
- session_id,
- lang,
- platform: platform(),
- events
- })
- ],
- { type: 'text/plain' } // see http://crbug.com/490015
- )
- );
- events.splice(0);
-}
-
-async function addEvent(event_type, event_properties) {
- const user_id = await appState.user.metricId();
- const device_id = await appState.user.deviceId();
- events.push({
- device_id,
- event_properties,
- event_type,
- time: Date.now(),
- user_id,
- user_properties: {
- anonymous: !appState.user.loggedIn,
- first_action: appState.user.firstAction,
- active_count: storage.files.length
- }
- });
- if (events.length === 25) {
- submitEvents();
- }
-}
-
-function cancelledUpload(archive, duration) {
- return addEvent('client_upload', {
- download_limit: archive.dlimit,
- duration: sizeOrder(duration),
- file_count: archive.numFiles,
- password_protected: !!archive.password,
- size: sizeOrder(archive.size),
- status: 'cancel',
- time_limit: archive.timeLimit
- });
-}
-
-function completedUpload(archive, duration) {
- return addEvent('client_upload', {
- download_limit: archive.dlimit,
- duration: sizeOrder(duration),
- file_count: archive.numFiles,
- password_protected: !!archive.password,
- size: sizeOrder(archive.size),
- status: 'ok',
- time_limit: archive.timeLimit
- });
-}
-
-function stoppedUpload(archive) {
- return addEvent('client_upload', {
- download_limit: archive.dlimit,
- file_count: archive.numFiles,
- password_protected: !!archive.password,
- size: sizeOrder(archive.size),
- status: 'error',
- time_limit: archive.timeLimit
- });
-}
-
-function stoppedDownload(params) {
- return addEvent('client_download', {
- duration: sizeOrder(params.duration),
- password_protected: params.password_protected,
- size: sizeOrder(params.size),
- status: 'error'
- });
-}
-
-function completedDownload(params) {
- return addEvent('client_download', {
- duration: sizeOrder(params.duration),
- password_protected: params.password_protected,
- size: sizeOrder(params.size),
- status: 'ok'
- });
-}
-
-function deletedUpload(ownedFile) {
- return addEvent('client_delete', {
- age: Math.floor((Date.now() - ownedFile.createdAt) / HOUR),
- downloaded: ownedFile.dtotal > 0,
- status: 'ok'
- });
-}
-
-function experimentEvent(params) {
- return addEvent('client_experiment', params);
-}
-
-function submittedSignup(params) {
- return addEvent('client_login', {
- status: 'ok',
- trigger: params.trigger
- });
-}
-
-function canceledSignup(params) {
- return addEvent('client_login', {
- status: 'cancel',
- trigger: params.trigger
- });
-}
-
-function loggedOut(params) {
- addEvent('client_logout', {
- status: 'ok',
- trigger: params.trigger
- });
- // flush events and start new anon session
- submitEvents();
- session_id = Date.now();
-}
-
-export {
- cancelledUpload,
- stoppedUpload,
- completedUpload,
- deletedUpload,
- stoppedDownload,
- completedDownload,
- submittedSignup,
- canceledSignup,
- loggedOut
-};
diff --git a/app/qrcode.js b/app/qrcode.js
new file mode 100644
index 00000000..5d960df3
--- /dev/null
+++ b/app/qrcode.js
@@ -0,0 +1,2345 @@
+//---------------------------------------------------------------------
+//
+// QR Code Generator for JavaScript
+//
+// Copyright (c) 2009 Kazuhiko Arase
+//
+// URL: http://www.d-project.com/
+//
+// Licensed under the MIT license:
+// http://www.opensource.org/licenses/mit-license.php
+//
+// The word 'QR Code' is registered trademark of
+// DENSO WAVE INCORPORATED
+// http://www.denso-wave.com/qrcode/faqpatent-e.html
+//
+//---------------------------------------------------------------------
+
+var qrcode = (function() {
+ //---------------------------------------------------------------------
+ // qrcode
+ //---------------------------------------------------------------------
+
+ /**
+ * qrcode
+ * @param typeNumber 1 to 40
+ * @param errorCorrectionLevel 'L','M','Q','H'
+ */
+ var qrcode = function(typeNumber, errorCorrectionLevel) {
+ var PAD0 = 0xec;
+ var PAD1 = 0x11;
+
+ var _typeNumber = typeNumber;
+ var _errorCorrectionLevel = QRErrorCorrectionLevel[errorCorrectionLevel];
+ var _modules = null;
+ var _moduleCount = 0;
+ var _dataCache = null;
+ var _dataList = [];
+
+ var _this = {};
+
+ var makeImpl = function(test, maskPattern) {
+ _moduleCount = _typeNumber * 4 + 17;
+ _modules = (function(moduleCount) {
+ var modules = new Array(moduleCount);
+ for (var row = 0; row < moduleCount; row += 1) {
+ modules[row] = new Array(moduleCount);
+ for (var col = 0; col < moduleCount; col += 1) {
+ modules[row][col] = null;
+ }
+ }
+ return modules;
+ })(_moduleCount);
+
+ setupPositionProbePattern(0, 0);
+ setupPositionProbePattern(_moduleCount - 7, 0);
+ setupPositionProbePattern(0, _moduleCount - 7);
+ setupPositionAdjustPattern();
+ setupTimingPattern();
+ setupTypeInfo(test, maskPattern);
+
+ if (_typeNumber >= 7) {
+ setupTypeNumber(test);
+ }
+
+ if (_dataCache == null) {
+ _dataCache = createData(_typeNumber, _errorCorrectionLevel, _dataList);
+ }
+
+ mapData(_dataCache, maskPattern);
+ };
+
+ var setupPositionProbePattern = function(row, col) {
+ for (var r = -1; r <= 7; r += 1) {
+ if (row + r <= -1 || _moduleCount <= row + r) continue;
+
+ for (var c = -1; c <= 7; c += 1) {
+ if (col + c <= -1 || _moduleCount <= col + c) continue;
+
+ if (
+ (0 <= r && r <= 6 && (c == 0 || c == 6)) ||
+ (0 <= c && c <= 6 && (r == 0 || r == 6)) ||
+ (2 <= r && r <= 4 && 2 <= c && c <= 4)
+ ) {
+ _modules[row + r][col + c] = true;
+ } else {
+ _modules[row + r][col + c] = false;
+ }
+ }
+ }
+ };
+
+ var getBestMaskPattern = function() {
+ var minLostPoint = 0;
+ var pattern = 0;
+
+ for (var i = 0; i < 8; i += 1) {
+ makeImpl(true, i);
+
+ var lostPoint = QRUtil.getLostPoint(_this);
+
+ if (i == 0 || minLostPoint > lostPoint) {
+ minLostPoint = lostPoint;
+ pattern = i;
+ }
+ }
+
+ return pattern;
+ };
+
+ var setupTimingPattern = function() {
+ for (var r = 8; r < _moduleCount - 8; r += 1) {
+ if (_modules[r][6] != null) {
+ continue;
+ }
+ _modules[r][6] = r % 2 == 0;
+ }
+
+ for (var c = 8; c < _moduleCount - 8; c += 1) {
+ if (_modules[6][c] != null) {
+ continue;
+ }
+ _modules[6][c] = c % 2 == 0;
+ }
+ };
+
+ var setupPositionAdjustPattern = function() {
+ var pos = QRUtil.getPatternPosition(_typeNumber);
+
+ for (var i = 0; i < pos.length; i += 1) {
+ for (var j = 0; j < pos.length; j += 1) {
+ var row = pos[i];
+ var col = pos[j];
+
+ if (_modules[row][col] != null) {
+ continue;
+ }
+
+ for (var r = -2; r <= 2; r += 1) {
+ for (var c = -2; c <= 2; c += 1) {
+ if (
+ r == -2 ||
+ r == 2 ||
+ c == -2 ||
+ c == 2 ||
+ (r == 0 && c == 0)
+ ) {
+ _modules[row + r][col + c] = true;
+ } else {
+ _modules[row + r][col + c] = false;
+ }
+ }
+ }
+ }
+ }
+ };
+
+ var setupTypeNumber = function(test) {
+ var bits = QRUtil.getBCHTypeNumber(_typeNumber);
+
+ for (var i = 0; i < 18; i += 1) {
+ var mod = !test && ((bits >> i) & 1) == 1;
+ _modules[Math.floor(i / 3)][(i % 3) + _moduleCount - 8 - 3] = mod;
+ }
+
+ for (var i = 0; i < 18; i += 1) {
+ var mod = !test && ((bits >> i) & 1) == 1;
+ _modules[(i % 3) + _moduleCount - 8 - 3][Math.floor(i / 3)] = mod;
+ }
+ };
+
+ var setupTypeInfo = function(test, maskPattern) {
+ var data = (_errorCorrectionLevel << 3) | maskPattern;
+ var bits = QRUtil.getBCHTypeInfo(data);
+
+ // vertical
+ for (var i = 0; i < 15; i += 1) {
+ var mod = !test && ((bits >> i) & 1) == 1;
+
+ if (i < 6) {
+ _modules[i][8] = mod;
+ } else if (i < 8) {
+ _modules[i + 1][8] = mod;
+ } else {
+ _modules[_moduleCount - 15 + i][8] = mod;
+ }
+ }
+
+ // horizontal
+ for (var i = 0; i < 15; i += 1) {
+ var mod = !test && ((bits >> i) & 1) == 1;
+
+ if (i < 8) {
+ _modules[8][_moduleCount - i - 1] = mod;
+ } else if (i < 9) {
+ _modules[8][15 - i - 1 + 1] = mod;
+ } else {
+ _modules[8][15 - i - 1] = mod;
+ }
+ }
+
+ // fixed module
+ _modules[_moduleCount - 8][8] = !test;
+ };
+
+ var mapData = function(data, maskPattern) {
+ var inc = -1;
+ var row = _moduleCount - 1;
+ var bitIndex = 7;
+ var byteIndex = 0;
+ var maskFunc = QRUtil.getMaskFunction(maskPattern);
+
+ for (var col = _moduleCount - 1; col > 0; col -= 2) {
+ if (col == 6) col -= 1;
+
+ while (true) {
+ for (var c = 0; c < 2; c += 1) {
+ if (_modules[row][col - c] == null) {
+ var dark = false;
+
+ if (byteIndex < data.length) {
+ dark = ((data[byteIndex] >>> bitIndex) & 1) == 1;
+ }
+
+ var mask = maskFunc(row, col - c);
+
+ if (mask) {
+ dark = !dark;
+ }
+
+ _modules[row][col - c] = dark;
+ bitIndex -= 1;
+
+ if (bitIndex == -1) {
+ byteIndex += 1;
+ bitIndex = 7;
+ }
+ }
+ }
+
+ row += inc;
+
+ if (row < 0 || _moduleCount <= row) {
+ row -= inc;
+ inc = -inc;
+ break;
+ }
+ }
+ }
+ };
+
+ var createBytes = function(buffer, rsBlocks) {
+ var offset = 0;
+
+ var maxDcCount = 0;
+ var maxEcCount = 0;
+
+ var dcdata = new Array(rsBlocks.length);
+ var ecdata = new Array(rsBlocks.length);
+
+ for (var r = 0; r < rsBlocks.length; r += 1) {
+ var dcCount = rsBlocks[r].dataCount;
+ var ecCount = rsBlocks[r].totalCount - dcCount;
+
+ maxDcCount = Math.max(maxDcCount, dcCount);
+ maxEcCount = Math.max(maxEcCount, ecCount);
+
+ dcdata[r] = new Array(dcCount);
+
+ for (var i = 0; i < dcdata[r].length; i += 1) {
+ dcdata[r][i] = 0xff & buffer.getBuffer()[i + offset];
+ }
+ offset += dcCount;
+
+ var rsPoly = QRUtil.getErrorCorrectPolynomial(ecCount);
+ var rawPoly = qrPolynomial(dcdata[r], rsPoly.getLength() - 1);
+
+ var modPoly = rawPoly.mod(rsPoly);
+ ecdata[r] = new Array(rsPoly.getLength() - 1);
+ for (var i = 0; i < ecdata[r].length; i += 1) {
+ var modIndex = i + modPoly.getLength() - ecdata[r].length;
+ ecdata[r][i] = modIndex >= 0 ? modPoly.getAt(modIndex) : 0;
+ }
+ }
+
+ var totalCodeCount = 0;
+ for (var i = 0; i < rsBlocks.length; i += 1) {
+ totalCodeCount += rsBlocks[i].totalCount;
+ }
+
+ var data = new Array(totalCodeCount);
+ var index = 0;
+
+ for (var i = 0; i < maxDcCount; i += 1) {
+ for (var r = 0; r < rsBlocks.length; r += 1) {
+ if (i < dcdata[r].length) {
+ data[index] = dcdata[r][i];
+ index += 1;
+ }
+ }
+ }
+
+ for (var i = 0; i < maxEcCount; i += 1) {
+ for (var r = 0; r < rsBlocks.length; r += 1) {
+ if (i < ecdata[r].length) {
+ data[index] = ecdata[r][i];
+ index += 1;
+ }
+ }
+ }
+
+ return data;
+ };
+
+ var createData = function(typeNumber, errorCorrectionLevel, dataList) {
+ var rsBlocks = QRRSBlock.getRSBlocks(typeNumber, errorCorrectionLevel);
+
+ var buffer = qrBitBuffer();
+
+ for (var i = 0; i < dataList.length; i += 1) {
+ var data = dataList[i];
+ buffer.put(data.getMode(), 4);
+ buffer.put(
+ data.getLength(),
+ QRUtil.getLengthInBits(data.getMode(), typeNumber)
+ );
+ data.write(buffer);
+ }
+
+ // calc num max data.
+ var totalDataCount = 0;
+ for (var i = 0; i < rsBlocks.length; i += 1) {
+ totalDataCount += rsBlocks[i].dataCount;
+ }
+
+ if (buffer.getLengthInBits() > totalDataCount * 8) {
+ throw 'code length overflow. (' +
+ buffer.getLengthInBits() +
+ '>' +
+ totalDataCount * 8 +
+ ')';
+ }
+
+ // end code
+ if (buffer.getLengthInBits() + 4 <= totalDataCount * 8) {
+ buffer.put(0, 4);
+ }
+
+ // padding
+ while (buffer.getLengthInBits() % 8 != 0) {
+ buffer.putBit(false);
+ }
+
+ // padding
+ while (true) {
+ if (buffer.getLengthInBits() >= totalDataCount * 8) {
+ break;
+ }
+ buffer.put(PAD0, 8);
+
+ if (buffer.getLengthInBits() >= totalDataCount * 8) {
+ break;
+ }
+ buffer.put(PAD1, 8);
+ }
+
+ return createBytes(buffer, rsBlocks);
+ };
+
+ _this.addData = function(data, mode) {
+ mode = mode || 'Byte';
+
+ var newData = null;
+
+ switch (mode) {
+ case 'Numeric':
+ newData = qrNumber(data);
+ break;
+ case 'Alphanumeric':
+ newData = qrAlphaNum(data);
+ break;
+ case 'Byte':
+ newData = qr8BitByte(data);
+ break;
+ case 'Kanji':
+ newData = qrKanji(data);
+ break;
+ default:
+ throw 'mode:' + mode;
+ }
+
+ _dataList.push(newData);
+ _dataCache = null;
+ };
+
+ _this.isDark = function(row, col) {
+ if (row < 0 || _moduleCount <= row || col < 0 || _moduleCount <= col) {
+ throw row + ',' + col;
+ }
+ return _modules[row][col];
+ };
+
+ _this.getModuleCount = function() {
+ return _moduleCount;
+ };
+
+ _this.make = function() {
+ if (_typeNumber < 1) {
+ var typeNumber = 1;
+
+ for (; typeNumber < 40; typeNumber++) {
+ var rsBlocks = QRRSBlock.getRSBlocks(
+ typeNumber,
+ _errorCorrectionLevel
+ );
+ var buffer = qrBitBuffer();
+
+ for (var i = 0; i < _dataList.length; i++) {
+ var data = _dataList[i];
+ buffer.put(data.getMode(), 4);
+ buffer.put(
+ data.getLength(),
+ QRUtil.getLengthInBits(data.getMode(), typeNumber)
+ );
+ data.write(buffer);
+ }
+
+ var totalDataCount = 0;
+ for (var i = 0; i < rsBlocks.length; i++) {
+ totalDataCount += rsBlocks[i].dataCount;
+ }
+
+ if (buffer.getLengthInBits() <= totalDataCount * 8) {
+ break;
+ }
+ }
+
+ _typeNumber = typeNumber;
+ }
+
+ makeImpl(false, getBestMaskPattern());
+ };
+
+ _this.createTableTag = function(cellSize, margin) {
+ cellSize = cellSize || 2;
+ margin = typeof margin == 'undefined' ? cellSize * 4 : margin;
+
+ var qrHtml = '';
+
+ qrHtml += '
';
+ qrHtml += '';
+
+ for (var r = 0; r < _this.getModuleCount(); r += 1) {
+ qrHtml += '';
+
+ for (var c = 0; c < _this.getModuleCount(); c += 1) {
+ qrHtml += ' | ';
+ }
+
+ qrHtml += '
';
+ }
+
+ qrHtml += '';
+ qrHtml += '
';
+
+ return qrHtml;
+ };
+
+ _this.createSvgTag = function(cellSize, margin, alt, title) {
+ var opts = {};
+ if (typeof arguments[0] == 'object') {
+ // Called by options.
+ opts = arguments[0];
+ // overwrite cellSize and margin.
+ cellSize = opts.cellSize;
+ margin = opts.margin;
+ alt = opts.alt;
+ title = opts.title;
+ }
+
+ cellSize = cellSize || 2;
+ margin = typeof margin == 'undefined' ? cellSize * 4 : margin;
+
+ // Compose alt property surrogate
+ alt = typeof alt === 'string' ? { text: alt } : alt || {};
+ alt.text = alt.text || null;
+ alt.id = alt.text ? alt.id || 'qrcode-description' : null;
+
+ // Compose title property surrogate
+ title = typeof title === 'string' ? { text: title } : title || {};
+ title.text = title.text || null;
+ title.id = title.text ? title.id || 'qrcode-title' : null;
+
+ var size = _this.getModuleCount() * cellSize + margin * 2;
+ var c,
+ mc,
+ r,
+ mr,
+ qrSvg = '',
+ rect;
+
+ rect =
+ 'l' +
+ cellSize +
+ ',0 0,' +
+ cellSize +
+ ' -' +
+ cellSize +
+ ',0 0,-' +
+ cellSize +
+ 'z ';
+
+ qrSvg += '
';
+
+ return qrSvg;
+ };
+
+ _this.createDataURL = function(cellSize, margin) {
+ cellSize = cellSize || 2;
+ margin = typeof margin == 'undefined' ? cellSize * 4 : margin;
+
+ var size = _this.getModuleCount() * cellSize + margin * 2;
+ var min = margin;
+ var max = size - margin;
+
+ return createDataURL(size, size, function(x, y) {
+ if (min <= x && x < max && min <= y && y < max) {
+ var c = Math.floor((x - min) / cellSize);
+ var r = Math.floor((y - min) / cellSize);
+ return _this.isDark(r, c) ? 0 : 1;
+ } else {
+ return 1;
+ }
+ });
+ };
+
+ _this.createImgTag = function(cellSize, margin, alt) {
+ cellSize = cellSize || 2;
+ margin = typeof margin == 'undefined' ? cellSize * 4 : margin;
+
+ var size = _this.getModuleCount() * cellSize + margin * 2;
+
+ var img = '';
+ img += '
![]()
';
+
+ return img;
+ };
+
+ var escapeXml = function(s) {
+ var escaped = '';
+ for (var i = 0; i < s.length; i += 1) {
+ var c = s.charAt(i);
+ switch (c) {
+ case '<':
+ escaped += '<';
+ break;
+ case '>':
+ escaped += '>';
+ break;
+ case '&':
+ escaped += '&';
+ break;
+ case '"':
+ escaped += '"';
+ break;
+ default:
+ escaped += c;
+ break;
+ }
+ }
+ return escaped;
+ };
+
+ var _createHalfASCII = function(margin) {
+ var cellSize = 1;
+ margin = typeof margin == 'undefined' ? cellSize * 2 : margin;
+
+ var size = _this.getModuleCount() * cellSize + margin * 2;
+ var min = margin;
+ var max = size - margin;
+
+ var y, x, r1, r2, p;
+
+ var blocks = {
+ '██': '█',
+ '█ ': '▀',
+ ' █': '▄',
+ ' ': ' '
+ };
+
+ var blocksLastLineNoMargin = {
+ '██': '▀',
+ '█ ': '▀',
+ ' █': ' ',
+ ' ': ' '
+ };
+
+ var ascii = '';
+ for (y = 0; y < size; y += 2) {
+ r1 = Math.floor((y - min) / cellSize);
+ r2 = Math.floor((y + 1 - min) / cellSize);
+ for (x = 0; x < size; x += 1) {
+ p = '█';
+
+ if (
+ min <= x &&
+ x < max &&
+ min <= y &&
+ y < max &&
+ _this.isDark(r1, Math.floor((x - min) / cellSize))
+ ) {
+ p = ' ';
+ }
+
+ if (
+ min <= x &&
+ x < max &&
+ min <= y + 1 &&
+ y + 1 < max &&
+ _this.isDark(r2, Math.floor((x - min) / cellSize))
+ ) {
+ p += ' ';
+ } else {
+ p += '█';
+ }
+
+ // Output 2 characters per pixel, to create full square. 1 character per pixels gives only half width of square.
+ ascii +=
+ margin < 1 && y + 1 >= max ? blocksLastLineNoMargin[p] : blocks[p];
+ }
+
+ ascii += '\n';
+ }
+
+ if (size % 2 && margin > 0) {
+ return (
+ ascii.substring(0, ascii.length - size - 1) +
+ Array(size + 1).join('▀')
+ );
+ }
+
+ return ascii.substring(0, ascii.length - 1);
+ };
+
+ _this.createASCII = function(cellSize, margin) {
+ cellSize = cellSize || 1;
+
+ if (cellSize < 2) {
+ return _createHalfASCII(margin);
+ }
+
+ cellSize -= 1;
+ margin = typeof margin == 'undefined' ? cellSize * 2 : margin;
+
+ var size = _this.getModuleCount() * cellSize + margin * 2;
+ var min = margin;
+ var max = size - margin;
+
+ var y, x, r, p;
+
+ var white = Array(cellSize + 1).join('██');
+ var black = Array(cellSize + 1).join(' ');
+
+ var ascii = '';
+ var line = '';
+ for (y = 0; y < size; y += 1) {
+ r = Math.floor((y - min) / cellSize);
+ line = '';
+ for (x = 0; x < size; x += 1) {
+ p = 1;
+
+ if (
+ min <= x &&
+ x < max &&
+ min <= y &&
+ y < max &&
+ _this.isDark(r, Math.floor((x - min) / cellSize))
+ ) {
+ p = 0;
+ }
+
+ // Output 2 characters per pixel, to create full square. 1 character per pixels gives only half width of square.
+ line += p ? white : black;
+ }
+
+ for (r = 0; r < cellSize; r += 1) {
+ ascii += line + '\n';
+ }
+ }
+
+ return ascii.substring(0, ascii.length - 1);
+ };
+
+ _this.renderTo2dContext = function(context, cellSize) {
+ cellSize = cellSize || 2;
+ var length = _this.getModuleCount();
+ for (var row = 0; row < length; row++) {
+ for (var col = 0; col < length; col++) {
+ context.fillStyle = _this.isDark(row, col) ? 'black' : 'white';
+ context.fillRect(row * cellSize, col * cellSize, cellSize, cellSize);
+ }
+ }
+ };
+
+ return _this;
+ };
+
+ //---------------------------------------------------------------------
+ // qrcode.stringToBytes
+ //---------------------------------------------------------------------
+
+ qrcode.stringToBytesFuncs = {
+ default: function(s) {
+ var bytes = [];
+ for (var i = 0; i < s.length; i += 1) {
+ var c = s.charCodeAt(i);
+ bytes.push(c & 0xff);
+ }
+ return bytes;
+ }
+ };
+
+ qrcode.stringToBytes = qrcode.stringToBytesFuncs['default'];
+
+ //---------------------------------------------------------------------
+ // qrcode.createStringToBytes
+ //---------------------------------------------------------------------
+
+ /**
+ * @param unicodeData base64 string of byte array.
+ * [16bit Unicode],[16bit Bytes], ...
+ * @param numChars
+ */
+ qrcode.createStringToBytes = function(unicodeData, numChars) {
+ // create conversion map.
+
+ var unicodeMap = (function() {
+ var bin = base64DecodeInputStream(unicodeData);
+ var read = function() {
+ var b = bin.read();
+ if (b == -1) throw 'eof';
+ return b;
+ };
+
+ var count = 0;
+ var unicodeMap = {};
+ while (true) {
+ var b0 = bin.read();
+ if (b0 == -1) break;
+ var b1 = read();
+ var b2 = read();
+ var b3 = read();
+ var k = String.fromCharCode((b0 << 8) | b1);
+ var v = (b2 << 8) | b3;
+ unicodeMap[k] = v;
+ count += 1;
+ }
+ if (count != numChars) {
+ throw count + ' != ' + numChars;
+ }
+
+ return unicodeMap;
+ })();
+
+ var unknownChar = '?'.charCodeAt(0);
+
+ return function(s) {
+ var bytes = [];
+ for (var i = 0; i < s.length; i += 1) {
+ var c = s.charCodeAt(i);
+ if (c < 128) {
+ bytes.push(c);
+ } else {
+ var b = unicodeMap[s.charAt(i)];
+ if (typeof b == 'number') {
+ if ((b & 0xff) == b) {
+ // 1byte
+ bytes.push(b);
+ } else {
+ // 2bytes
+ bytes.push(b >>> 8);
+ bytes.push(b & 0xff);
+ }
+ } else {
+ bytes.push(unknownChar);
+ }
+ }
+ }
+ return bytes;
+ };
+ };
+
+ //---------------------------------------------------------------------
+ // QRMode
+ //---------------------------------------------------------------------
+
+ var QRMode = {
+ MODE_NUMBER: 1 << 0,
+ MODE_ALPHA_NUM: 1 << 1,
+ MODE_8BIT_BYTE: 1 << 2,
+ MODE_KANJI: 1 << 3
+ };
+
+ //---------------------------------------------------------------------
+ // QRErrorCorrectionLevel
+ //---------------------------------------------------------------------
+
+ var QRErrorCorrectionLevel = {
+ L: 1,
+ M: 0,
+ Q: 3,
+ H: 2
+ };
+
+ //---------------------------------------------------------------------
+ // QRMaskPattern
+ //---------------------------------------------------------------------
+
+ var QRMaskPattern = {
+ PATTERN000: 0,
+ PATTERN001: 1,
+ PATTERN010: 2,
+ PATTERN011: 3,
+ PATTERN100: 4,
+ PATTERN101: 5,
+ PATTERN110: 6,
+ PATTERN111: 7
+ };
+
+ //---------------------------------------------------------------------
+ // QRUtil
+ //---------------------------------------------------------------------
+
+ var QRUtil = (function() {
+ var PATTERN_POSITION_TABLE = [
+ [],
+ [6, 18],
+ [6, 22],
+ [6, 26],
+ [6, 30],
+ [6, 34],
+ [6, 22, 38],
+ [6, 24, 42],
+ [6, 26, 46],
+ [6, 28, 50],
+ [6, 30, 54],
+ [6, 32, 58],
+ [6, 34, 62],
+ [6, 26, 46, 66],
+ [6, 26, 48, 70],
+ [6, 26, 50, 74],
+ [6, 30, 54, 78],
+ [6, 30, 56, 82],
+ [6, 30, 58, 86],
+ [6, 34, 62, 90],
+ [6, 28, 50, 72, 94],
+ [6, 26, 50, 74, 98],
+ [6, 30, 54, 78, 102],
+ [6, 28, 54, 80, 106],
+ [6, 32, 58, 84, 110],
+ [6, 30, 58, 86, 114],
+ [6, 34, 62, 90, 118],
+ [6, 26, 50, 74, 98, 122],
+ [6, 30, 54, 78, 102, 126],
+ [6, 26, 52, 78, 104, 130],
+ [6, 30, 56, 82, 108, 134],
+ [6, 34, 60, 86, 112, 138],
+ [6, 30, 58, 86, 114, 142],
+ [6, 34, 62, 90, 118, 146],
+ [6, 30, 54, 78, 102, 126, 150],
+ [6, 24, 50, 76, 102, 128, 154],
+ [6, 28, 54, 80, 106, 132, 158],
+ [6, 32, 58, 84, 110, 136, 162],
+ [6, 26, 54, 82, 110, 138, 166],
+ [6, 30, 58, 86, 114, 142, 170]
+ ];
+ var G15 =
+ (1 << 10) |
+ (1 << 8) |
+ (1 << 5) |
+ (1 << 4) |
+ (1 << 2) |
+ (1 << 1) |
+ (1 << 0);
+ var G18 =
+ (1 << 12) |
+ (1 << 11) |
+ (1 << 10) |
+ (1 << 9) |
+ (1 << 8) |
+ (1 << 5) |
+ (1 << 2) |
+ (1 << 0);
+ var G15_MASK = (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1);
+
+ var _this = {};
+
+ var getBCHDigit = function(data) {
+ var digit = 0;
+ while (data != 0) {
+ digit += 1;
+ data >>>= 1;
+ }
+ return digit;
+ };
+
+ _this.getBCHTypeInfo = function(data) {
+ var d = data << 10;
+ while (getBCHDigit(d) - getBCHDigit(G15) >= 0) {
+ d ^= G15 << (getBCHDigit(d) - getBCHDigit(G15));
+ }
+ return ((data << 10) | d) ^ G15_MASK;
+ };
+
+ _this.getBCHTypeNumber = function(data) {
+ var d = data << 12;
+ while (getBCHDigit(d) - getBCHDigit(G18) >= 0) {
+ d ^= G18 << (getBCHDigit(d) - getBCHDigit(G18));
+ }
+ return (data << 12) | d;
+ };
+
+ _this.getPatternPosition = function(typeNumber) {
+ return PATTERN_POSITION_TABLE[typeNumber - 1];
+ };
+
+ _this.getMaskFunction = function(maskPattern) {
+ switch (maskPattern) {
+ case QRMaskPattern.PATTERN000:
+ return function(i, j) {
+ return (i + j) % 2 == 0;
+ };
+ case QRMaskPattern.PATTERN001:
+ return function(i, j) {
+ return i % 2 == 0;
+ };
+ case QRMaskPattern.PATTERN010:
+ return function(i, j) {
+ return j % 3 == 0;
+ };
+ case QRMaskPattern.PATTERN011:
+ return function(i, j) {
+ return (i + j) % 3 == 0;
+ };
+ case QRMaskPattern.PATTERN100:
+ return function(i, j) {
+ return (Math.floor(i / 2) + Math.floor(j / 3)) % 2 == 0;
+ };
+ case QRMaskPattern.PATTERN101:
+ return function(i, j) {
+ return ((i * j) % 2) + ((i * j) % 3) == 0;
+ };
+ case QRMaskPattern.PATTERN110:
+ return function(i, j) {
+ return (((i * j) % 2) + ((i * j) % 3)) % 2 == 0;
+ };
+ case QRMaskPattern.PATTERN111:
+ return function(i, j) {
+ return (((i * j) % 3) + ((i + j) % 2)) % 2 == 0;
+ };
+
+ default:
+ throw 'bad maskPattern:' + maskPattern;
+ }
+ };
+
+ _this.getErrorCorrectPolynomial = function(errorCorrectLength) {
+ var a = qrPolynomial([1], 0);
+ for (var i = 0; i < errorCorrectLength; i += 1) {
+ a = a.multiply(qrPolynomial([1, QRMath.gexp(i)], 0));
+ }
+ return a;
+ };
+
+ _this.getLengthInBits = function(mode, type) {
+ if (1 <= type && type < 10) {
+ // 1 - 9
+
+ switch (mode) {
+ case QRMode.MODE_NUMBER:
+ return 10;
+ case QRMode.MODE_ALPHA_NUM:
+ return 9;
+ case QRMode.MODE_8BIT_BYTE:
+ return 8;
+ case QRMode.MODE_KANJI:
+ return 8;
+ default:
+ throw 'mode:' + mode;
+ }
+ } else if (type < 27) {
+ // 10 - 26
+
+ switch (mode) {
+ case QRMode.MODE_NUMBER:
+ return 12;
+ case QRMode.MODE_ALPHA_NUM:
+ return 11;
+ case QRMode.MODE_8BIT_BYTE:
+ return 16;
+ case QRMode.MODE_KANJI:
+ return 10;
+ default:
+ throw 'mode:' + mode;
+ }
+ } else if (type < 41) {
+ // 27 - 40
+
+ switch (mode) {
+ case QRMode.MODE_NUMBER:
+ return 14;
+ case QRMode.MODE_ALPHA_NUM:
+ return 13;
+ case QRMode.MODE_8BIT_BYTE:
+ return 16;
+ case QRMode.MODE_KANJI:
+ return 12;
+ default:
+ throw 'mode:' + mode;
+ }
+ } else {
+ throw 'type:' + type;
+ }
+ };
+
+ _this.getLostPoint = function(qrcode) {
+ var moduleCount = qrcode.getModuleCount();
+
+ var lostPoint = 0;
+
+ // LEVEL1
+
+ for (var row = 0; row < moduleCount; row += 1) {
+ for (var col = 0; col < moduleCount; col += 1) {
+ var sameCount = 0;
+ var dark = qrcode.isDark(row, col);
+
+ for (var r = -1; r <= 1; r += 1) {
+ if (row + r < 0 || moduleCount <= row + r) {
+ continue;
+ }
+
+ for (var c = -1; c <= 1; c += 1) {
+ if (col + c < 0 || moduleCount <= col + c) {
+ continue;
+ }
+
+ if (r == 0 && c == 0) {
+ continue;
+ }
+
+ if (dark == qrcode.isDark(row + r, col + c)) {
+ sameCount += 1;
+ }
+ }
+ }
+
+ if (sameCount > 5) {
+ lostPoint += 3 + sameCount - 5;
+ }
+ }
+ }
+
+ // LEVEL2
+
+ for (var row = 0; row < moduleCount - 1; row += 1) {
+ for (var col = 0; col < moduleCount - 1; col += 1) {
+ var count = 0;
+ if (qrcode.isDark(row, col)) count += 1;
+ if (qrcode.isDark(row + 1, col)) count += 1;
+ if (qrcode.isDark(row, col + 1)) count += 1;
+ if (qrcode.isDark(row + 1, col + 1)) count += 1;
+ if (count == 0 || count == 4) {
+ lostPoint += 3;
+ }
+ }
+ }
+
+ // LEVEL3
+
+ for (var row = 0; row < moduleCount; row += 1) {
+ for (var col = 0; col < moduleCount - 6; col += 1) {
+ if (
+ qrcode.isDark(row, col) &&
+ !qrcode.isDark(row, col + 1) &&
+ qrcode.isDark(row, col + 2) &&
+ qrcode.isDark(row, col + 3) &&
+ qrcode.isDark(row, col + 4) &&
+ !qrcode.isDark(row, col + 5) &&
+ qrcode.isDark(row, col + 6)
+ ) {
+ lostPoint += 40;
+ }
+ }
+ }
+
+ for (var col = 0; col < moduleCount; col += 1) {
+ for (var row = 0; row < moduleCount - 6; row += 1) {
+ if (
+ qrcode.isDark(row, col) &&
+ !qrcode.isDark(row + 1, col) &&
+ qrcode.isDark(row + 2, col) &&
+ qrcode.isDark(row + 3, col) &&
+ qrcode.isDark(row + 4, col) &&
+ !qrcode.isDark(row + 5, col) &&
+ qrcode.isDark(row + 6, col)
+ ) {
+ lostPoint += 40;
+ }
+ }
+ }
+
+ // LEVEL4
+
+ var darkCount = 0;
+
+ for (var col = 0; col < moduleCount; col += 1) {
+ for (var row = 0; row < moduleCount; row += 1) {
+ if (qrcode.isDark(row, col)) {
+ darkCount += 1;
+ }
+ }
+ }
+
+ var ratio =
+ Math.abs((100 * darkCount) / moduleCount / moduleCount - 50) / 5;
+ lostPoint += ratio * 10;
+
+ return lostPoint;
+ };
+
+ return _this;
+ })();
+
+ //---------------------------------------------------------------------
+ // QRMath
+ //---------------------------------------------------------------------
+
+ var QRMath = (function() {
+ var EXP_TABLE = new Array(256);
+ var LOG_TABLE = new Array(256);
+
+ // initialize tables
+ for (var i = 0; i < 8; i += 1) {
+ EXP_TABLE[i] = 1 << i;
+ }
+ for (var i = 8; i < 256; i += 1) {
+ EXP_TABLE[i] =
+ EXP_TABLE[i - 4] ^
+ EXP_TABLE[i - 5] ^
+ EXP_TABLE[i - 6] ^
+ EXP_TABLE[i - 8];
+ }
+ for (var i = 0; i < 255; i += 1) {
+ LOG_TABLE[EXP_TABLE[i]] = i;
+ }
+
+ var _this = {};
+
+ _this.glog = function(n) {
+ if (n < 1) {
+ throw 'glog(' + n + ')';
+ }
+
+ return LOG_TABLE[n];
+ };
+
+ _this.gexp = function(n) {
+ while (n < 0) {
+ n += 255;
+ }
+
+ while (n >= 256) {
+ n -= 255;
+ }
+
+ return EXP_TABLE[n];
+ };
+
+ return _this;
+ })();
+
+ //---------------------------------------------------------------------
+ // qrPolynomial
+ //---------------------------------------------------------------------
+
+ function qrPolynomial(num, shift) {
+ if (typeof num.length == 'undefined') {
+ throw num.length + '/' + shift;
+ }
+
+ var _num = (function() {
+ var offset = 0;
+ while (offset < num.length && num[offset] == 0) {
+ offset += 1;
+ }
+ var _num = new Array(num.length - offset + shift);
+ for (var i = 0; i < num.length - offset; i += 1) {
+ _num[i] = num[i + offset];
+ }
+ return _num;
+ })();
+
+ var _this = {};
+
+ _this.getAt = function(index) {
+ return _num[index];
+ };
+
+ _this.getLength = function() {
+ return _num.length;
+ };
+
+ _this.multiply = function(e) {
+ var num = new Array(_this.getLength() + e.getLength() - 1);
+
+ for (var i = 0; i < _this.getLength(); i += 1) {
+ for (var j = 0; j < e.getLength(); j += 1) {
+ num[i + j] ^= QRMath.gexp(
+ QRMath.glog(_this.getAt(i)) + QRMath.glog(e.getAt(j))
+ );
+ }
+ }
+
+ return qrPolynomial(num, 0);
+ };
+
+ _this.mod = function(e) {
+ if (_this.getLength() - e.getLength() < 0) {
+ return _this;
+ }
+
+ var ratio = QRMath.glog(_this.getAt(0)) - QRMath.glog(e.getAt(0));
+
+ var num = new Array(_this.getLength());
+ for (var i = 0; i < _this.getLength(); i += 1) {
+ num[i] = _this.getAt(i);
+ }
+
+ for (var i = 0; i < e.getLength(); i += 1) {
+ num[i] ^= QRMath.gexp(QRMath.glog(e.getAt(i)) + ratio);
+ }
+
+ // recursive call
+ return qrPolynomial(num, 0).mod(e);
+ };
+
+ return _this;
+ }
+
+ //---------------------------------------------------------------------
+ // QRRSBlock
+ //---------------------------------------------------------------------
+
+ var QRRSBlock = (function() {
+ var RS_BLOCK_TABLE = [
+ // L
+ // M
+ // Q
+ // H
+
+ // 1
+ [1, 26, 19],
+ [1, 26, 16],
+ [1, 26, 13],
+ [1, 26, 9],
+
+ // 2
+ [1, 44, 34],
+ [1, 44, 28],
+ [1, 44, 22],
+ [1, 44, 16],
+
+ // 3
+ [1, 70, 55],
+ [1, 70, 44],
+ [2, 35, 17],
+ [2, 35, 13],
+
+ // 4
+ [1, 100, 80],
+ [2, 50, 32],
+ [2, 50, 24],
+ [4, 25, 9],
+
+ // 5
+ [1, 134, 108],
+ [2, 67, 43],
+ [2, 33, 15, 2, 34, 16],
+ [2, 33, 11, 2, 34, 12],
+
+ // 6
+ [2, 86, 68],
+ [4, 43, 27],
+ [4, 43, 19],
+ [4, 43, 15],
+
+ // 7
+ [2, 98, 78],
+ [4, 49, 31],
+ [2, 32, 14, 4, 33, 15],
+ [4, 39, 13, 1, 40, 14],
+
+ // 8
+ [2, 121, 97],
+ [2, 60, 38, 2, 61, 39],
+ [4, 40, 18, 2, 41, 19],
+ [4, 40, 14, 2, 41, 15],
+
+ // 9
+ [2, 146, 116],
+ [3, 58, 36, 2, 59, 37],
+ [4, 36, 16, 4, 37, 17],
+ [4, 36, 12, 4, 37, 13],
+
+ // 10
+ [2, 86, 68, 2, 87, 69],
+ [4, 69, 43, 1, 70, 44],
+ [6, 43, 19, 2, 44, 20],
+ [6, 43, 15, 2, 44, 16],
+
+ // 11
+ [4, 101, 81],
+ [1, 80, 50, 4, 81, 51],
+ [4, 50, 22, 4, 51, 23],
+ [3, 36, 12, 8, 37, 13],
+
+ // 12
+ [2, 116, 92, 2, 117, 93],
+ [6, 58, 36, 2, 59, 37],
+ [4, 46, 20, 6, 47, 21],
+ [7, 42, 14, 4, 43, 15],
+
+ // 13
+ [4, 133, 107],
+ [8, 59, 37, 1, 60, 38],
+ [8, 44, 20, 4, 45, 21],
+ [12, 33, 11, 4, 34, 12],
+
+ // 14
+ [3, 145, 115, 1, 146, 116],
+ [4, 64, 40, 5, 65, 41],
+ [11, 36, 16, 5, 37, 17],
+ [11, 36, 12, 5, 37, 13],
+
+ // 15
+ [5, 109, 87, 1, 110, 88],
+ [5, 65, 41, 5, 66, 42],
+ [5, 54, 24, 7, 55, 25],
+ [11, 36, 12, 7, 37, 13],
+
+ // 16
+ [5, 122, 98, 1, 123, 99],
+ [7, 73, 45, 3, 74, 46],
+ [15, 43, 19, 2, 44, 20],
+ [3, 45, 15, 13, 46, 16],
+
+ // 17
+ [1, 135, 107, 5, 136, 108],
+ [10, 74, 46, 1, 75, 47],
+ [1, 50, 22, 15, 51, 23],
+ [2, 42, 14, 17, 43, 15],
+
+ // 18
+ [5, 150, 120, 1, 151, 121],
+ [9, 69, 43, 4, 70, 44],
+ [17, 50, 22, 1, 51, 23],
+ [2, 42, 14, 19, 43, 15],
+
+ // 19
+ [3, 141, 113, 4, 142, 114],
+ [3, 70, 44, 11, 71, 45],
+ [17, 47, 21, 4, 48, 22],
+ [9, 39, 13, 16, 40, 14],
+
+ // 20
+ [3, 135, 107, 5, 136, 108],
+ [3, 67, 41, 13, 68, 42],
+ [15, 54, 24, 5, 55, 25],
+ [15, 43, 15, 10, 44, 16],
+
+ // 21
+ [4, 144, 116, 4, 145, 117],
+ [17, 68, 42],
+ [17, 50, 22, 6, 51, 23],
+ [19, 46, 16, 6, 47, 17],
+
+ // 22
+ [2, 139, 111, 7, 140, 112],
+ [17, 74, 46],
+ [7, 54, 24, 16, 55, 25],
+ [34, 37, 13],
+
+ // 23
+ [4, 151, 121, 5, 152, 122],
+ [4, 75, 47, 14, 76, 48],
+ [11, 54, 24, 14, 55, 25],
+ [16, 45, 15, 14, 46, 16],
+
+ // 24
+ [6, 147, 117, 4, 148, 118],
+ [6, 73, 45, 14, 74, 46],
+ [11, 54, 24, 16, 55, 25],
+ [30, 46, 16, 2, 47, 17],
+
+ // 25
+ [8, 132, 106, 4, 133, 107],
+ [8, 75, 47, 13, 76, 48],
+ [7, 54, 24, 22, 55, 25],
+ [22, 45, 15, 13, 46, 16],
+
+ // 26
+ [10, 142, 114, 2, 143, 115],
+ [19, 74, 46, 4, 75, 47],
+ [28, 50, 22, 6, 51, 23],
+ [33, 46, 16, 4, 47, 17],
+
+ // 27
+ [8, 152, 122, 4, 153, 123],
+ [22, 73, 45, 3, 74, 46],
+ [8, 53, 23, 26, 54, 24],
+ [12, 45, 15, 28, 46, 16],
+
+ // 28
+ [3, 147, 117, 10, 148, 118],
+ [3, 73, 45, 23, 74, 46],
+ [4, 54, 24, 31, 55, 25],
+ [11, 45, 15, 31, 46, 16],
+
+ // 29
+ [7, 146, 116, 7, 147, 117],
+ [21, 73, 45, 7, 74, 46],
+ [1, 53, 23, 37, 54, 24],
+ [19, 45, 15, 26, 46, 16],
+
+ // 30
+ [5, 145, 115, 10, 146, 116],
+ [19, 75, 47, 10, 76, 48],
+ [15, 54, 24, 25, 55, 25],
+ [23, 45, 15, 25, 46, 16],
+
+ // 31
+ [13, 145, 115, 3, 146, 116],
+ [2, 74, 46, 29, 75, 47],
+ [42, 54, 24, 1, 55, 25],
+ [23, 45, 15, 28, 46, 16],
+
+ // 32
+ [17, 145, 115],
+ [10, 74, 46, 23, 75, 47],
+ [10, 54, 24, 35, 55, 25],
+ [19, 45, 15, 35, 46, 16],
+
+ // 33
+ [17, 145, 115, 1, 146, 116],
+ [14, 74, 46, 21, 75, 47],
+ [29, 54, 24, 19, 55, 25],
+ [11, 45, 15, 46, 46, 16],
+
+ // 34
+ [13, 145, 115, 6, 146, 116],
+ [14, 74, 46, 23, 75, 47],
+ [44, 54, 24, 7, 55, 25],
+ [59, 46, 16, 1, 47, 17],
+
+ // 35
+ [12, 151, 121, 7, 152, 122],
+ [12, 75, 47, 26, 76, 48],
+ [39, 54, 24, 14, 55, 25],
+ [22, 45, 15, 41, 46, 16],
+
+ // 36
+ [6, 151, 121, 14, 152, 122],
+ [6, 75, 47, 34, 76, 48],
+ [46, 54, 24, 10, 55, 25],
+ [2, 45, 15, 64, 46, 16],
+
+ // 37
+ [17, 152, 122, 4, 153, 123],
+ [29, 74, 46, 14, 75, 47],
+ [49, 54, 24, 10, 55, 25],
+ [24, 45, 15, 46, 46, 16],
+
+ // 38
+ [4, 152, 122, 18, 153, 123],
+ [13, 74, 46, 32, 75, 47],
+ [48, 54, 24, 14, 55, 25],
+ [42, 45, 15, 32, 46, 16],
+
+ // 39
+ [20, 147, 117, 4, 148, 118],
+ [40, 75, 47, 7, 76, 48],
+ [43, 54, 24, 22, 55, 25],
+ [10, 45, 15, 67, 46, 16],
+
+ // 40
+ [19, 148, 118, 6, 149, 119],
+ [18, 75, 47, 31, 76, 48],
+ [34, 54, 24, 34, 55, 25],
+ [20, 45, 15, 61, 46, 16]
+ ];
+
+ var qrRSBlock = function(totalCount, dataCount) {
+ var _this = {};
+ _this.totalCount = totalCount;
+ _this.dataCount = dataCount;
+ return _this;
+ };
+
+ var _this = {};
+
+ var getRsBlockTable = function(typeNumber, errorCorrectionLevel) {
+ switch (errorCorrectionLevel) {
+ case QRErrorCorrectionLevel.L:
+ return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 0];
+ case QRErrorCorrectionLevel.M:
+ return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 1];
+ case QRErrorCorrectionLevel.Q:
+ return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 2];
+ case QRErrorCorrectionLevel.H:
+ return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 3];
+ default:
+ return undefined;
+ }
+ };
+
+ _this.getRSBlocks = function(typeNumber, errorCorrectionLevel) {
+ var rsBlock = getRsBlockTable(typeNumber, errorCorrectionLevel);
+
+ if (typeof rsBlock == 'undefined') {
+ throw 'bad rs block @ typeNumber:' +
+ typeNumber +
+ '/errorCorrectionLevel:' +
+ errorCorrectionLevel;
+ }
+
+ var length = rsBlock.length / 3;
+
+ var list = [];
+
+ for (var i = 0; i < length; i += 1) {
+ var count = rsBlock[i * 3 + 0];
+ var totalCount = rsBlock[i * 3 + 1];
+ var dataCount = rsBlock[i * 3 + 2];
+
+ for (var j = 0; j < count; j += 1) {
+ list.push(qrRSBlock(totalCount, dataCount));
+ }
+ }
+
+ return list;
+ };
+
+ return _this;
+ })();
+
+ //---------------------------------------------------------------------
+ // qrBitBuffer
+ //---------------------------------------------------------------------
+
+ var qrBitBuffer = function() {
+ var _buffer = [];
+ var _length = 0;
+
+ var _this = {};
+
+ _this.getBuffer = function() {
+ return _buffer;
+ };
+
+ _this.getAt = function(index) {
+ var bufIndex = Math.floor(index / 8);
+ return ((_buffer[bufIndex] >>> (7 - (index % 8))) & 1) == 1;
+ };
+
+ _this.put = function(num, length) {
+ for (var i = 0; i < length; i += 1) {
+ _this.putBit(((num >>> (length - i - 1)) & 1) == 1);
+ }
+ };
+
+ _this.getLengthInBits = function() {
+ return _length;
+ };
+
+ _this.putBit = function(bit) {
+ var bufIndex = Math.floor(_length / 8);
+ if (_buffer.length <= bufIndex) {
+ _buffer.push(0);
+ }
+
+ if (bit) {
+ _buffer[bufIndex] |= 0x80 >>> _length % 8;
+ }
+
+ _length += 1;
+ };
+
+ return _this;
+ };
+
+ //---------------------------------------------------------------------
+ // qrNumber
+ //---------------------------------------------------------------------
+
+ var qrNumber = function(data) {
+ var _mode = QRMode.MODE_NUMBER;
+ var _data = data;
+
+ var _this = {};
+
+ _this.getMode = function() {
+ return _mode;
+ };
+
+ _this.getLength = function(buffer) {
+ return _data.length;
+ };
+
+ _this.write = function(buffer) {
+ var data = _data;
+
+ var i = 0;
+
+ while (i + 2 < data.length) {
+ buffer.put(strToNum(data.substring(i, i + 3)), 10);
+ i += 3;
+ }
+
+ if (i < data.length) {
+ if (data.length - i == 1) {
+ buffer.put(strToNum(data.substring(i, i + 1)), 4);
+ } else if (data.length - i == 2) {
+ buffer.put(strToNum(data.substring(i, i + 2)), 7);
+ }
+ }
+ };
+
+ var strToNum = function(s) {
+ var num = 0;
+ for (var i = 0; i < s.length; i += 1) {
+ num = num * 10 + chatToNum(s.charAt(i));
+ }
+ return num;
+ };
+
+ var chatToNum = function(c) {
+ if ('0' <= c && c <= '9') {
+ return c.charCodeAt(0) - '0'.charCodeAt(0);
+ }
+ throw 'illegal char :' + c;
+ };
+
+ return _this;
+ };
+
+ //---------------------------------------------------------------------
+ // qrAlphaNum
+ //---------------------------------------------------------------------
+
+ var qrAlphaNum = function(data) {
+ var _mode = QRMode.MODE_ALPHA_NUM;
+ var _data = data;
+
+ var _this = {};
+
+ _this.getMode = function() {
+ return _mode;
+ };
+
+ _this.getLength = function(buffer) {
+ return _data.length;
+ };
+
+ _this.write = function(buffer) {
+ var s = _data;
+
+ var i = 0;
+
+ while (i + 1 < s.length) {
+ buffer.put(getCode(s.charAt(i)) * 45 + getCode(s.charAt(i + 1)), 11);
+ i += 2;
+ }
+
+ if (i < s.length) {
+ buffer.put(getCode(s.charAt(i)), 6);
+ }
+ };
+
+ var getCode = function(c) {
+ if ('0' <= c && c <= '9') {
+ return c.charCodeAt(0) - '0'.charCodeAt(0);
+ } else if ('A' <= c && c <= 'Z') {
+ return c.charCodeAt(0) - 'A'.charCodeAt(0) + 10;
+ } else {
+ switch (c) {
+ case ' ':
+ return 36;
+ case '$':
+ return 37;
+ case '%':
+ return 38;
+ case '*':
+ return 39;
+ case '+':
+ return 40;
+ case '-':
+ return 41;
+ case '.':
+ return 42;
+ case '/':
+ return 43;
+ case ':':
+ return 44;
+ default:
+ throw 'illegal char :' + c;
+ }
+ }
+ };
+
+ return _this;
+ };
+
+ //---------------------------------------------------------------------
+ // qr8BitByte
+ //---------------------------------------------------------------------
+
+ var qr8BitByte = function(data) {
+ var _mode = QRMode.MODE_8BIT_BYTE;
+ var _data = data;
+ var _bytes = qrcode.stringToBytes(data);
+
+ var _this = {};
+
+ _this.getMode = function() {
+ return _mode;
+ };
+
+ _this.getLength = function(buffer) {
+ return _bytes.length;
+ };
+
+ _this.write = function(buffer) {
+ for (var i = 0; i < _bytes.length; i += 1) {
+ buffer.put(_bytes[i], 8);
+ }
+ };
+
+ return _this;
+ };
+
+ //---------------------------------------------------------------------
+ // qrKanji
+ //---------------------------------------------------------------------
+
+ var qrKanji = function(data) {
+ var _mode = QRMode.MODE_KANJI;
+ var _data = data;
+
+ var stringToBytes = qrcode.stringToBytesFuncs['SJIS'];
+ if (!stringToBytes) {
+ throw 'sjis not supported.';
+ }
+ !(function(c, code) {
+ // self test for sjis support.
+ var test = stringToBytes(c);
+ if (test.length != 2 || ((test[0] << 8) | test[1]) != code) {
+ throw 'sjis not supported.';
+ }
+ })('\u53cb', 0x9746);
+
+ var _bytes = stringToBytes(data);
+
+ var _this = {};
+
+ _this.getMode = function() {
+ return _mode;
+ };
+
+ _this.getLength = function(buffer) {
+ return ~~(_bytes.length / 2);
+ };
+
+ _this.write = function(buffer) {
+ var data = _bytes;
+
+ var i = 0;
+
+ while (i + 1 < data.length) {
+ var c = ((0xff & data[i]) << 8) | (0xff & data[i + 1]);
+
+ if (0x8140 <= c && c <= 0x9ffc) {
+ c -= 0x8140;
+ } else if (0xe040 <= c && c <= 0xebbf) {
+ c -= 0xc140;
+ } else {
+ throw 'illegal char at ' + (i + 1) + '/' + c;
+ }
+
+ c = ((c >>> 8) & 0xff) * 0xc0 + (c & 0xff);
+
+ buffer.put(c, 13);
+
+ i += 2;
+ }
+
+ if (i < data.length) {
+ throw 'illegal char at ' + (i + 1);
+ }
+ };
+
+ return _this;
+ };
+
+ //=====================================================================
+ // GIF Support etc.
+ //
+
+ //---------------------------------------------------------------------
+ // byteArrayOutputStream
+ //---------------------------------------------------------------------
+
+ var byteArrayOutputStream = function() {
+ var _bytes = [];
+
+ var _this = {};
+
+ _this.writeByte = function(b) {
+ _bytes.push(b & 0xff);
+ };
+
+ _this.writeShort = function(i) {
+ _this.writeByte(i);
+ _this.writeByte(i >>> 8);
+ };
+
+ _this.writeBytes = function(b, off, len) {
+ off = off || 0;
+ len = len || b.length;
+ for (var i = 0; i < len; i += 1) {
+ _this.writeByte(b[i + off]);
+ }
+ };
+
+ _this.writeString = function(s) {
+ for (var i = 0; i < s.length; i += 1) {
+ _this.writeByte(s.charCodeAt(i));
+ }
+ };
+
+ _this.toByteArray = function() {
+ return _bytes;
+ };
+
+ _this.toString = function() {
+ var s = '';
+ s += '[';
+ for (var i = 0; i < _bytes.length; i += 1) {
+ if (i > 0) {
+ s += ',';
+ }
+ s += _bytes[i];
+ }
+ s += ']';
+ return s;
+ };
+
+ return _this;
+ };
+
+ //---------------------------------------------------------------------
+ // base64EncodeOutputStream
+ //---------------------------------------------------------------------
+
+ var base64EncodeOutputStream = function() {
+ var _buffer = 0;
+ var _buflen = 0;
+ var _length = 0;
+ var _base64 = '';
+
+ var _this = {};
+
+ var writeEncoded = function(b) {
+ _base64 += String.fromCharCode(encode(b & 0x3f));
+ };
+
+ var encode = function(n) {
+ if (n < 0) {
+ // error.
+ } else if (n < 26) {
+ return 0x41 + n;
+ } else if (n < 52) {
+ return 0x61 + (n - 26);
+ } else if (n < 62) {
+ return 0x30 + (n - 52);
+ } else if (n == 62) {
+ return 0x2b;
+ } else if (n == 63) {
+ return 0x2f;
+ }
+ throw 'n:' + n;
+ };
+
+ _this.writeByte = function(n) {
+ _buffer = (_buffer << 8) | (n & 0xff);
+ _buflen += 8;
+ _length += 1;
+
+ while (_buflen >= 6) {
+ writeEncoded(_buffer >>> (_buflen - 6));
+ _buflen -= 6;
+ }
+ };
+
+ _this.flush = function() {
+ if (_buflen > 0) {
+ writeEncoded(_buffer << (6 - _buflen));
+ _buffer = 0;
+ _buflen = 0;
+ }
+
+ if (_length % 3 != 0) {
+ // padding
+ var padlen = 3 - (_length % 3);
+ for (var i = 0; i < padlen; i += 1) {
+ _base64 += '=';
+ }
+ }
+ };
+
+ _this.toString = function() {
+ return _base64;
+ };
+
+ return _this;
+ };
+
+ //---------------------------------------------------------------------
+ // base64DecodeInputStream
+ //---------------------------------------------------------------------
+
+ var base64DecodeInputStream = function(str) {
+ var _str = str;
+ var _pos = 0;
+ var _buffer = 0;
+ var _buflen = 0;
+
+ var _this = {};
+
+ _this.read = function() {
+ while (_buflen < 8) {
+ if (_pos >= _str.length) {
+ if (_buflen == 0) {
+ return -1;
+ }
+ throw 'unexpected end of file./' + _buflen;
+ }
+
+ var c = _str.charAt(_pos);
+ _pos += 1;
+
+ if (c == '=') {
+ _buflen = 0;
+ return -1;
+ } else if (c.match(/^\s$/)) {
+ // ignore if whitespace.
+ continue;
+ }
+
+ _buffer = (_buffer << 6) | decode(c.charCodeAt(0));
+ _buflen += 6;
+ }
+
+ var n = (_buffer >>> (_buflen - 8)) & 0xff;
+ _buflen -= 8;
+ return n;
+ };
+
+ var decode = function(c) {
+ if (0x41 <= c && c <= 0x5a) {
+ return c - 0x41;
+ } else if (0x61 <= c && c <= 0x7a) {
+ return c - 0x61 + 26;
+ } else if (0x30 <= c && c <= 0x39) {
+ return c - 0x30 + 52;
+ } else if (c == 0x2b) {
+ return 62;
+ } else if (c == 0x2f) {
+ return 63;
+ } else {
+ throw 'c:' + c;
+ }
+ };
+
+ return _this;
+ };
+
+ //---------------------------------------------------------------------
+ // gifImage (B/W)
+ //---------------------------------------------------------------------
+
+ var gifImage = function(width, height) {
+ var _width = width;
+ var _height = height;
+ var _data = new Array(width * height);
+
+ var _this = {};
+
+ _this.setPixel = function(x, y, pixel) {
+ _data[y * _width + x] = pixel;
+ };
+
+ _this.write = function(out) {
+ //---------------------------------
+ // GIF Signature
+
+ out.writeString('GIF87a');
+
+ //---------------------------------
+ // Screen Descriptor
+
+ out.writeShort(_width);
+ out.writeShort(_height);
+
+ out.writeByte(0x80); // 2bit
+ out.writeByte(0);
+ out.writeByte(0);
+
+ //---------------------------------
+ // Global Color Map
+
+ // black
+ out.writeByte(0x00);
+ out.writeByte(0x00);
+ out.writeByte(0x00);
+
+ // white
+ out.writeByte(0xff);
+ out.writeByte(0xff);
+ out.writeByte(0xff);
+
+ //---------------------------------
+ // Image Descriptor
+
+ out.writeString(',');
+ out.writeShort(0);
+ out.writeShort(0);
+ out.writeShort(_width);
+ out.writeShort(_height);
+ out.writeByte(0);
+
+ //---------------------------------
+ // Local Color Map
+
+ //---------------------------------
+ // Raster Data
+
+ var lzwMinCodeSize = 2;
+ var raster = getLZWRaster(lzwMinCodeSize);
+
+ out.writeByte(lzwMinCodeSize);
+
+ var offset = 0;
+
+ while (raster.length - offset > 255) {
+ out.writeByte(255);
+ out.writeBytes(raster, offset, 255);
+ offset += 255;
+ }
+
+ out.writeByte(raster.length - offset);
+ out.writeBytes(raster, offset, raster.length - offset);
+ out.writeByte(0x00);
+
+ //---------------------------------
+ // GIF Terminator
+ out.writeString(';');
+ };
+
+ var bitOutputStream = function(out) {
+ var _out = out;
+ var _bitLength = 0;
+ var _bitBuffer = 0;
+
+ var _this = {};
+
+ _this.write = function(data, length) {
+ if (data >>> length != 0) {
+ throw 'length over';
+ }
+
+ while (_bitLength + length >= 8) {
+ _out.writeByte(0xff & ((data << _bitLength) | _bitBuffer));
+ length -= 8 - _bitLength;
+ data >>>= 8 - _bitLength;
+ _bitBuffer = 0;
+ _bitLength = 0;
+ }
+
+ _bitBuffer = (data << _bitLength) | _bitBuffer;
+ _bitLength = _bitLength + length;
+ };
+
+ _this.flush = function() {
+ if (_bitLength > 0) {
+ _out.writeByte(_bitBuffer);
+ }
+ };
+
+ return _this;
+ };
+
+ var getLZWRaster = function(lzwMinCodeSize) {
+ var clearCode = 1 << lzwMinCodeSize;
+ var endCode = (1 << lzwMinCodeSize) + 1;
+ var bitLength = lzwMinCodeSize + 1;
+
+ // Setup LZWTable
+ var table = lzwTable();
+
+ for (var i = 0; i < clearCode; i += 1) {
+ table.add(String.fromCharCode(i));
+ }
+ table.add(String.fromCharCode(clearCode));
+ table.add(String.fromCharCode(endCode));
+
+ var byteOut = byteArrayOutputStream();
+ var bitOut = bitOutputStream(byteOut);
+
+ // clear code
+ bitOut.write(clearCode, bitLength);
+
+ var dataIndex = 0;
+
+ var s = String.fromCharCode(_data[dataIndex]);
+ dataIndex += 1;
+
+ while (dataIndex < _data.length) {
+ var c = String.fromCharCode(_data[dataIndex]);
+ dataIndex += 1;
+
+ if (table.contains(s + c)) {
+ s = s + c;
+ } else {
+ bitOut.write(table.indexOf(s), bitLength);
+
+ if (table.size() < 0xfff) {
+ if (table.size() == 1 << bitLength) {
+ bitLength += 1;
+ }
+
+ table.add(s + c);
+ }
+
+ s = c;
+ }
+ }
+
+ bitOut.write(table.indexOf(s), bitLength);
+
+ // end code
+ bitOut.write(endCode, bitLength);
+
+ bitOut.flush();
+
+ return byteOut.toByteArray();
+ };
+
+ var lzwTable = function() {
+ var _map = {};
+ var _size = 0;
+
+ var _this = {};
+
+ _this.add = function(key) {
+ if (_this.contains(key)) {
+ throw 'dup key:' + key;
+ }
+ _map[key] = _size;
+ _size += 1;
+ };
+
+ _this.size = function() {
+ return _size;
+ };
+
+ _this.indexOf = function(key) {
+ return _map[key];
+ };
+
+ _this.contains = function(key) {
+ return typeof _map[key] != 'undefined';
+ };
+
+ return _this;
+ };
+
+ return _this;
+ };
+
+ var createDataURL = function(width, height, getPixel) {
+ var gif = gifImage(width, height);
+ for (var y = 0; y < height; y += 1) {
+ for (var x = 0; x < width; x += 1) {
+ gif.setPixel(x, y, getPixel(x, y));
+ }
+ }
+
+ var b = byteArrayOutputStream();
+ gif.write(b);
+
+ var base64 = base64EncodeOutputStream();
+ var bytes = b.toByteArray();
+ for (var i = 0; i < bytes.length; i += 1) {
+ base64.writeByte(bytes[i]);
+ }
+ base64.flush();
+
+ return 'data:image/gif;base64,' + base64;
+ };
+
+ //---------------------------------------------------------------------
+ // returns qrcode function.
+
+ return qrcode;
+})();
+
+// multibyte support
+!(function() {
+ qrcode.stringToBytesFuncs['UTF-8'] = function(s) {
+ // http://stackoverflow.com/questions/18729405/how-to-convert-utf8-string-to-byte-array
+ function toUTF8Array(str) {
+ var utf8 = [];
+ for (var i = 0; i < str.length; i++) {
+ var charcode = str.charCodeAt(i);
+ if (charcode < 0x80) utf8.push(charcode);
+ else if (charcode < 0x800) {
+ utf8.push(0xc0 | (charcode >> 6), 0x80 | (charcode & 0x3f));
+ } else if (charcode < 0xd800 || charcode >= 0xe000) {
+ utf8.push(
+ 0xe0 | (charcode >> 12),
+ 0x80 | ((charcode >> 6) & 0x3f),
+ 0x80 | (charcode & 0x3f)
+ );
+ }
+ // surrogate pair
+ else {
+ i++;
+ // UTF-16 encodes 0x10000-0x10FFFF by
+ // subtracting 0x10000 and splitting the
+ // 20 bits of 0x0-0xFFFFF into two halves
+ charcode =
+ 0x10000 +
+ (((charcode & 0x3ff) << 10) | (str.charCodeAt(i) & 0x3ff));
+ utf8.push(
+ 0xf0 | (charcode >> 18),
+ 0x80 | ((charcode >> 12) & 0x3f),
+ 0x80 | ((charcode >> 6) & 0x3f),
+ 0x80 | (charcode & 0x3f)
+ );
+ }
+ }
+ return utf8;
+ }
+ return toUTF8Array(s);
+ };
+})();
+
+(function(factory) {
+ if (typeof define === 'function' && define.amd) {
+ define([], factory);
+ } else if (typeof exports === 'object') {
+ module.exports = factory();
+ }
+})(function() {
+ return qrcode;
+});
diff --git a/app/readme.md b/app/readme.md
index 7708988a..b80e5246 100644
--- a/app/readme.md
+++ b/app/readme.md
@@ -2,7 +2,7 @@
`app/` contains the browser code that gets bundled into `app.[hash].js`. It's got all the logic, crypto, and UI. All of it gets used in the browser, and some of it by the server for server side rendering.
-The main entrypoint for the browser is [main.js](./main.js) and on the server [routes/index.js](./routes/index.js) gets imported by [/server/routes/pages.js](../server/routes/pages.js)
+The main entrypoint for the browser is [main.js](./main.js) and on the server [routes.js](./routes.js) is imported by [/server/routes/pages.js](../server/routes/pages.js)
- `pages` contains display logic an markup for pages
- `routes` contains route definitions and logic
diff --git a/app/routes.js b/app/routes.js
index 54a199a2..6a259710 100644
--- a/app/routes.js
+++ b/app/routes.js
@@ -2,17 +2,20 @@ const choo = require('choo');
const download = require('./ui/download');
const body = require('./ui/body');
-module.exports = function(app = choo()) {
+module.exports = function(app = choo({ hash: true })) {
app.route('/', body(require('./ui/home')));
app.route('/download/:id', body(download));
app.route('/download/:id/:key', body(download));
app.route('/unsupported/:reason', body(require('./ui/unsupported')));
- app.route('/legal', body(require('./ui/legal')));
app.route('/error', body(require('./ui/error')));
app.route('/blank', body(require('./ui/blank')));
app.route('/oauth', function(state, emit) {
emit('authenticate', state.query.code, state.query.state);
});
+ app.route('/login', function(state, emit) {
+ emit('replaceState', '/');
+ setTimeout(() => emit('render'));
+ });
app.route('*', body(require('./ui/notFound')));
return app;
};
diff --git a/app/serviceWorker.js b/app/serviceWorker.js
index cc709bab..34ae25b2 100644
--- a/app/serviceWorker.js
+++ b/app/serviceWorker.js
@@ -9,15 +9,16 @@ import contentDisposition from 'content-disposition';
let noSave = false;
const map = new Map();
const IMAGES = /.*\.(png|svg|jpg)$/;
-const VERSIONED_ASSET = /\.[A-Fa-f0-9]{8}\.(js|css|png|svg|jpg)$/;
+const VERSIONED_ASSET = /\.[A-Fa-f0-9]{8}\.(js|css|png|svg|jpg)(#\w+)?$/;
const DOWNLOAD_URL = /\/api\/download\/([A-Fa-f0-9]{4,})/;
+const FONT = /\.woff2?$/;
-self.addEventListener('install', event => {
- event.waitUntil(precache());
+self.addEventListener('install', () => {
+ self.skipWaiting();
});
self.addEventListener('activate', event => {
- event.waitUntil(self.clients.claim());
+ event.waitUntil(self.clients.claim().then(precache));
});
async function decryptStream(id) {
@@ -83,16 +84,28 @@ async function decryptStream(id) {
}
async function precache() {
+ try {
+ await cleanCache();
+ const cache = await caches.open(version);
+ const images = assets.match(IMAGES);
+ await cache.addAll(images);
+ } catch (e) {
+ console.error(e);
+ // cache will get populated on demand
+ }
+}
+
+async function cleanCache() {
const oldCaches = await caches.keys();
for (const c of oldCaches) {
if (c !== version) {
await caches.delete(c);
}
}
- const cache = await caches.open(version);
- const images = assets.match(IMAGES);
- await cache.addAll(images);
- return self.skipWaiting();
+}
+
+function cacheable(url) {
+ return VERSIONED_ASSET.test(url) || FONT.test(url);
}
async function cachedOrFetched(req) {
@@ -102,7 +115,7 @@ async function cachedOrFetched(req) {
return cached;
}
const fetched = await fetch(req);
- if (fetched.ok && VERSIONED_ASSET.test(req.url)) {
+ if (fetched.ok && cacheable(req.url)) {
cache.put(req, fetched.clone());
}
return fetched;
@@ -115,7 +128,7 @@ self.onfetch = event => {
const dlmatch = DOWNLOAD_URL.exec(url.pathname);
if (dlmatch) {
event.respondWith(decryptStream(dlmatch[1]));
- } else if (VERSIONED_ASSET.test(url.pathname)) {
+ } else if (cacheable(url.pathname)) {
event.respondWith(cachedOrFetched(req));
}
};
diff --git a/app/storage.js b/app/storage.js
index d66391e0..304759ea 100644
--- a/app/storage.js
+++ b/app/storage.js
@@ -86,16 +86,13 @@ class Storage {
this.engine.setItem('referrer', str);
}
get enrolled() {
- return JSON.parse(this.engine.getItem('experiments') || '[]');
+ return JSON.parse(this.engine.getItem('ab_experiments') || '{}');
}
enroll(id, variant) {
- const enrolled = this.enrolled;
- // eslint-disable-next-line no-unused-vars
- if (!enrolled.find(([i, v]) => i === id)) {
- enrolled.push([id, variant]);
- this.engine.setItem('experiments', JSON.stringify(enrolled));
- }
+ const enrolled = {};
+ enrolled[id] = variant;
+ this.engine.setItem('ab_experiments', JSON.stringify(enrolled));
}
get files() {
diff --git a/app/streams.js b/app/streams.js
index 73d15097..00159a24 100644
--- a/app/streams.js
+++ b/app/streams.js
@@ -1,4 +1,4 @@
-/* global ReadableStream TransformStream */
+/* global TransformStream */
export function transformStream(readable, transformer, oncancel) {
try {
diff --git a/app/ui/account.js b/app/ui/account.js
index d4ec57fb..9845a3a1 100644
--- a/app/ui/account.js
+++ b/app/ui/account.js
@@ -8,7 +8,8 @@ class Account extends Component {
this.emit = emit;
this.enabled = state.capabilities.account;
this.local = state.components[name] = {};
- this.setState();
+ this.buttonClass = '';
+ this.setLocal();
}
avatarClick(event) {
@@ -38,7 +39,7 @@ class Account extends Component {
return this.local.loggedIn !== this.state.user.loggedIn;
}
- setState() {
+ setLocal() {
const changed = this.changed();
if (changed) {
this.local.loggedIn = this.state.user.loggedIn;
@@ -47,26 +48,32 @@ class Account extends Component {
}
update() {
- return this.setState();
+ return this.setLocal();
}
createElement() {
if (!this.enabled) {
return html`
-
+
`;
}
const user = this.state.user;
const translate = this.state.translate;
+ this.setLocal();
+ if (user.loginRequired && !this.local.loggedIn) {
+ return html`
+
+ `;
+ }
if (!this.local.loggedIn) {
return html`
`;
@@ -76,19 +83,19 @@ class Account extends Component {
- - ${user.email}
+ - ${user.email}
-