Compare commits

..

1 Commits

Author SHA1 Message Date
Jake Archibald
008bae62da Splitting PointerTracker into its own project 2018-11-06 14:18:53 +00:00
50 changed files with 2228 additions and 6397 deletions

13
.babelrc Normal file
View File

@@ -0,0 +1,13 @@
{
"plugins": [
"transform-class-properties",
"transform-react-constant-elements",
"transform-react-remove-prop-types",
[
"transform-react-jsx",
{
"pragma": "h"
}
]
]
}

View File

@@ -1,36 +0,0 @@
---
name: Bug report
about: Something is not working as expected
labels:
---
**Before you start**
Please take a look at the [FAQ](https://github.com/GoogleChromeLabs/squoosh/wiki/FAQ) as well as the already opened issues! If nothing fits your problem, go ahead and fill out the following template:
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Version:**
- OS w/ version: [e.g. iOS 12]
- Browser w/ version [e.g. Chrome 70]
- Node version: [e.g. 10.11.0]
- npm version: [e.g. 6.4.1]
**Is your issue related to the quality of image compression?**
Please attach original and output images (you can drag & drop to attach).
- Original image
- Output image from Squoosh
**Additional context, screenshots, screencasts**
Add any other context about the problem here.

View File

@@ -1,34 +1,5 @@
# [Squoosh]! # Squoosh!
[Squoosh] is an image compression web app that allows you to dive into the advanced options provided Squoosh will be an image compression web app that allows you to dive into the
by various image compressors. advanced options provided by various image compressors.
# Privacy
Google Analytics is used to record the following:
* [Basic visit data](https://support.google.com/analytics/answer/6004245?ref_topic=2919631).
* Before and after image size once an image is downloaded. These values are rounded to the nearest
kilobyte.
Image compression is handled locally; no additional data is sent to the server.
# Building locally
Clone the repo, and:
```sh
npm install
npm run build
```
You'll get an error on first build because of [a stupid bug we haven't fixed
yet](https://github.com/GoogleChromeLabs/squoosh/issues/251).
You can run the development server with:
```sh
npm start
```
[Squoosh]: https://squoosh.app

View File

@@ -1,18 +0,0 @@
# Long-term cache by default.
/*
Cache-Control: max-age=31536000
# And here are the exceptions:
/
Cache-Control: no-cache
/serviceworker.js
Cache-Control: no-cache
/manifest.json
Cache-Control: must-revalidate, max-age=3600
# URLs in /assets do not include a hash and are mutable.
# But it isn't a big deal if the user gets an old version.
/assets/*
Cache-Control: must-revalidate, max-age=3600

View File

@@ -1,47 +0,0 @@
const path = require('path');
const fs = require('fs');
const ejs = require('ejs');
const AssetsPlugin = require('assets-webpack-plugin');
module.exports = class AssetTemplatePlugin extends AssetsPlugin {
constructor(options) {
options = options || {};
if (!options.template) throw Error('AssetTemplatePlugin: template option is required.');
super({
useCompilerPath: true,
filename: options.filename,
processOutput: files => this._processOutput(files)
});
this._template = path.resolve(process.cwd(), options.template);
const ignore = options.ignore || /(manifest\.json|\.DS_Store)$/;
this._ignore = typeof ignore === 'function' ? ({ test: ignore }) : ignore;
}
_processOutput(files) {
const mapping = {
all: [],
byType: {},
entries: {}
};
for (const entryName in files) {
// non-entry-point-derived assets are collected under an empty string key
// since that's a bit awkward, we'll call them "assets"
const name = entryName === '' ? 'assets' : entryName;
const listing = files[entryName];
const entry = mapping.entries[name] = {
all: [],
byType: {}
};
for (let type in listing) {
const list = [].concat(listing[type]).filter(file => !this._ignore.test(file));
if (!list.length) continue;
mapping.all = mapping.all.concat(list);
mapping.byType[type] = (mapping.byType[type] || []).concat(list);
entry.all = entry.all.concat(list);
entry.byType[type] = (entry.byType[type] || []).concat(list);
}
}
mapping.files = mapping.all;
return ejs.render(fs.readFileSync(this._template, 'utf8'), mapping);
}
};

View File

@@ -1,158 +0,0 @@
const util = require('util');
const minimatch = require('minimatch');
const SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin');
const WebWorkerTemplatePlugin = require('webpack/lib/webworker/WebWorkerTemplatePlugin');
const ParserHelpers = require('webpack/lib/ParserHelpers');
const NAME = 'auto-sw-plugin';
const JS_TYPES = ['auto', 'esm', 'dynamic'];
/**
* Automatically finds and bundles Service Workers by looking for navigator.serviceWorker.register(..).
* An Array of webpack assets is injected into the Service Worker bundle as a `BUILD_ASSETS` global.
* Hidden and `.map` files are excluded by default, and this can be customized using the include & exclude options.
* @example
* // webpack config
* plugins: [
* new AutoSWPlugin({
* exclude: [
* '**\/.*', // don't expose hidden files (default)
* '**\/*.map', // don't precache sourcemaps (default)
* 'index.html' // don't cache the page itself
* ]
* })
* ]
* @param {Object} [options={}]
* @param {string[]} [options.exclude] Minimatch pattern(s) of which assets to omit from BUILD_ASSETS.
* @param {string[]} [options.include] Minimatch pattern(s) of assets to allow in BUILD_ASSETS.
*/
module.exports = class AutoSWPlugin {
constructor(options) {
this.options = Object.assign({
exclude: [
'**/*.map',
'**/.*'
]
}, options || {});
}
apply(compiler) {
const serviceWorkers = [];
compiler.hooks.emit.tapPromise(NAME, compilation => this.emit(compiler, compilation, serviceWorkers));
compiler.hooks.normalModuleFactory.tap(NAME, (factory) => {
for (const type of JS_TYPES) {
factory.hooks.parser.for(`javascript/${type}`).tap(NAME, parser => {
let counter = 0;
const processRegisterCall = expr => {
const dep = parser.evaluateExpression(expr.arguments[0]);
if (!dep.isString()) {
parser.state.module.warnings.push({
message: 'navigator.serviceWorker.register() will only be bundled if passed a String literal.'
});
return false;
}
const filename = dep.string;
const outputFilename = this.options.filename || 'serviceworker.js'
const context = parser.state.current.context;
serviceWorkers.push({
outputFilename,
filename,
context
});
const id = `__webpack__serviceworker__${++counter}`;
ParserHelpers.toConstantDependency(parser, id)(expr.arguments[0]);
return ParserHelpers.addParsedVariableToModule(parser, id, '__webpack_public_path__ + ' + JSON.stringify(outputFilename));
};
parser.hooks.call.for('navigator.serviceWorker.register').tap(NAME, processRegisterCall);
parser.hooks.call.for('self.navigator.serviceWorker.register').tap(NAME, processRegisterCall);
parser.hooks.call.for('window.navigator.serviceWorker.register').tap(NAME, processRegisterCall);
});
}
});
}
createFilter(list) {
const filters = [].concat(list);
for (let i=0; i<filters.length; i++) {
if (typeof filters[i] === 'string') {
filters[i] = minimatch.filter(filters[i]);
}
}
return filters;
}
async emit(compiler, compilation, serviceWorkers) {
let assetMapping = Object.keys(compilation.assets);
if (this.options.include) {
const filters = this.createFilter(this.options.include);
assetMapping = assetMapping.filter(filename => {
for (const filter of filters) {
if (filter(filename)) return true;
}
return false;
});
}
if (this.options.exclude) {
const filters = this.createFilter(this.options.exclude);
assetMapping = assetMapping.filter(filename => {
for (const filter of filters) {
if (filter(filename)) return false;
}
return true;
});
}
await Promise.all(serviceWorkers.map(
(serviceWorker, index) => this.compileServiceWorker(compiler, compilation, serviceWorker, index, assetMapping)
));
}
async compileServiceWorker(compiler, compilation, options, index, assetMapping) {
const entryFilename = options.filename;
const chunkFilename = compiler.options.output.chunkFilename.replace(/\.([a-z]+)$/i, '.serviceworker.$1');
const workerOptions = {
filename: options.outputFilename, // chunkFilename.replace(/\.?\[(?:chunkhash|contenthash|hash)(:\d+(?::\d+)?)?\]/g, ''),
chunkFilename: this.options.chunkFilename || chunkFilename,
globalObject: 'self'
};
const childCompiler = compilation.createChildCompiler(NAME, { filename: workerOptions.filename });
(new WebWorkerTemplatePlugin(workerOptions)).apply(childCompiler);
/* The duplication DefinePlugin ends up causing is problematic (it doesn't hoist injections), so we'll do it manually. */
// (new DefinePlugin({
// BUILD_ASSETS: JSON.stringify(assetMapping)
// })).apply(childCompiler);
(new SingleEntryPlugin(options.context, entryFilename, workerOptions.filename)).apply(childCompiler);
const subCache = `subcache ${__dirname} ${entryFilename} ${index}`;
let childCompilation;
childCompiler.hooks.compilation.tap(NAME, c => {
childCompilation = c;
if (childCompilation.cache) {
if (!childCompilation.cache[subCache]) childCompilation.cache[subCache] = {};
childCompilation.cache = childCompilation.cache[subCache];
}
});
await (util.promisify(childCompiler.runAsChild.bind(childCompiler)))();
const versionVar = this.options.version ?
`var VERSION = ${JSON.stringify(this.options.version)};` : '';
const original = childCompilation.assets[workerOptions.filename].source();
const source = `${versionVar}var BUILD_ASSETS=${JSON.stringify(assetMapping)};${original}`;
childCompilation.assets[workerOptions.filename] = {
source: () => source,
size: () => Buffer.byteLength(source, 'utf8')
};
Object.assign(compilation.assets, childCompilation.assets);
}
};

7
global.d.ts vendored
View File

@@ -1,21 +1,16 @@
declare const __webpack_public_path__: string; declare const __webpack_public_path__: string;
declare const PRERENDER: boolean;
declare interface NodeModule { declare interface NodeModule {
hot: any; hot: any;
} }
declare interface Window { declare interface Window {
STATE: any; STATE: any
ga: typeof ga;
} }
declare namespace JSX { declare namespace JSX {
interface Element { } interface Element { }
interface IntrinsicElements { } interface IntrinsicElements { }
interface HTMLAttributes {
decoding?: string;
}
} }
declare module 'classnames' { declare module 'classnames' {

7549
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,13 @@
{ {
"private": true, "private": true,
"name": "squoosh", "name": "squoosh",
"version": "1.0.2", "version": "0.0.0",
"license": "apache-2.0", "license": "apache-2.0",
"scripts": { "scripts": {
"start": "webpack-dev-server --host 0.0.0.0 --hot", "start": "webpack serve --host 0.0.0.0 --hot",
"build": "webpack -p", "build": "webpack -p",
"lint": "tslint -c tslint.json -p tsconfig.json -t verbose 'src/**/*.{ts,tsx,js,jsx}'", "lint": "tslint -c tslint.json -t verbose 'src/**/*.{ts,tsx,js,jsx}'",
"lintfix": "tslint -c tslint.json -p tsconfig.json -t verbose --fix 'src/**/*.{ts,tsx,js,jsx}'" "lintfix": "tslint -c tslint.json -t verbose --fix 'src/**/*.{ts,tsx,js,jsx}'"
}, },
"husky": { "husky": {
"hooks": { "hooks": {
@@ -15,55 +15,58 @@
} }
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^10.12.6", "@types/node": "^9.6.35",
"@types/pretty-bytes": "^5.1.0", "@types/pretty-bytes": "^5.1.0",
"@types/webassembly-js-api": "0.0.1", "@types/webassembly-js-api": "0.0.1",
"@webcomponents/custom-elements": "^1.2.1", "@webcomponents/custom-elements": "^1.2.1",
"@webpack-cli/serve": "^0.1.2", "babel-loader": "^7.1.5",
"assets-webpack-plugin": "^3.9.7", "babel-plugin-jsx-pragmatic": "^1.0.2",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-decorators-legacy": "^1.3.5",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-plugin-transform-react-constant-elements": "^6.23.0",
"babel-plugin-transform-react-jsx": "^6.24.1",
"babel-plugin-transform-react-remove-prop-types": "^0.4.19",
"babel-preset-env": "^1.7.0",
"babel-register": "^6.26.0",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"clean-webpack-plugin": "^1.0.0", "clean-webpack-plugin": "^0.1.19",
"comlink": "^3.0.3", "comlink": "^3.0.3",
"copy-webpack-plugin": "^4.6.0", "copy-webpack-plugin": "^4.5.3",
"critters-webpack-plugin": "^2.0.1", "css-loader": "^0.28.11",
"css-loader": "^1.0.1",
"ejs": "^2.6.1",
"exports-loader": "^0.7.0", "exports-loader": "^0.7.0",
"file-drop-element": "^0.0.9", "file-drop-element": "0.0.7",
"file-loader": "^2.0.0", "file-loader": "^1.1.11",
"html-webpack-plugin": "^3.2.0", "html-webpack-plugin": "^3.2.0",
"husky": "^1.1.4", "husky": "^1.1.2",
"idb-keyval": "^3.1.0",
"if-env": "^1.0.4", "if-env": "^1.0.4",
"linkstate": "^1.1.1", "linkstate": "^1.1.1",
"loader-utils": "^1.1.0", "loader-utils": "^1.1.0",
"mini-css-extract-plugin": "^0.4.4", "mini-css-extract-plugin": "^0.3.0",
"minimatch": "^3.0.4", "node-sass": "^4.9.4",
"node-sass": "^4.10.0", "optimize-css-assets-webpack-plugin": "^4.0.3",
"optimize-css-assets-webpack-plugin": "^5.0.1",
"pointer-tracker": "^2.0.3", "pointer-tracker": "^2.0.3",
"preact": "^8.3.1", "preact": "^8.3.1",
"prerender-loader": "^1.2.0",
"pretty-bytes": "^5.1.0", "pretty-bytes": "^5.1.0",
"progress-bar-webpack-plugin": "^1.11.0", "progress-bar-webpack-plugin": "^1.11.0",
"raw-loader": "^0.5.1", "raw-loader": "^0.5.1",
"sass-loader": "^7.1.0", "sass-loader": "^7.1.0",
"script-ext-html-webpack-plugin": "^2.1.3", "script-ext-html-webpack-plugin": "^2.0.1",
"source-map-loader": "^0.2.4", "source-map-loader": "^0.2.3",
"style-loader": "^0.23.1", "style-loader": "^0.22.1",
"terser-webpack-plugin": "^1.1.0", "ts-loader": "^4.4.2",
"ts-loader": "^5.3.0",
"tslint": "^5.11.0", "tslint": "^5.11.0",
"tslint-config-airbnb": "^5.11.0", "tslint-config-airbnb": "^5.9.2",
"tslint-config-semistandard": "^7.0.0", "tslint-config-semistandard": "^7.0.0",
"tslint-react": "^3.6.0", "tslint-react": "^3.6.0",
"typescript": "^3.1.6", "typescript": "^2.9.2",
"typings-for-css-modules-loader": "^1.7.0", "typings-for-css-modules-loader": "^1.7.0",
"url-loader": "^1.1.2", "webpack": "=4.19.1",
"webpack": "^4.25.1", "webpack-bundle-analyzer": "^2.13.1",
"webpack-bundle-analyzer": "^3.0.3", "webpack-cli": "^2.1.5",
"webpack-cli": "^3.1.2", "webpack-dev-server": "^3.1.5",
"webpack-dev-server": "^3.1.10", "webpack-plugin-replace": "^1.1.1",
"worker-plugin": "^1.1.1" "worker-plugin": "^1.1.1"
} }
} }

View File

@@ -1,8 +1,9 @@
import { nativeDecode, sniffMimeType, canDecodeImage } from '../lib/util'; import { nativeDecode, sniffMimeType, canDecodeImage } from '../lib/util';
import Processor from './processor'; import Processor from './processor';
import webpDataUrl from 'url-loader!./tiny.webp';
const nativeWebPSupported = canDecodeImage(webpDataUrl); // tslint:disable-next-line:max-line-length Its a data URL. Whatcha gonna do?
const webpFile = 'data:image/webp;base64,UklGRh4AAABXRUJQVlA4TBEAAAAvAAAAAAfQ//73v/+BiOh/AAA=';
const nativeWebPSupported = canDecodeImage(webpFile);
export async function decodeImage(blob: Blob, processor: Processor): Promise<ImageData> { export async function decodeImage(blob: Blob, processor: Processor): Promise<ImageData> {
const mimeType = await sniffMimeType(blob); const mimeType = await sniffMimeType(blob);

View File

@@ -7,46 +7,31 @@ import { EncodeOptions as WebPEncoderOptions } from './webp/encoder-meta';
async function mozjpegEncode( async function mozjpegEncode(
data: ImageData, options: MozJPEGEncoderOptions, data: ImageData, options: MozJPEGEncoderOptions,
): Promise<ArrayBuffer> { ): Promise<ArrayBuffer> {
const { encode } = await import( const { encode } = await import('./mozjpeg/encoder');
/* webpackChunkName: "process-mozjpeg-enc" */
'./mozjpeg/encoder',
);
return encode(data, options); return encode(data, options);
} }
async function quantize(data: ImageData, opts: QuantizeOptions): Promise<ImageData> { async function quantize(data: ImageData, opts: QuantizeOptions): Promise<ImageData> {
const { process } = await import( const { process } = await import('./imagequant/processor');
/* webpackChunkName: "process-imagequant" */
'./imagequant/processor',
);
return process(data, opts); return process(data, opts);
} }
async function optiPngEncode( async function optiPngEncode(
data: BufferSource, options: OptiPNGEncoderOptions, data: BufferSource, options: OptiPNGEncoderOptions,
): Promise<ArrayBuffer> { ): Promise<ArrayBuffer> {
const { compress } = await import( const { compress } = await import('./optipng/encoder');
/* webpackChunkName: "process-optipng" */
'./optipng/encoder',
);
return compress(data, options); return compress(data, options);
} }
async function webpEncode( async function webpEncode(
data: ImageData, options: WebPEncoderOptions, data: ImageData, options: WebPEncoderOptions,
): Promise<ArrayBuffer> { ): Promise<ArrayBuffer> {
const { encode } = await import( const { encode } = await import('./webp/encoder');
/* webpackChunkName: "process-webp-enc" */
'./webp/encoder',
);
return encode(data, options); return encode(data, options);
} }
async function webpDecode(data: ArrayBuffer): Promise<ImageData> { async function webpDecode(data: ArrayBuffer): Promise<ImageData> {
const { decode } = await import( const { decode } = await import('./webp/decoder');
/* webpackChunkName: "process-webp-dec" */
'./webp/decoder',
);
return decode(data); return decode(data);
} }

View File

@@ -61,10 +61,7 @@ export default class Processor {
// worker-loader does magic here. // worker-loader does magic here.
// @ts-ignore - Typescript doesn't know about the 2nd param to new Worker, and the // @ts-ignore - Typescript doesn't know about the 2nd param to new Worker, and the
// definition can't be overwritten. // definition can't be overwritten.
this._worker = new Worker( this._worker = new Worker('./processor-worker.ts', { type: 'module' }) as Worker;
'./processor-worker.ts',
{ name: 'processor-worker', type: 'module' },
) as Worker;
// Need to do some TypeScript trickery to make the type match. // Need to do some TypeScript trickery to make the type match.
this._workerApi = proxy(this._worker) as any as ProcessorWorkerApi; this._workerApi = proxy(this._worker) as any as ProcessorWorkerApi;
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 B

View File

@@ -12,15 +12,6 @@ import '../custom-els/LoadingSpinner';
// This is imported for TypeScript only. It isn't used. // This is imported for TypeScript only. It isn't used.
import Compress from '../compress'; import Compress from '../compress';
const compressPromise = import(
/* webpackChunkName: "main-app" */
'../compress',
);
const offlinerPromise = import(
/* webpackChunkName: "offliner" */
'../../lib/offliner',
);
export interface SourceImage { export interface SourceImage {
file: File | Fileish; file: File | Fileish;
data: ImageData; data: ImageData;
@@ -45,14 +36,12 @@ export default class App extends Component<Props, State> {
constructor() { constructor() {
super(); super();
compressPromise.then((module) => { import('../compress').then((module) => {
this.setState({ Compress: module.default }); this.setState({ Compress: module.default });
}).catch(() => { }).catch(() => {
this.showSnack('Failed to load app'); this.showSnack('Failed to load app');
}); });
offlinerPromise.then(({ offliner }) => offliner(this.showSnack));
// In development, persist application state across hot reloads: // In development, persist application state across hot reloads:
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
this.setState(window.STATE); this.setState(window.STATE);
@@ -82,11 +71,6 @@ export default class App extends Component<Props, State> {
return this.snackbar.showSnackbar(message, options); return this.snackbar.showSnackbar(message, options);
} }
@bind
private onBack() {
this.setState({ file: undefined });
}
render({}: Props, { file, Compress }: State) { render({}: Props, { file, Compress }: State) {
return ( return (
<div id="app" class={style.app}> <div id="app" class={style.app}>
@@ -94,7 +78,7 @@ export default class App extends Component<Props, State> {
{(!file) {(!file)
? <Intro onFile={this.onIntroPickFile} showSnack={this.showSnack} /> ? <Intro onFile={this.onIntroPickFile} showSnack={this.showSnack} />
: (Compress) : (Compress)
? <Compress file={file} showSnack={this.showSnack} onBack={this.onBack} /> ? <Compress file={file} showSnack={this.showSnack} />
: <loading-spinner class={style.appLoader}/> : <loading-spinner class={style.appLoader}/>
} }
<snack-bar ref={linkRef(this, 'snackbar')} /> <snack-bar ref={linkRef(this, 'snackbar')} />

View File

@@ -54,5 +54,4 @@ $horizontalPadding: 15px;
.options-scroller { .options-scroller {
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
-webkit-overflow-scrolling: touch;
} }

View File

@@ -242,7 +242,7 @@ export default class PinchZoom extends HTMLElement {
/** /**
* Update transform values without checking bounds. This is only called in setTransform. * Update transform values without checking bounds. This is only called in setTransform.
*/ */
private _updateTransform(scale: number, x: number, y: number, allowChangeEvent: boolean) { _updateTransform(scale: number, x: number, y: number, allowChangeEvent: boolean) {
// Avoid scaling to zero // Avoid scaling to zero
if (scale < MIN_SCALE) return; if (scale < MIN_SCALE) return;

View File

@@ -70,13 +70,12 @@ export default class TwoUp extends HTMLElement {
connectedCallback() { connectedCallback() {
this._childrenChange(); this._childrenChange();
this._handle.innerHTML = `<div class="${styles.scrubber}">${
`<svg viewBox="0 0 27 20" fill="currentColor">${
'<path d="M17 19.2l9.5-9.6L16.9 0zM9.6 0L0 9.6l9.6 9.6z"/>'
}</svg>`
}</div>`;
if (!this._everConnected) { if (!this._everConnected) {
this._handle.innerHTML = `<div class="${styles.scrubber}">${
`<svg viewBox="0 0 27 20" fill="currentColor">${
'<path d="M17 19.2l9.5-9.6L16.9 0zM9.6 0L0 9.6l9.6 9.6z"/>'
}</svg>`
}</div>`;
this._resetPosition(); this._resetPosition();
this._everConnected = true; this._everConnected = true;
} }

View File

@@ -5,7 +5,7 @@ import './custom-els/TwoUp';
import * as style from './style.scss'; import * as style from './style.scss';
import { bind, linkRef } from '../../lib/initial-util'; import { bind, linkRef } from '../../lib/initial-util';
import { shallowEqual, drawDataToCanvas } from '../../lib/util'; import { shallowEqual, drawDataToCanvas } from '../../lib/util';
import { ToggleIcon, AddIcon, RemoveIcon, BackIcon } from '../../lib/icons'; import { ToggleIcon, AddIcon, RemoveIcon } from '../../lib/icons';
import { twoUpHandle } from './custom-els/TwoUp/styles.css'; import { twoUpHandle } from './custom-els/TwoUp/styles.css';
interface Props { interface Props {
@@ -15,7 +15,6 @@ interface Props {
rightCompressed?: ImageData; rightCompressed?: ImageData;
leftImgContain: boolean; leftImgContain: boolean;
rightImgContain: boolean; rightImgContain: boolean;
onBack: () => void;
} }
interface State { interface State {
@@ -48,15 +47,6 @@ export default class Output extends Component<Props, State> {
const leftDraw = this.leftDrawable(); const leftDraw = this.leftDrawable();
const rightDraw = this.rightDrawable(); const rightDraw = this.rightDrawable();
// Reset the pinch zoom, which may have an position set from the previous view, after pressing
// the back button.
this.pinchZoomLeft!.setTransform({
allowChangeEvent: true,
x: 0,
y: 0,
scale: 1,
});
if (this.canvasLeft && leftDraw) { if (this.canvasLeft && leftDraw) {
drawDataToCanvas(this.canvasLeft, leftDraw); drawDataToCanvas(this.canvasLeft, leftDraw);
} }
@@ -187,21 +177,10 @@ export default class Output extends Component<Props, State> {
const clonedEvent = new (event.constructor as typeof Event)(event.type, event); const clonedEvent = new (event.constructor as typeof Event)(event.type, event);
this.retargetedEvents.add(clonedEvent); this.retargetedEvents.add(clonedEvent);
this.pinchZoomLeft.dispatchEvent(clonedEvent); this.pinchZoomLeft.dispatchEvent(clonedEvent);
// Unfocus any active element on touchend. This fixes an issue on (at least) Android Chrome,
// where the software keyboard is hidden, but the input remains focused, then after interaction
// with this element the keyboard reappears for NO GOOD REASON. Thanks Android.
if (
event.type === 'touchend' &&
document.activeElement &&
document.activeElement instanceof HTMLElement
) {
document.activeElement.blur();
}
} }
render( render(
{ mobileView, leftImgContain, rightImgContain, originalImage, onBack }: Props, { mobileView, leftImgContain, rightImgContain, originalImage }: Props,
{ scale, editingScale, altBackground }: State, { scale, editingScale, altBackground }: State,
) { ) {
const leftDraw = this.leftDrawable(); const leftDraw = this.leftDrawable();
@@ -253,12 +232,6 @@ export default class Output extends Component<Props, State> {
</pinch-zoom> </pinch-zoom>
</two-up> </two-up>
<div class={style.back}>
<button class={style.button} onClick={onBack}>
<BackIcon />
</button>
</div>
<div class={style.controls}> <div class={style.controls}>
<div class={style.zoomControls}> <div class={style.zoomControls}>
<button class={style.button} onClick={this.zoomOut}> <button class={style.button} onClick={this.zoomOut}>

View File

@@ -38,7 +38,7 @@
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
padding: 9px 84px; padding: 9px;
overflow: hidden; overflow: hidden;
flex-wrap: wrap; flex-wrap: wrap;
contain: content; contain: content;
@@ -50,7 +50,6 @@
} }
@media (min-width: 860px) { @media (min-width: 860px) {
padding: 9px;
top: auto; top: auto;
left: 320px; left: 320px;
right: 320px; right: 320px;
@@ -79,19 +78,14 @@
display: flex; display: flex;
align-items: center; align-items: center;
box-sizing: border-box; box-sizing: border-box;
height: 48px;
padding: 0 16px;
margin: 4px; margin: 4px;
background-color: #fff; background-color: #fff;
border: 1px solid rgba(0,0,0,0.2); border: 1px solid rgba(0,0,0,0.2);
border-radius: 5px; border-radius: 5px;
line-height: 1; line-height: 1;
white-space: nowrap; white-space: nowrap;
height: 36px;
padding: 0 8px;
@media (min-width: 600px) {
height: 48px;
padding: 0 16px;
}
&:focus { &:focus {
box-shadow: 0 0 0 2px var(--button-fg); box-shadow: 0 0 0 2px var(--button-fg);
@@ -140,10 +134,3 @@
// https://bugs.chromium.org/p/chromium/issues/detail?id=870222#c10 // https://bugs.chromium.org/p/chromium/issues/detail?id=870222#c10
will-change: auto; will-change: auto;
} }
.back {
position: absolute;
top: 0;
left: 0;
padding: 9px;
}

View File

@@ -103,7 +103,7 @@ export default class MultiPanel extends HTMLElement {
// KeyDown event handler // KeyDown event handler
private _onKeyDown(event: KeyboardEvent) { private _onKeyDown(event: KeyboardEvent) {
const selectedEl = document.activeElement!; const selectedEl = document.activeElement;
const heading = getClosestHeading(selectedEl); const heading = getClosestHeading(selectedEl);
// if keydown event is not on heading element, ignore // if keydown event is not on heading element, ignore
@@ -252,8 +252,8 @@ export default class MultiPanel extends HTMLElement {
return this.firstElementChild as HTMLElement; return this.firstElementChild as HTMLElement;
} }
// previous Element of active Element is previous Content, // previous Element of active Element is previous Content,
// previous Element of previous Content is previousHeading // previous Element of previous Content is previousHeading
const previousContent = document.activeElement!.previousElementSibling; const previousContent = document.activeElement.previousElementSibling;
if (previousContent) { if (previousContent) {
return previousContent.previousElementSibling as HTMLElement; return previousContent.previousElementSibling as HTMLElement;
} }
@@ -263,7 +263,7 @@ export default class MultiPanel extends HTMLElement {
private _nextHeading() { private _nextHeading() {
// activeElement would be the currently selected heading // activeElement would be the currently selected heading
// 2 elemements after that would be the next heading. // 2 elemements after that would be the next heading.
const nextContent = document.activeElement!.nextElementSibling; const nextContent = document.activeElement.nextElementSibling;
if (nextContent) { if (nextContent) {
return nextContent.nextElementSibling as HTMLElement; return nextContent.nextElementSibling as HTMLElement;
} }

View File

@@ -60,7 +60,6 @@ interface EncodedImage {
interface Props { interface Props {
file: File | Fileish; file: File | Fileish;
showSnack: SnackBarElement['showSnackbar']; showSnack: SnackBarElement['showSnackbar'];
onBack: () => void;
} }
interface State { interface State {
@@ -196,8 +195,6 @@ export default class Compress extends Component<Props, State> {
super(props); super(props);
this.widthQuery.addListener(this.onMobileWidthChange); this.widthQuery.addListener(this.onMobileWidthChange);
this.updateFile(props.file); this.updateFile(props.file);
import('../../lib/offliner').then(({ mainAppLoaded }) => mainAppLoaded());
} }
@bind @bind
@@ -416,7 +413,7 @@ export default class Compress extends Component<Props, State> {
this.setState({ images }); this.setState({ images });
} }
render({ onBack }: Props, { loading, images, source, mobileView }: State) { render({ }: Props, { loading, images, source, mobileView }: State) {
const [leftImage, rightImage] = images; const [leftImage, rightImage] = images;
const [leftImageData, rightImageData] = images.map(i => i.data); const [leftImageData, rightImageData] = images.map(i => i.data);
@@ -461,7 +458,6 @@ export default class Compress extends Component<Props, State> {
rightCompressed={rightImageData} rightCompressed={rightImageData}
leftImgContain={leftImage.preprocessorState.resize.fitMethod === 'cover'} leftImgContain={leftImage.preprocessorState.resize.fitMethod === 'cover'}
rightImgContain={rightImage.preprocessorState.resize.fitMethod === 'cover'} rightImgContain={rightImage.preprocessorState.resize.fitMethod === 'cover'}
onBack={onBack}
/> />
{mobileView {mobileView
? ( ? (

View File

@@ -22,7 +22,7 @@
max-width: 400px; max-width: 400px;
margin: 0 auto; margin: 0 auto;
width: calc(100% - 60px); width: calc(100% - 60px);
max-height: calc(100% - 104px); max-height: calc(100% - 143px);
overflow: hidden; overflow: hidden;
@media (min-width: 600px) { @media (min-width: 600px) {
@@ -32,7 +32,7 @@
} }
@media (min-width: 860px) { @media (min-width: 860px) {
max-height: calc(100% - 40px); max-height: 100%;
} }
} }

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Before

Width:  |  Height:  |  Size: 2.8 MiB

After

Width:  |  Height:  |  Size: 2.8 MiB

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 MiB

After

Width:  |  Height:  |  Size: 2.7 MiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -4,13 +4,13 @@ import { bind, linkRef, Fileish } from '../../lib/initial-util';
import '../custom-els/LoadingSpinner'; import '../custom-els/LoadingSpinner';
import logo from './imgs/logo.svg'; import logo from './imgs/logo.svg';
import largePhoto from './imgs/demos/demo-large-photo.jpg'; import largePhoto from './imgs/demos/large-photo.jpg';
import artwork from './imgs/demos/demo-artwork.jpg'; import artwork from './imgs/demos/artwork.jpg';
import deviceScreen from './imgs/demos/demo-device-screen.png'; import deviceScreen from './imgs/demos/device-screen.png';
import largePhotoIcon from './imgs/demos/icon-demo-large-photo.jpg'; import largePhotoIcon from './imgs/demos/large-photo-icon.jpg';
import artworkIcon from './imgs/demos/icon-demo-artwork.jpg'; import artworkIcon from './imgs/demos/artwork-icon.jpg';
import deviceScreenIcon from './imgs/demos/icon-demo-device-screen.jpg'; import deviceScreenIcon from './imgs/demos/device-screen-icon.jpg';
import logoIcon from './imgs/demos/icon-demo-logo.png'; import logoIcon from './imgs/demos/logo-icon.png';
import * as style from './style.scss'; import * as style from './style.scss';
import SnackBarElement from '../../lib/SnackBar'; import SnackBarElement from '../../lib/SnackBar';
@@ -90,7 +90,7 @@ export default class Intro extends Component<Props, State> {
<div> <div>
<div class={style.logoSizer}> <div class={style.logoSizer}>
<div class={style.logoContainer}> <div class={style.logoContainer}>
<img src={logo} class={style.logo} alt="Squoosh" decoding="async" /> <img src={logo} class={style.logo} alt="Squoosh" />
</div> </div>
</div> </div>
<p class={style.openImageGuide}> <p class={style.openImageGuide}>
@@ -111,7 +111,7 @@ export default class Intro extends Component<Props, State> {
<div class={style.demo}> <div class={style.demo}>
<div class={style.demoImgContainer}> <div class={style.demoImgContainer}>
<div class={style.demoImgAspect}> <div class={style.demoImgAspect}>
<img class={style.demoIcon} src={demo.iconUrl} alt="" decoding="async" /> <img class={style.demoIcon} src={demo.iconUrl} alt=""/>
{fetchingDemoIndex === i && {fetchingDemoIndex === i &&
<div class={style.demoLoading}> <div class={style.demoLoading}>
<loading-spinner class={style.demoLoadingSpinner}/> <loading-spinner class={style.demoLoadingSpinner}/>
@@ -129,11 +129,6 @@ export default class Intro extends Component<Props, State> {
<ul class={style.relatedLinks}> <ul class={style.relatedLinks}>
<li><a href="https://github.com/GoogleChromeLabs/squoosh/">View the code</a></li> <li><a href="https://github.com/GoogleChromeLabs/squoosh/">View the code</a></li>
<li><a href="https://github.com/GoogleChromeLabs/squoosh/issues">Report a bug</a></li> <li><a href="https://github.com/GoogleChromeLabs/squoosh/issues">Report a bug</a></li>
<li>
<a href="https://github.com/GoogleChromeLabs/squoosh/blob/master/README.md#privacy">
Privacy
</a>
</li>
</ul> </ul>
</div> </div>
); );

View File

@@ -2,7 +2,6 @@
font-family: 'intro-text'; font-family: 'intro-text';
font-style: normal; font-style: normal;
font-weight: 300; font-weight: 300;
font-display: block;
// This only contains the chars for "Drag & drop or" // This only contains the chars for "Drag & drop or"
src: url('data:font/woff2;base64,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') format('woff2'); src: url('data:font/woff2;base64,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') format('woff2');
} }
@@ -11,7 +10,6 @@
font-family: 'intro-text'; font-family: 'intro-text';
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
font-display: block;
// Only contains the chars for "select an image" // Only contains the chars for "select an image"
src: url('data:font/woff2;base64,d09GMgABAAAAAAXMAA4AAAAACwQAAAV5AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhYbg1IcMAZgAGwRCAqJUId/CxoAATYCJAMwBCAFgnAHIBskCcjEh6dNff8Ou/9Tj9VZGnUhJeqFzWGiVVOkxkTthr9f6kWkRdsBbkIj3YuLaloFZWr7aBg22z7IOoWqBWCW5cZU3GBrh0n+dAcBYAUlzYHAzWTYchqKXEyAT0zOLIS1qqm+B8q2ur4OhEMy0PHHUH8KklaSr8T0mp/EU7kRvXlI1E09HXA1qLN8Djxa0AsSDOg3cJARb9mtQJoSK3gvEn372/gcAigg/gOnbsT/MYv491GTReW4rJC5LA+h5FFclF6QQgoZ5Kx7GbsuGeytUgFClkOomY2Gdake3m9HegkHieAx/a0hBTALsy4jvpxBcnFXUnjC+2ZS5zHnDeEaJVwi+ZWqzOm4Uvgy4k6kGv4kFDVkfjk1gkVRRkk2zlo42PBbRJmG30cClJQjak7BnfQqHza4ITKftQZ/ZMUaEiyy1+mYCh4clKhDA5rQglZ0oG9jw+qiNvT+SfxIXCeuFdeIq8VV4gpxyRaGOl0JiChiCocfc7e93DwZIPvWgPiZJLcJugxyjW7UQyl4TJk6dWqYU7Cn0WQiWnNJCdGeprqjW63fpVS3mKy4YGZ6I3ya4nbIVgM1mwkpNEBzixlNxfPmH7owvdE4973OM9quvk11dwvnzDUy/Zn5S5Ywpn/PeqXBQI2m4lna05CRtsI6+GIENjS9K4jWRHUGYA2ozdZm2Smmf0DI3aqpeNbsJfxe7YdFmcZAn5gXLCFa2/Umqiu017APFhMZ0rfQp4sJX0ZrJ+n9UtAljr5VYWb6oj1MrpvX3qe6u8WRJg0bj7aPkDOa7m+E0Oa9Y8eY/gbRbr+efH7hcO49bMd28fbDVHcUmm3XkozQGKjeBHSJ4TQnI879LIFmF2v/BJuEQlffJPfE9oKayS/PsPE44fvM4MsBESxbuEEV39d5pw6oW4vD6S1WQC3UpSbHNbK0Jikl0bphSs+0CGW8Ew4Kzw7zarmcVz873JHTFhKYay18R8vY0ozPiHPAGyROAqlW5fLj5+HbWBn9TpgekKsOy8N+4dlFfL9i/Nk3+gY1bwzZUAvLVNiFpvqHRenetSoVrgn2obGtPsltEVxEeHJAQFhyBIcHT9rDvTJm06e0TLgase2gd2RffGJNg0o1zrdRyi9s1bYE5bi85cK+o/nUwvBR5+jweEBaSMoCub29fEFISmib9Dn5yl5kVFpoGrPQSmZhafQ7WimttNCH7Cktohb6kFpoEfIsdDF7SgvZTbaY3mKFyLXQh+wpzWE3mUNQQkWnR+lgiW9Afkunej49Nz3sYlI8XFTRdkNhUR5d6h4oOpJc8OjcItMXVqoHW2fSW6ycWuiM8NDYoICouLAZ9BYrwwmhycvKlt4Q8hUlCV5nZ7vOm2ut2rizcFpuWpSrT3K1Z3xinbuHnXBTyGAljV/XzHZaNGu6y7vLDziMpIyUdOBBRXXlxznUQiOoheZsZk9njG8er1mNmz2eOCQ3x9BbLP+Zxt+VrbEz9aWxmimRvyl4/sumyoM/nw+LNzV4/uP0/9T/P5f08cvhl38USAS/xTYs2fL/VNFF0vd+SVsRB/khPwW4SCi5SHhx8fDgVPAiAqIJRQL/EuK5bsRzNnAiezD1i7u2VyHHAKRU+E2YUaA5DUsE2ZfbApAmsJcxjBwmQ2Xk4Y2BqZJ+oxSzsNEogz1O3/xkBMKEBHSiC8PoQStaoEIflPCHL/wQBCUKoUITlMhHP+olt5ojykUPOrEQTWgY+f449KMPKnSiB72jGrLQhEZO24925LtbkG5DndTkD2/4um+NQBEyUIJsRBxX24tcDGmbWtGJjq05jhzTaMgCcR+6EA4f+KAXDay5u6RsL7xJsm3w3mzvFvggB8nI/AYBdVnxNvx/2wA=') format('woff2'); src: url('data:font/woff2;base64,d09GMgABAAAAAAXMAA4AAAAACwQAAAV5AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhYbg1IcMAZgAGwRCAqJUId/CxoAATYCJAMwBCAFgnAHIBskCcjEh6dNff8Ou/9Tj9VZGnUhJeqFzWGiVVOkxkTthr9f6kWkRdsBbkIj3YuLaloFZWr7aBg22z7IOoWqBWCW5cZU3GBrh0n+dAcBYAUlzYHAzWTYchqKXEyAT0zOLIS1qqm+B8q2ur4OhEMy0PHHUH8KklaSr8T0mp/EU7kRvXlI1E09HXA1qLN8Djxa0AsSDOg3cJARb9mtQJoSK3gvEn372/gcAigg/gOnbsT/MYv491GTReW4rJC5LA+h5FFclF6QQgoZ5Kx7GbsuGeytUgFClkOomY2Gdake3m9HegkHieAx/a0hBTALsy4jvpxBcnFXUnjC+2ZS5zHnDeEaJVwi+ZWqzOm4Uvgy4k6kGv4kFDVkfjk1gkVRRkk2zlo42PBbRJmG30cClJQjak7BnfQqHza4ITKftQZ/ZMUaEiyy1+mYCh4clKhDA5rQglZ0oG9jw+qiNvT+SfxIXCeuFdeIq8VV4gpxyRaGOl0JiChiCocfc7e93DwZIPvWgPiZJLcJugxyjW7UQyl4TJk6dWqYU7Cn0WQiWnNJCdGeprqjW63fpVS3mKy4YGZ6I3ya4nbIVgM1mwkpNEBzixlNxfPmH7owvdE4973OM9quvk11dwvnzDUy/Zn5S5Ywpn/PeqXBQI2m4lna05CRtsI6+GIENjS9K4jWRHUGYA2ozdZm2Smmf0DI3aqpeNbsJfxe7YdFmcZAn5gXLCFa2/Umqiu017APFhMZ0rfQp4sJX0ZrJ+n9UtAljr5VYWb6oj1MrpvX3qe6u8WRJg0bj7aPkDOa7m+E0Oa9Y8eY/gbRbr+efH7hcO49bMd28fbDVHcUmm3XkozQGKjeBHSJ4TQnI879LIFmF2v/BJuEQlffJPfE9oKayS/PsPE44fvM4MsBESxbuEEV39d5pw6oW4vD6S1WQC3UpSbHNbK0Jikl0bphSs+0CGW8Ew4Kzw7zarmcVz873JHTFhKYay18R8vY0ozPiHPAGyROAqlW5fLj5+HbWBn9TpgekKsOy8N+4dlFfL9i/Nk3+gY1bwzZUAvLVNiFpvqHRenetSoVrgn2obGtPsltEVxEeHJAQFhyBIcHT9rDvTJm06e0TLgase2gd2RffGJNg0o1zrdRyi9s1bYE5bi85cK+o/nUwvBR5+jweEBaSMoCub29fEFISmib9Dn5yl5kVFpoGrPQSmZhafQ7WimttNCH7Cktohb6kFpoEfIsdDF7SgvZTbaY3mKFyLXQh+wpzWE3mUNQQkWnR+lgiW9Afkunej49Nz3sYlI8XFTRdkNhUR5d6h4oOpJc8OjcItMXVqoHW2fSW6ycWuiM8NDYoICouLAZ9BYrwwmhycvKlt4Q8hUlCV5nZ7vOm2ut2rizcFpuWpSrT3K1Z3xinbuHnXBTyGAljV/XzHZaNGu6y7vLDziMpIyUdOBBRXXlxznUQiOoheZsZk9njG8er1mNmz2eOCQ3x9BbLP+Zxt+VrbEz9aWxmimRvyl4/sumyoM/nw+LNzV4/uP0/9T/P5f08cvhl38USAS/xTYs2fL/VNFF0vd+SVsRB/khPwW4SCi5SHhx8fDgVPAiAqIJRQL/EuK5bsRzNnAiezD1i7u2VyHHAKRU+E2YUaA5DUsE2ZfbApAmsJcxjBwmQ2Xk4Y2BqZJ+oxSzsNEogz1O3/xkBMKEBHSiC8PoQStaoEIflPCHL/wQBCUKoUITlMhHP+olt5ojykUPOrEQTWgY+f449KMPKnSiB72jGrLQhEZO24925LtbkG5DndTkD2/4um+NQBEyUIJsRBxX24tcDGmbWtGJjq05jhzTaMgCcR+6EA4f+KAXDay5u6RsL7xJsm3w3mzvFvggB8nI/AYBdVnxNvx/2wA=') format('woff2');
} }
@@ -21,6 +19,7 @@
} }
.intro { .intro {
composes: abs-fill from '../../lib/util.scss';
display: grid; display: grid;
grid-template-rows: 1fr min-content; grid-template-rows: 1fr min-content;
align-items: center; align-items: center;
@@ -30,9 +29,6 @@
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
overflow: auto; overflow: auto;
padding: 20px 0 0; padding: 20px 0 0;
height: 100%;
box-sizing: border-box;
overscroll-behavior: contain;
} }
.logo-container { .logo-container {
@@ -42,17 +38,16 @@
.logo-sizer { .logo-sizer {
width: 90%; width: 90%;
max-width: 52vh; max-width: 480px;
margin: 0 auto; margin: 0 auto;
} }
.logo { .logo {
composes: abs-fill from '../../lib/util.scss'; composes: abs-fill from '../../lib/util.scss';
pointer-events: none;
} }
.open-image-guide { .open-image-guide {
font: 300 11vw intro-text, sans-serif; font: 300 11vw intro-text;
margin-bottom: 0; margin-bottom: 0;
@media (min-width: 460px) { @media (min-width: 460px) {
@@ -145,7 +140,6 @@
.demo-icon { .demo-icon {
composes: abs-fill from '../../lib/util.scss'; composes: abs-fill from '../../lib/util.scss';
pointer-events: none;
} }
.demo-description { .demo-description {

View File

@@ -13,8 +13,6 @@ export default class Range extends Component<Props, State> {
@bind @bind
private onTextInput(event: Event) { private onTextInput(event: Event) {
const input = event.target as HTMLInputElement; const input = event.target as HTMLInputElement;
const value = input.value.trim();
if (!value) return;
this.rangeWc!.value = input.value; this.rangeWc!.value = input.value;
const { onInput } = this.props; const { onInput } = this.props;
if (onInput) onInput(event); if (onInput) onInput(event);

View File

@@ -58,21 +58,6 @@ export default class Results extends Component<Props, State> {
this.props.onCopyToOtherClick(); this.props.onCopyToOtherClick();
} }
@bind
onDownload() {
// GA cant do floats. So we round to ints. We're deliberately rounding to nearest kilobyte to
// avoid cases where exact image sizes leak something interesting about the user.
const before = Math.round(this.props.source!.file.size / 1024);
const after = Math.round(this.props.imageFile!.size / 1024);
const change = Math.round(after / before * 1000);
ga('send', 'event', 'compression', 'download', {
metric1: before,
metric2: after,
metric3: change,
});
}
render( render(
{ source, imageFile, downloadUrl, children, copyDirection, buttonPosition }: Props, { source, imageFile, downloadUrl, children, copyDirection, buttonPosition }: Props,
{ showLoadingState }: State, { showLoadingState }: State,
@@ -108,7 +93,6 @@ export default class Results extends Component<Props, State> {
href={downloadUrl} href={downloadUrl}
download={imageFile.name} download={imageFile.name}
title="Download" title="Download"
onClick={this.onDownload}
> >
<DownloadIcon class={style.downloadIcon} /> <DownloadIcon class={style.downloadIcon} />
</a> </a>

View File

@@ -4,13 +4,13 @@
<meta charset="utf-8"> <meta charset="utf-8">
<title>Squoosh</title> <title>Squoosh</title>
<meta name="description" content="Compress and compare images with different codecs, right in your browser"> <meta name="description" content="Compress and compare images with different codecs, right in your browser">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> <meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<link rel="shortcut icon" href="/assets/favicon.ico"> <meta name="theme-color" content="#673ab8">
<meta name="theme-color" content="#f78f21">
<link rel="manifest" href="/manifest.json"> <link rel="manifest" href="/manifest.json">
</head> </head>
<body> <body>
<div id="app"></div>
</body> </body>
</html> </html>

View File

@@ -1,25 +1,9 @@
declare module '@webcomponents/custom-elements'; declare module '@webcomponents/custom-elements';
function init() { (async function () {
if (!('customElements' in self)) {
await import('@webcomponents/custom-elements');
}
require('./init-app.tsx'); require('./init-app.tsx');
} })();
if (!('customElements' in self)) {
import(
/* webpackChunkName: "wc-polyfill" */
'@webcomponents/custom-elements',
).then(init);
} else {
init();
}
if (typeof PRERENDER === 'undefined') {
window.ga = window.ga || ((...args) => (ga.q = ga.q || []).push(args));
ga('create', 'UA-128752250-1', 'auto');
ga('set', 'transport', 'beacon');
ga('send', 'pageview');
// Load the GA script
const s = document.createElement('script');
s.src = 'https://www.google-analytics.com/analytics.js';
document.head!.appendChild(s);
}

View File

@@ -4,13 +4,13 @@ import './style';
import App from './components/App'; import App from './components/App';
// Find the outermost Element in our server-rendered HTML structure. // Find the outermost Element in our server-rendered HTML structure.
let root = document.getElementById('app_root') as Element; let root = document.querySelector('#app') || undefined;
// "attach" the client-side rendering to it, updating the DOM in-place instead of replacing: // "attach" the client-side rendering to it, updating the DOM in-place instead of replacing:
root = render(<App />, document.body, root); root = render(<App />, document.body, root);
root.setAttribute('id', 'app_root');
if (process.env.NODE_ENV !== 'production') { // In production, this entire condition is removed.
if (process.env.NODE_ENV === 'development') {
// Enable support for React DevTools and some helpful console warnings: // Enable support for React DevTools and some helpful console warnings:
require('preact/debug'); require('preact/debug');

View File

@@ -8,9 +8,12 @@ export interface SnackOptions {
function createSnack(message: string, options: SnackOptions): [Element, Promise<string>] { function createSnack(message: string, options: SnackOptions): [Element, Promise<string>] {
const { const {
timeout = 0, timeout = 0,
actions = ['dismiss'], actions = [],
} = options; } = options;
// Provide a default 'dismiss' action
if (!timeout && actions.length === 0) actions.push('dismiss');
const el = document.createElement('div'); const el = document.createElement('div');
el.className = style.snackbar; el.className = style.snackbar;
el.setAttribute('aria-live', 'assertive'); el.setAttribute('aria-live', 'assertive');

View File

@@ -48,12 +48,6 @@ export const ExpandIcon = (props: JSX.HTMLAttributes) => (
</Icon> </Icon>
); );
export const BackIcon = (props: JSX.HTMLAttributes) => (
<Icon {...props}>
<path d="M20 11H7.8l5.6-5.6L12 4l-8 8 8 8 1.4-1.4L7.8 13H20v-2z"/>
</Icon>
);
const copyAcrossRotations = { const copyAcrossRotations = {
up: 90, right: 180, down: -90, left: 0, up: 90, right: 180, down: -90, left: 0,
}; };

View File

@@ -1,98 +0,0 @@
import { get, set } from 'idb-keyval';
// Just for TypeScript
import SnackBarElement from './SnackBar';
/** Tell the service worker to skip waiting */
async function skipWaiting() {
const reg = await navigator.serviceWorker.getRegistration();
if (!reg || !reg.waiting) return;
reg.waiting.postMessage('skip-waiting');
}
/** Find the service worker that's 'active' or closest to 'active' */
async function getMostActiveServiceWorker() {
const reg = await navigator.serviceWorker.getRegistration();
if (!reg) return null;
return reg.active || reg.waiting || reg.installing;
}
/** Wait for an installing worker */
async function installingWorker(reg: ServiceWorkerRegistration): Promise<ServiceWorker> {
if (reg.installing) return reg.installing;
return new Promise<ServiceWorker>((resolve) => {
reg.addEventListener(
'updatefound',
() => resolve(reg.installing!),
{ once: true },
);
});
}
/** Wait a service worker to become waiting */
async function updateReady(reg: ServiceWorkerRegistration): Promise<void> {
if (reg.waiting) return;
const installing = await installingWorker(reg);
return new Promise<void>((resolve) => {
installing.addEventListener('statechange', () => {
if (installing.state === 'installed') resolve();
});
});
}
/** Set up the service worker and monitor changes */
export async function offliner(showSnack: SnackBarElement['showSnackbar']) {
// This needs to be a typeof because Webpack.
if (typeof PRERENDER === 'boolean') return;
if (process.env.NODE_ENV === 'production') {
navigator.serviceWorker.register('../sw');
}
const hasController = !!navigator.serviceWorker.controller;
// Look for changes in the controller
navigator.serviceWorker.addEventListener('controllerchange', async () => {
// Is it the first install?
if (!hasController) {
showSnack('Ready to work offline', { timeout: 5000 });
return;
}
// Otherwise reload (the user will have agreed to this).
location.reload();
});
// If we don't have a controller, we don't need to check for updates we've just loaded from the
// network.
if (!hasController) return;
const reg = await navigator.serviceWorker.getRegistration();
// Service worker not registered yet.
if (!reg) return;
// Look for updates
await updateReady(reg);
// Ask the user if they want to update.
const result = await showSnack('Update available', {
actions: ['reload', 'dismiss'],
});
// Tell the waiting worker to activate, this will change the controller and cause a reload (see
// 'controllerchange')
if (result === 'reload') skipWaiting();
}
/**
* Tell the service worker the main app has loaded. If it's the first time the service worker has
* heard about this, cache the heavier assets like codecs.
*/
export async function mainAppLoaded() {
// If the user has already interacted, no need to tell the service worker anything.
const userInteracted = await get<boolean | undefined>('user-interacted');
if (userInteracted) return;
set('user-interacted', true);
const serviceWorker = await getMostActiveServiceWorker();
if (!serviceWorker) return; // Service worker not installing yet.
serviceWorker.postMessage('cache-all');
}

View File

@@ -57,32 +57,16 @@ export async function canvasEncode(data: ImageData, type: string, quality?: numb
return blob; return blob;
} }
async function decodeImage(url: string): Promise<HTMLImageElement> {
const img = new Image();
img.decoding = 'async';
img.src = url;
const loaded = new Promise((resolve, reject) => {
img.onload = () => resolve();
img.onerror = () => reject(Error('Image loading error'));
});
if (img.decode) {
// Nice off-thread way supported in Safari/Chrome.
// Safari throws on decode if the source is SVG.
// https://bugs.webkit.org/show_bug.cgi?id=188347
await img.decode().catch(() => null);
}
// Always await loaded, as we may have bailed due to the Safari bug above.
await loaded;
return img;
}
/** /**
* Attempts to load the given URL as an image. * Attempts to load the given URL as an image.
*/ */
export function canDecodeImage(url: string): Promise<boolean> { export function canDecodeImage(data: string): Promise<boolean> {
return decodeImage(url).then(() => true, () => false); return new Promise((resolve) => {
const img = document.createElement('img');
img.src = data;
img.onload = _ => resolve(true);
img.onerror = _ => resolve(false);
});
} }
export function blobToArrayBuffer(blob: Blob): Promise<ArrayBuffer> { export function blobToArrayBuffer(blob: Blob): Promise<ArrayBuffer> {
@@ -124,7 +108,24 @@ export async function blobToImg(blob: Blob): Promise<HTMLImageElement> {
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
try { try {
return await decodeImage(url); const img = new Image();
img.decoding = 'async';
img.src = url;
const loaded = new Promise((resolve, reject) => {
img.onload = () => resolve();
img.onerror = () => reject(Error('Image loading error'));
});
if (img.decode) {
// Nice off-thread way supported in Safari/Chrome.
// Safari throws on decode if the source is SVG.
// https://bugs.webkit.org/show_bug.cgi?id=188347
await img.decode().catch(() => null);
}
// Always await loaded, as we may have bailed due to the Safari bug above.
await loaded;
return img;
} finally { } finally {
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
} }

View File

@@ -3,14 +3,14 @@
"short_name": "Squoosh", "short_name": "Squoosh",
"start_url": "/", "start_url": "/",
"display": "standalone", "display": "standalone",
"orientation": "any", "orientation": "portrait",
"background_color": "#fff", "background_color": "#fff",
"theme_color": "#f78f21", "theme_color": "#673ab8",
"icons": [ "icons": [
{ {
"src": "/assets/icon-large.png", "src": "/assets/icon.png",
"type": "image/png", "type": "image/png",
"sizes": "1024x1024" "sizes": "512x512"
} }
] ]
} }

View File

@@ -27,15 +27,3 @@ declare module '*.wasm' {
const content: string; const content: string;
export default content; export default content;
} }
declare module 'url-loader!*' {
const value: string;
export default value;
}
declare var VERSION: string;
declare var ga: {
(...args: any[]): void;
q: any[];
};

View File

@@ -2,6 +2,7 @@
html, body { html, body {
height: 100%; height: 100%;
width: 100%;
padding: 0; padding: 0;
margin: 0; margin: 0;
font: 12px/1.3 system-ui, -apple-system, BlinkMacSystemFont, Roboto, Helvetica, sans-serif; font: 12px/1.3 system-ui, -apple-system, BlinkMacSystemFont, Roboto, Helvetica, sans-serif;

View File

@@ -1,69 +0,0 @@
import {
cacheOrNetworkAndCache, cleanupCache, cacheOrNetwork, cacheBasics, cacheAdditionalProcessors,
} from './util';
import { get } from 'idb-keyval';
// Give TypeScript the correct global.
declare var self: ServiceWorkerGlobalScope;
// This is populated by webpack.
declare var BUILD_ASSETS: string[];
const versionedCache = 'static-' + VERSION;
const dynamicCache = 'dynamic';
const expectedCaches = [versionedCache, dynamicCache];
self.addEventListener('install', (event) => {
event.waitUntil(async function () {
const promises = [];
promises.push(cacheBasics(versionedCache, BUILD_ASSETS));
// If the user has already interacted with the app, update the codecs too.
if (await get('user-interacted')) {
promises.push(cacheAdditionalProcessors(versionedCache, BUILD_ASSETS));
}
await Promise.all(promises);
}());
});
self.addEventListener('activate', (event) => {
self.clients.claim();
event.waitUntil(async function () {
// Remove old caches.
const promises = (await caches.keys()).map((cacheName) => {
if (!expectedCaches.includes(cacheName)) return caches.delete(cacheName);
});
await Promise.all<any>(promises);
}());
});
self.addEventListener('fetch', (event) => {
// We only care about GET.
if (event.request.method !== 'GET') return;
const url = new URL(event.request.url);
// Don't care about other-origin URLs
if (url.origin !== location.origin) return;
if (url.pathname.startsWith('/demo-') || url.pathname.startsWith('/wc-polyfill')) {
cacheOrNetworkAndCache(event, dynamicCache);
cleanupCache(event, dynamicCache, BUILD_ASSETS);
return;
}
cacheOrNetwork(event);
});
self.addEventListener('message', (event) => {
switch (event.data) {
case 'cache-all':
event.waitUntil(cacheAdditionalProcessors(versionedCache, BUILD_ASSETS));
break;
case 'skip-waiting':
self.skipWaiting();
break;
}
});

View File

@@ -1 +0,0 @@
import '../missing-types';

View File

@@ -1,18 +0,0 @@
{
"compileOnSave": false,
"compilerOptions": {
"strict": true,
"target": "esnext",
"module": "esnext",
"lib": [
"webworker",
"esnext"
],
"moduleResolution": "node",
"experimentalDecorators": true,
"noUnusedLocals": true,
"sourceMap": true,
"allowJs": false,
"baseUrl": "."
}
}

View File

@@ -1,106 +0,0 @@
import webpDataUrl from 'url-loader!../codecs/tiny.webp';
export function cacheOrNetwork(event: FetchEvent): void {
event.respondWith(async function () {
const cachedResponse = await caches.match(event.request);
return cachedResponse || fetch(event.request);
}());
}
export function cacheOrNetworkAndCache(event: FetchEvent, cacheName: string): void {
event.respondWith(async function () {
const { request } = event;
// Return from cache if possible.
const cachedResponse = await caches.match(request);
if (cachedResponse) return cachedResponse;
// Else go to the network.
const response = await fetch(request);
const responseToCache = response.clone();
event.waitUntil(async function () {
// Cache what we fetched.
const cache = await caches.open(cacheName);
await cache.put(request, responseToCache);
}());
// Return the network response.
return response;
}());
}
export function cleanupCache(event: FetchEvent, cacheName: string, keepAssets: string[]) {
event.waitUntil(async function () {
const cache = await caches.open(cacheName);
// Clean old entries from the dynamic cache.
const requests = await cache.keys();
const promises = requests.map((cachedRequest) => {
// Get pathname without leading /
const assetPath = new URL(cachedRequest.url).pathname.slice(1);
// If it isn't one of our keepAssets, we don't need it anymore.
if (!keepAssets.includes(assetPath)) return cache.delete(cachedRequest);
});
await Promise.all<any>(promises);
}());
}
function getAssetsWithPrefix(assets: string[], prefixes: string[]) {
return assets.filter(
asset => prefixes.some(prefix => asset.startsWith(prefix)),
);
}
export async function cacheBasics(cacheName: string, buildAssets: string[]) {
const toCache = ['/', '/assets/favicon.ico'];
const prefixesToCache = [
// Main app JS & CSS:
'main-app.',
// Service worker handler:
'offliner.',
// Little icons for the demo images on the homescreen:
'icon-demo-',
// Site logo:
'logo.',
];
const prefixMatches = getAssetsWithPrefix(buildAssets, prefixesToCache);
toCache.push(...prefixMatches);
const cache = await caches.open(cacheName);
await cache.addAll(toCache);
}
export async function cacheAdditionalProcessors(cacheName: string, buildAssets: string[]) {
let toCache = [];
const prefixesToCache = [
// Worker which handles image processing:
'processor-worker.',
// processor-worker imports:
'process-',
];
const prefixMatches = getAssetsWithPrefix(buildAssets, prefixesToCache);
const wasm = buildAssets.filter(asset => asset.endsWith('.wasm'));
toCache.push(...prefixMatches, ...wasm);
const supportsWebP = await (async () => {
if (!self.createImageBitmap) return false;
const response = await fetch(webpDataUrl);
const blob = await response.blob();
return createImageBitmap(blob).then(() => true, () => false);
})();
// No point caching the WebP decoder if it's supported natively:
if (supportsWebP) {
toCache = toCache.filter(asset => !/webp[\-_]dec/.test(asset));
}
const cache = await caches.open(cacheName);
await cache.addAll(toCache);
}

View File

@@ -12,6 +12,5 @@
"jsxFactory": "h", "jsxFactory": "h",
"allowJs": false, "allowJs": false,
"baseUrl": "." "baseUrl": "."
}, }
"exclude": ["src/sw/**/*"] }
}

View File

@@ -2,26 +2,22 @@ const fs = require('fs');
const path = require('path'); const path = require('path');
const webpack = require('webpack'); const webpack = require('webpack');
const CleanPlugin = require('clean-webpack-plugin'); const CleanPlugin = require('clean-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin'); const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const ProgressBarPlugin = require('progress-bar-webpack-plugin'); const ProgressBarPlugin = require('progress-bar-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'); const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const HtmlPlugin = require('html-webpack-plugin'); const HtmlPlugin = require('html-webpack-plugin');
const ScriptExtHtmlPlugin = require('script-ext-html-webpack-plugin'); const ScriptExtHtmlPlugin = require('script-ext-html-webpack-plugin');
const ReplacePlugin = require('webpack-plugin-replace');
const CopyPlugin = require('copy-webpack-plugin'); const CopyPlugin = require('copy-webpack-plugin');
const WatchTimestampsPlugin = require('./config/watch-timestamps-plugin'); const WatchTimestampsPlugin = require('./config/watch-timestamps-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const WorkerPlugin = require('worker-plugin'); const WorkerPlugin = require('worker-plugin');
const AutoSWPlugin = require('./config/auto-sw-plugin');
const CrittersPlugin = require('critters-webpack-plugin');
const AssetTemplatePlugin = require('./config/asset-template-plugin');
function readJson (filename) { function readJson (filename) {
return JSON.parse(fs.readFileSync(filename)); return JSON.parse(fs.readFileSync(filename));
} }
const VERSION = readJson('./package.json').version;
module.exports = function (_, env) { module.exports = function (_, env) {
const isProd = env.mode === 'production'; const isProd = env.mode === 'production';
const nodeModules = path.join(__dirname, 'node_modules'); const nodeModules = path.join(__dirname, 'node_modules');
@@ -34,14 +30,12 @@ module.exports = function (_, env) {
return { return {
mode: isProd ? 'production' : 'development', mode: isProd ? 'production' : 'development',
entry: { entry: './src/index',
'first-interaction': './src/index'
},
devtool: isProd ? 'source-map' : 'inline-source-map', devtool: isProd ? 'source-map' : 'inline-source-map',
stats: 'minimal', stats: 'minimal',
output: { output: {
filename: isProd ? '[name].[chunkhash:5].js' : '[name].js', filename: isProd ? '[name].[chunkhash:5].js' : '[name].js',
chunkFilename: '[name].[chunkhash:5].js', chunkFilename: '[name].chunk.[chunkhash:5].js',
path: path.join(__dirname, 'build'), path: path.join(__dirname, 'build'),
publicPath: '/', publicPath: '/',
globalObject: 'self' globalObject: 'self'
@@ -144,6 +138,12 @@ module.exports = function (_, env) {
exclude: nodeModules, exclude: nodeModules,
loader: 'ts-loader' loader: 'ts-loader'
}, },
{
test: /\.jsx?$/,
loader: 'babel-loader',
// Don't respect any Babel RC files found on the filesystem:
options: Object.assign(readJson('.babelrc'), { babelrc: false })
},
{ {
// All the codec files define a global with the same name as their file name. `exports-loader` attaches those to `module.exports`. // All the codec files define a global with the same name as their file name. `exports-loader` attaches those to `module.exports`.
test: /\/codecs\/.*\.js$/, test: /\/codecs\/.*\.js$/,
@@ -154,17 +154,11 @@ module.exports = function (_, env) {
// This is needed to make webpack NOT process wasm files. // This is needed to make webpack NOT process wasm files.
// See https://github.com/webpack/webpack/issues/6725 // See https://github.com/webpack/webpack/issues/6725
type: 'javascript/auto', type: 'javascript/auto',
loader: 'file-loader', loader: 'file-loader'
options: {
name: '[name].[hash:5].[ext]',
},
}, },
{ {
test: /\.(png|svg|jpg|gif)$/, test: /\.(png|svg|jpg|gif)$/,
loader: 'file-loader', loader: 'file-loader'
options: {
name: '[name].[hash:5].[ext]',
},
} }
] ]
}, },
@@ -201,7 +195,7 @@ module.exports = function (_, env) {
// See also: https://twitter.com/wsokra/status/970253245733113856 // See also: https://twitter.com/wsokra/status/970253245733113856
isProd && new MiniCssExtractPlugin({ isProd && new MiniCssExtractPlugin({
filename: '[name].[contenthash:5].css', filename: '[name].[contenthash:5].css',
chunkFilename: '[name].[contenthash:5].css' chunkFilename: '[name].chunk.[contenthash:5].css'
}), }),
new OptimizeCssAssetsPlugin({ new OptimizeCssAssetsPlugin({
@@ -226,7 +220,7 @@ module.exports = function (_, env) {
// For now we're not doing SSR. // For now we're not doing SSR.
new HtmlPlugin({ new HtmlPlugin({
filename: path.join(__dirname, 'build/index.html'), filename: path.join(__dirname, 'build/index.html'),
template: isProd ? '!!prerender-loader?string!src/index.html' : 'src/index.html', template: 'src/index.html',
minify: isProd && { minify: isProd && {
collapseWhitespace: true, collapseWhitespace: true,
removeScriptTypeAttributes: true, removeScriptTypeAttributes: true,
@@ -235,29 +229,32 @@ module.exports = function (_, env) {
removeComments: true removeComments: true
}, },
manifest: readJson('./src/manifest.json'), manifest: readJson('./src/manifest.json'),
inject: 'body', inject: true,
compile: true compile: true
}), }),
new AutoSWPlugin({ version: VERSION }),
isProd && new AssetTemplatePlugin({
template: path.join(__dirname, '_headers.ejs'),
filename: '_headers',
}),
new ScriptExtHtmlPlugin({ new ScriptExtHtmlPlugin({
inline: ['first'] defaultAttribute: 'async'
}), }),
// Inline constants during build, so they can be folded by UglifyJS. // Inline constants during build, so they can be folded by UglifyJS.
new webpack.DefinePlugin({ new webpack.DefinePlugin({
VERSION: JSON.stringify(VERSION),
// We set node.process=false later in this config. // We set node.process=false later in this config.
// Here we make sure if (process && process.foo) still works: // Here we make sure if (process && process.foo) still works:
process: '{}' process: '{}'
}), }),
// Babel embeds helpful error messages into transpiled classes that we don't need in production.
// Here we replace the constructor and message with a static throw, leaving the message to be DCE'd.
// This is useful since it shows the message in SourceMapped code when debugging.
isProd && new ReplacePlugin({
include: /babel-helper$/,
patterns: [{
regex: /throw\s+(?:new\s+)?((?:Type|Reference)?Error)\s*\(/g,
value: (s, type) => `throw 'babel error'; (`
}]
}),
// Copying files via Webpack allows them to be served dynamically by `webpack serve` // Copying files via Webpack allows them to be served dynamically by `webpack serve`
new CopyPlugin([ new CopyPlugin([
{ from: 'src/manifest.json', to: 'manifest.json' }, { from: 'src/manifest.json', to: 'manifest.json' },
@@ -269,31 +266,17 @@ module.exports = function (_, env) {
analyzerMode: 'static', analyzerMode: 'static',
defaultSizes: 'gzip', defaultSizes: 'gzip',
openAnalyzer: false openAnalyzer: false
}),
// Inline Critical CSS (for the intro screen, essentially)
isProd && new CrittersPlugin({
// use <link rel="stylesheet" media="not x" onload="this.media='all'"> hack to load async css:
preload: 'media',
// inline all styles from any stylesheet below this size:
inlineThreshold: 2000,
// don't bother lazy-loading non-critical stylesheets below this size, just inline the non-critical styles too:
minimumExternalSize: 4000,
// don't emit <noscript> external stylesheet links since the app fundamentally requires JS anyway:
noscriptFallback: false,
// inline the tiny data URL fonts we have for the intro screen:
inlineFonts: true,
// (and don't lazy load them):
preloadFonts: false
}) })
].filter(Boolean), // Filter out any falsey plugin array entries. ].filter(Boolean), // Filter out any falsey plugin array entries.
optimization: { optimization: {
minimizer: [ minimizer: [
new TerserPlugin({ new UglifyJsPlugin({
sourceMap: isProd, sourceMap: isProd,
extractComments: 'build/licenses.txt', extractComments: {
terserOptions: { file: 'build/licenses.txt'
},
uglifyOptions: {
compress: { compress: {
inline: 1 inline: 1
}, },