Compare commits

...

16 Commits

Author SHA1 Message Date
Jason Miller
b489d9cd5b other changes 2020-11-12 09:16:45 -05:00
Jason Miller
fc490b48ff codec-impl linking 2020-11-12 09:15:43 -05:00
Jason Miller
e6111be998 Remove decorators and bundler workarounds 2020-08-19 23:41:35 -04:00
Jason Miller
d383fa6b12 Mostly fix production bundling 2020-08-14 19:50:28 -04:00
Jason Miller
37966fad28 lockfile 2020-08-12 18:21:49 -04:00
Jason Miller
038f34b89a Bugfix for application/wasm content-type being served with a charset value 2020-08-12 18:21:38 -04:00
Jason Miller
88a5295e21 bugfix for pointer-tracker package having no CJS export 2020-08-12 18:21:12 -04:00
Jason Miller
c08d255f4d parcel build scripts 2020-08-12 18:20:37 -04:00
Jason Miller
84e5004ec1 wasm file loading as raw URLs 2020-08-12 18:20:00 -04:00
Jason Miller
46785491f0 Babel configuration for TypeScript 2020-08-12 18:19:46 -04:00
Jason Miller
1da9e9d7db Simple HMR (previous setup does not work) 2020-08-12 18:18:49 -04:00
Jason Miller
f2dcd6e246 Service Worker and Web Worker workarounds 2020-08-12 18:18:22 -04:00
Jason Miller
e972f7225a Fix non-Babel-compatible TS decorator usage 2020-08-12 18:17:50 -04:00
Jason Miller
68a5f45432 url: prefix for assets 2020-08-12 18:17:27 -04:00
Jason Miller
33e3eff839 postcss modules config 2020-08-12 18:16:34 -04:00
Jason Miller
3f0e31c529 Use .module.css extension for CSS Modules 2020-08-12 18:14:16 -04:00
63 changed files with 371 additions and 14278 deletions

39
.babelrc Normal file
View File

@@ -0,0 +1,39 @@
{
"presets": [
[
"@babel/preset-typescript",
{
"jsxPragma": "h"
}
],
[
"@parcel/babel-preset-env",
{
"loose": true,
"bugfixes": true
}
]
],
"plugins": [
[
"@babel/plugin-transform-react-jsx",
{
"loose": true,
"pragma": "h",
"pragmaFrag": "Fragment"
}
],
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
],
[
"@babel/plugin-proposal-class-properties",
{
"loose": true
}
]
]
}

3
.gitignore vendored
View File

@@ -4,3 +4,6 @@ node_modules
*.scss.d.ts *.scss.d.ts
*.css.d.ts *.css.d.ts
*.o *.o
.parcel-cache
.cache
/public

9
.parcelrc Normal file
View File

@@ -0,0 +1,9 @@
{
"extends": "@parcel/config-default",
"transformers": {
"*.wasm": ["@parcel/transformer-raw"]
},
"packagers": {
"*.wasm": "@parcel/packager-raw"
}
}

15
.proxyrc.js Normal file
View File

@@ -0,0 +1,15 @@
module.exports = function(app) {
// `app` is an Express instance
app.use(function(req, res, next) {
const sh = res.setHeader;
res.setHeader = function(key, value) {
// remove pointless/incorrect charset from binary responses:
if (/^content-type$/i.test(key)) {
const m = value && value.match(/^(image\/|application\/wasm); charset=.+$/);
if (m) value = m[1];
}
return sh.call(this, key, value);
}
next();
});
};

4
codecs/package.json Normal file
View File

@@ -0,0 +1,4 @@
{
"name": "codecs",
"version": "0.0.0"
}

3
global.d.ts vendored
View File

@@ -1,6 +1,3 @@
declare const __webpack_public_path__: string;
declare const PRERENDER: boolean;
declare interface NodeModule { declare interface NodeModule {
hot: any; hot: any;
} }

14016
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,8 +4,8 @@
"version": "1.11.4", "version": "1.11.4",
"license": "apache-2.0", "license": "apache-2.0",
"scripts": { "scripts": {
"start": "webpack-dev-server --host 0.0.0.0 --hot", "start": "parcel serve src/index.html --no-autoinstall --host 0.0.0.0 --dist-dir build",
"build": "webpack -p", "build": "parcel build src/index.html --no-autoinstall --dist-dir build",
"lint": "tslint -c tslint.json -p tsconfig.json -t verbose", "lint": "tslint -c tslint.json -p tsconfig.json -t verbose",
"lintfix": "tslint -c tslint.json -p tsconfig.json -t verbose --fix 'src/**/*.{ts,tsx,js,jsx}'", "lintfix": "tslint -c tslint.json -p tsconfig.json -t verbose --fix 'src/**/*.{ts,tsx,js,jsx}'",
"sizereport": "sizereport --config" "sizereport": "sizereport --config"
@@ -15,63 +15,37 @@
"pre-commit": "npm run lint" "pre-commit": "npm run lint"
} }
}, },
"dependencies": {
"codecs": "0.0.0"
},
"devDependencies": { "devDependencies": {
"@types/node": "10.14.15", "@types/node": "10.14.15",
"@types/pretty-bytes": "5.1.0", "@types/pretty-bytes": "5.1.0",
"@types/webassembly-js-api": "0.0.3", "@types/webassembly-js-api": "0.0.3",
"@webcomponents/custom-elements": "1.2.4", "@webcomponents/custom-elements": "1.2.4",
"@webpack-cli/serve": "0.1.8",
"assets-webpack-plugin": "3.9.10",
"chalk": "2.4.2",
"chokidar": "3.0.2",
"classnames": "2.2.6", "classnames": "2.2.6",
"clean-webpack-plugin": "1.0.1",
"comlink": "3.1.1", "comlink": "3.1.1",
"copy-webpack-plugin": "5.0.4",
"critters-webpack-plugin": "2.4.0",
"css-loader": "1.0.1",
"ejs": "2.6.2", "ejs": "2.6.2",
"escape-string-regexp": "2.0.0", "escape-string-regexp": "2.0.0",
"exports-loader": "0.7.0",
"file-drop-element": "0.2.0", "file-drop-element": "0.2.0",
"file-loader": "4.2.0",
"gzip-size": "5.1.1", "gzip-size": "5.1.1",
"html-webpack-plugin": "3.2.0",
"husky": "3.0.4", "husky": "3.0.4",
"idb-keyval": "3.2.0", "idb-keyval": "3.2.0",
"linkstate": "1.1.1", "linkstate": "1.1.1",
"loader-utils": "1.2.3",
"mini-css-extract-plugin": "0.8.0",
"minimatch": "3.0.4", "minimatch": "3.0.4",
"node-fetch": "2.6.0", "node-fetch": "2.6.0",
"node-sass": "4.13.0", "node-sass": "4.13.0",
"normalize-path": "^3.0.0", "normalize-path": "^3.0.0",
"optimize-css-assets-webpack-plugin": "5.0.1", "pointer-tracker": "^2.4.0",
"pointer-tracker": "2.0.3",
"preact": "8.4.2", "preact": "8.4.2",
"prerender-loader": "1.3.0",
"pretty-bytes": "5.3.0", "pretty-bytes": "5.3.0",
"progress-bar-webpack-plugin": "1.12.1",
"raw-loader": "3.1.0",
"readdirp": "3.1.2", "readdirp": "3.1.2",
"sass-loader": "7.3.1", "sass": "^1.26.10",
"script-ext-html-webpack-plugin": "2.1.4",
"source-map-loader": "0.2.4",
"style-loader": "1.0.0",
"terser-webpack-plugin": "1.4.1",
"travis-size-report": "1.1.0", "travis-size-report": "1.1.0",
"ts-loader": "6.0.3",
"tslint": "5.19.0", "tslint": "5.19.0",
"tslint-config-airbnb": "5.11.1", "tslint-config-airbnb": "5.11.1",
"tslint-config-semistandard": "8.0.1", "tslint-config-semistandard": "8.0.1",
"tslint-react": "4.0.0", "tslint-react": "4.0.0",
"typed-css-modules": "0.4.2", "typescript": "3.5.3"
"typescript": "3.5.3",
"url-loader": "2.1.0",
"webpack": "4.39.3",
"webpack-bundle-analyzer": "3.4.1",
"webpack-cli": "3.3.4",
"webpack-dev-server": "3.8.0",
"worker-plugin": "3.1.0"
} }
} }

1
src/codec-impl Symbolic link
View File

@@ -0,0 +1 @@
../codecs

View File

@@ -1,6 +1,6 @@
import { builtinDecode, sniffMimeType, canDecodeImage } from '../lib/util'; import { builtinDecode, sniffMimeType, canDecodeImage } from '../lib/util';
import Processor from './processor'; import Processor from './processor';
import webpDataUrl from 'url-loader!./tiny.webp'; import webpDataUrl from 'url:./tiny.webp';
const webPSupported = canDecodeImage(webpDataUrl); const webPSupported = canDecodeImage(webpDataUrl);

View File

@@ -1,6 +1,5 @@
import { h, Component } from 'preact'; import { h, Component } from 'preact';
import { bind } from '../../lib/initial-util'; import * as style from '../../components/Options/style.module.scss';
import * as style from '../../components/Options/style.scss';
import Range from '../../components/range'; import Range from '../../components/range';
interface EncodeOptions { interface EncodeOptions {
@@ -26,8 +25,7 @@ export default function qualityOption(opts: QualityOptionArg = {}) {
} = opts; } = opts;
class QualityOptions extends Component<Props, {}> { class QualityOptions extends Component<Props, {}> {
@bind onChange = (event: Event) => {
onChange(event: Event) {
const el = event.currentTarget as HTMLInputElement; const el = event.currentTarget as HTMLInputElement;
this.props.onChange({ quality: Number(el.value) }); this.props.onChange({ quality: Number(el.value) });
} }

View File

@@ -1,4 +1,4 @@
import { resize } from '../../../codecs/hqx/pkg'; import { resize } from '../../codec-impl/hqx/pkg';
import { HqxOptions } from './processor-meta'; import { HqxOptions } from './processor-meta';
export async function hqx( export async function hqx(

View File

@@ -1,8 +1,7 @@
import { h, Component } from 'preact'; import { h, Component } from 'preact';
import { bind } from '../../lib/initial-util';
import { inputFieldValueAsNumber, konami, preventDefault } from '../../lib/util'; import { inputFieldValueAsNumber, konami, preventDefault } from '../../lib/util';
import { QuantizeOptions } from './processor-meta'; import { QuantizeOptions } from './processor-meta';
import * as style from '../../components/Options/style.scss'; import * as style from '../../components/Options/style.module.scss';
import Expander from '../../components/expander'; import Expander from '../../components/expander';
import Select from '../../components/select'; import Select from '../../components/select';
import Range from '../../components/range'; import Range from '../../components/range';
@@ -27,8 +26,7 @@ export default class QuantizerOptions extends Component<Props, State> {
}); });
} }
@bind onChange = (event: Event) => {
onChange(event: Event) {
const form = (event.currentTarget as HTMLInputElement).closest('form') as HTMLFormElement; const form = (event.currentTarget as HTMLInputElement).closest('form') as HTMLFormElement;
const { options } = this.props; const { options } = this.props;

View File

@@ -1,5 +1,5 @@
import imagequant, { QuantizerModule } from '../../../codecs/imagequant/imagequant'; import imagequant, { QuantizerModule } from '../../codec-impl/imagequant/imagequant';
import wasmUrl from '../../../codecs/imagequant/imagequant.wasm'; import wasmUrl from 'url:../../codec-impl/imagequant/imagequant.wasm';
import { QuantizeOptions } from './processor-meta'; import { QuantizeOptions } from './processor-meta';
import { initEmscriptenModule } from '../util'; import { initEmscriptenModule } from '../util';

View File

@@ -1,5 +1,6 @@
import mozjpeg_enc, { MozJPEGModule } from '../../../codecs/mozjpeg_enc/mozjpeg_enc'; import mozjpeg_enc, { MozJPEGModule } from '../../codec-impl/mozjpeg_enc/mozjpeg_enc';
import wasmUrl from '../../../codecs/mozjpeg_enc/mozjpeg_enc.wasm'; // import wasmUrl from 'url:../../codecs/mozjpeg_enc/mozjpeg_enc.wasm';
import wasmUrl from 'url:../../codec-impl/mozjpeg_enc/mozjpeg_enc.wasm';
import { EncodeOptions } from './encoder-meta'; import { EncodeOptions } from './encoder-meta';
import { initEmscriptenModule } from '../util'; import { initEmscriptenModule } from '../util';

View File

@@ -1,8 +1,7 @@
import { h, Component } from 'preact'; import { h, Component } from 'preact';
import { bind } from '../../lib/initial-util';
import { inputFieldChecked, inputFieldValueAsNumber, preventDefault } from '../../lib/util'; import { inputFieldChecked, inputFieldValueAsNumber, preventDefault } from '../../lib/util';
import { EncodeOptions, MozJpegColorSpace } from './encoder-meta'; import { EncodeOptions, MozJpegColorSpace } from './encoder-meta';
import * as style from '../../components/Options/style.scss'; import * as style from '../../components/Options/style.module.scss';
import Checkbox from '../../components/checkbox'; import Checkbox from '../../components/checkbox';
import Expander from '../../components/expander'; import Expander from '../../components/expander';
import Select from '../../components/select'; import Select from '../../components/select';
@@ -23,8 +22,7 @@ export default class MozJPEGEncoderOptions extends Component<Props, State> {
showAdvanced: false, showAdvanced: false,
}; };
@bind onChange = (event: Event) => {
onChange(event: Event) {
const form = (event.currentTarget as HTMLInputElement).closest('form') as HTMLFormElement; const form = (event.currentTarget as HTMLInputElement).closest('form') as HTMLFormElement;
const { options } = this.props; const { options } = this.props;

View File

@@ -1,4 +1,4 @@
import { optimise } from '../../../codecs/oxipng/pkg'; import { optimise } from '../../codec-impl/oxipng/pkg';
import { EncodeOptions } from './encoder-meta'; import { EncodeOptions } from './encoder-meta';
export async function compress(data: ArrayBuffer, options: EncodeOptions): Promise<ArrayBuffer> { export async function compress(data: ArrayBuffer, options: EncodeOptions): Promise<ArrayBuffer> {

View File

@@ -1,9 +1,8 @@
import { h, Component } from 'preact'; import { h, Component } from 'preact';
import { bind } from '../../lib/initial-util';
import { inputFieldValueAsNumber, preventDefault } from '../../lib/util'; import { inputFieldValueAsNumber, preventDefault } from '../../lib/util';
import { EncodeOptions } from './encoder-meta'; import { EncodeOptions } from './encoder-meta';
import Range from '../../components/range'; import Range from '../../components/range';
import * as style from '../../components/Options/style.scss'; import * as style from '../../components/Options/style.module.scss';
type Props = { type Props = {
options: EncodeOptions; options: EncodeOptions;
@@ -11,8 +10,7 @@ type Props = {
}; };
export default class OxiPNGEncoderOptions extends Component<Props, {}> { export default class OxiPNGEncoderOptions extends Component<Props, {}> {
@bind onChange = (event: Event) => {
onChange(event: Event) {
const form = (event.currentTarget as HTMLInputElement).closest('form') as HTMLFormElement; const form = (event.currentTarget as HTMLInputElement).closest('form') as HTMLFormElement;
const options: EncodeOptions = { const options: EncodeOptions = {

View File

@@ -1,4 +1,5 @@
import { proxy } from 'comlink'; import { proxy } from 'comlink';
import workerURL from 'bundle:./processor-worker/index.ts';
import { QuantizeOptions } from './imagequant/processor-meta'; import { QuantizeOptions } from './imagequant/processor-meta';
import { canvasEncode, blobToArrayBuffer } from '../lib/util'; import { canvasEncode, blobToArrayBuffer } from '../lib/util';
import { EncodeOptions as MozJPEGEncoderOptions } from './mozjpeg/encoder-meta'; import { EncodeOptions as MozJPEGEncoderOptions } from './mozjpeg/encoder-meta';
@@ -16,13 +17,25 @@ import * as browserGIF from './browser-gif/encoder';
import * as browserTIFF from './browser-tiff/encoder'; import * as browserTIFF from './browser-tiff/encoder';
import * as browserJP2 from './browser-jp2/encoder'; import * as browserJP2 from './browser-jp2/encoder';
import * as browserPDF from './browser-pdf/encoder'; import * as browserPDF from './browser-pdf/encoder';
import { bind } from '../lib/initial-util';
type ProcessorWorkerApi = import('./processor-worker').ProcessorWorkerApi; type ProcessorWorkerApi = import('./processor-worker').ProcessorWorkerApi;
/** How long the worker should be idle before terminating. */ /** How long the worker should be idle before terminating. */
const workerTimeout = 10000; const workerTimeout = 10000;
/**
* Decorator that manages the (re)starting of the worker and aborting existing jobs. Not all
* processing jobs require a worker (e.g. the main thread canvas encodes), use the needsWorker
* option to control this.
*/
function processingJob(options: ProcessingJobOptions = {}) {
return (target: Processor, propertyKey: string, descriptor: PropertyDescriptor): void => {
const processingFunc = descriptor.value;
descriptor.value = target.runProcessingJob.bind(target, options, processingFunc);
};
}
interface ProcessingJobOptions { interface ProcessingJobOptions {
needsWorker?: boolean; needsWorker?: boolean;
} }
@@ -41,52 +54,46 @@ export default class Processor {
/** setTimeout ID for killing the worker when idle. */ /** setTimeout ID for killing the worker when idle. */
private _workerTimeoutId: number = 0; private _workerTimeoutId: number = 0;
/** /** @private */
* Decorator that manages the (re)starting of the worker and aborting existing jobs. Not all async _runProcessingJob(options: ProcessingJobOptions, job: Function) {
* processing jobs require a worker (e.g. the main thread canvas encodes), use the needsWorker
* option to control this.
*/
private static _processingJob(options: ProcessingJobOptions = {}) {
const { needsWorker = false } = options; const { needsWorker = false } = options;
return (target: Processor, propertyKey: string, descriptor: PropertyDescriptor): void => { this._latestJobId += 1;
const processingFunc = descriptor.value; const jobId = this._latestJobId;
this.abortCurrent();
descriptor.value = async function (this: Processor, ...args: any[]) { if (needsWorker) self.clearTimeout(this._workerTimeoutId);
this._latestJobId += 1;
const jobId = this._latestJobId;
this.abortCurrent();
if (needsWorker) self.clearTimeout(this._workerTimeoutId); if (!this._worker && needsWorker) {
// worker-loader does magic here.
// @ts-ignore - Typescript doesn't know about the 2nd param to new Worker, and the
// definition can't be overwritten.
this._worker = new Worker(
// './processor-worker',
// new URL('./processor-worker/index.ts', import.meta.url),
workerURL,
{ name: 'processor-worker', type: 'module' },
// { name: 'processor-worker', type: 'module' },
) as Worker;
console.log(this._worker);
// Need to do some TypeScript trickery to make the type match.
this._workerApi = proxy(this._worker) as any as ProcessorWorkerApi;
}
if (!this._worker && needsWorker) { this._busy = true;
// worker-loader does magic here.
// @ts-ignore - Typescript doesn't know about the 2nd param to new Worker, and the
// definition can't be overwritten.
this._worker = new Worker(
'./processor-worker',
{ name: 'processor-worker', type: 'module' },
) as Worker;
// Need to do some TypeScript trickery to make the type match.
this._workerApi = proxy(this._worker) as any as ProcessorWorkerApi;
}
this._busy = true; const returnVal = Promise.race([
job(),
new Promise((_, reject) => { this._abortRejector = reject; }),
]);
const returnVal = Promise.race([ // Wait for the operation to settle.
processingFunc.call(this, ...args), await returnVal.catch(() => {});
new Promise((_, reject) => { this._abortRejector = reject; }),
]);
// Wait for the operation to settle. // If no other jobs are happening, cleanup.
await returnVal.catch(() => {}); if (jobId === this._latestJobId) this._jobCleanup();
// If no other jobs are happening, cleanup. return returnVal;
if (jobId === this._latestJobId) this._jobCleanup();
return returnVal;
};
};
} }
private _jobCleanup(): void { private _jobCleanup(): void {
@@ -95,7 +102,7 @@ export default class Processor {
if (!this._worker) return; if (!this._worker) return;
// If the worker is unused for 10 seconds, remove it to save memory. // If the worker is unused for 10 seconds, remove it to save memory.
this._workerTimeoutId = self.setTimeout(this.terminateWorker, workerTimeout); this._workerTimeoutId = self.setTimeout(this.terminateWorker.bind(this), workerTimeout);
} }
/** Abort the current job, if any */ /** Abort the current job, if any */
@@ -108,7 +115,6 @@ export default class Processor {
this.terminateWorker(); this.terminateWorker();
} }
@bind
terminateWorker() { terminateWorker() {
if (!this._worker) return; if (!this._worker) return;
this._worker.terminate(); this._worker.terminate();
@@ -116,93 +122,108 @@ export default class Processor {
} }
// Off main thread jobs: // Off main thread jobs:
@Processor._processingJob({ needsWorker: true })
imageQuant(data: ImageData, opts: QuantizeOptions): Promise<ImageData> { imageQuant(data: ImageData, opts: QuantizeOptions): Promise<ImageData> {
return this._workerApi!.quantize(data, opts); return this._runProcessingJob({ needsWorker: true }, () => {
return this._workerApi!.quantize(data, opts);
});
} }
@Processor._processingJob({ needsWorker: true })
rotate( rotate(
data: ImageData, opts: import('./rotate/processor-meta').RotateOptions, data: ImageData, opts: import('./rotate/processor-meta').RotateOptions,
): Promise<ImageData> { ): Promise<ImageData> {
return this._workerApi!.rotate(data, opts); return this._runProcessingJob({ needsWorker: true }, () => {
return this._workerApi!.rotate(data, opts);
});
} }
@Processor._processingJob({ needsWorker: true })
workerResize( workerResize(
data: ImageData, opts: import('./resize/processor-meta').WorkerResizeOptions, data: ImageData, opts: import('./resize/processor-meta').WorkerResizeOptions,
): Promise<ImageData> { ): Promise<ImageData> {
return this._workerApi!.resize(data, opts); return this._runProcessingJob({ needsWorker: true }, () => {
return this._workerApi!.resize(data, opts);
});
} }
@Processor._processingJob({ needsWorker: true })
mozjpegEncode( mozjpegEncode(
data: ImageData, opts: MozJPEGEncoderOptions, data: ImageData, opts: MozJPEGEncoderOptions,
): Promise<ArrayBuffer> { ): Promise<ArrayBuffer> {
return this._workerApi!.mozjpegEncode(data, opts); return this._runProcessingJob({ needsWorker: true }, () => {
return this._workerApi!.mozjpegEncode(data, opts);
});
} }
@Processor._processingJob({ needsWorker: true })
async oxiPngEncode( async oxiPngEncode(
data: ImageData, opts: OxiPNGEncoderOptions, data: ImageData, opts: OxiPNGEncoderOptions,
): Promise<ArrayBuffer> { ): Promise<ArrayBuffer> {
// OxiPNG expects PNG input. return this._runProcessingJob({ needsWorker: true }, async () => {
const pngBlob = await canvasEncode(data, 'image/png'); // OxiPNG expects PNG input.
const pngBuffer = await blobToArrayBuffer(pngBlob); const pngBlob = await canvasEncode(data, 'image/png');
return this._workerApi!.oxiPngEncode(pngBuffer, opts); const pngBuffer = await blobToArrayBuffer(pngBlob);
return this._workerApi!.oxiPngEncode(pngBuffer, opts);
});
} }
@Processor._processingJob({ needsWorker: true })
webpEncode(data: ImageData, opts: WebPEncoderOptions): Promise<ArrayBuffer> { webpEncode(data: ImageData, opts: WebPEncoderOptions): Promise<ArrayBuffer> {
return this._workerApi!.webpEncode(data, opts); return this._runProcessingJob({ needsWorker: true }, () => {
return this._workerApi!.webpEncode(data, opts);
});
} }
@Processor._processingJob({ needsWorker: true })
async webpDecode(blob: Blob): Promise<ImageData> { async webpDecode(blob: Blob): Promise<ImageData> {
const data = await blobToArrayBuffer(blob); return this._runProcessingJob({ needsWorker: true }, async () => {
return this._workerApi!.webpDecode(data); const data = await blobToArrayBuffer(blob);
return this._workerApi!.webpDecode(data);
});
} }
// Not-worker jobs: // Not-worker jobs:
@Processor._processingJob()
browserBmpEncode(data: ImageData): Promise<Blob> { browserBmpEncode(data: ImageData): Promise<Blob> {
return browserBMP.encode(data); return this._runProcessingJob({}, () => {
return browserBMP.encode(data);
});
} }
@Processor._processingJob()
browserPngEncode(data: ImageData): Promise<Blob> { browserPngEncode(data: ImageData): Promise<Blob> {
return browserPNG.encode(data); return this._runProcessingJob({}, () => {
return browserPNG.encode(data);
});
} }
@Processor._processingJob()
browserJpegEncode(data: ImageData, opts: BrowserJPEGOptions): Promise<Blob> { browserJpegEncode(data: ImageData, opts: BrowserJPEGOptions): Promise<Blob> {
return browserJPEG.encode(data, opts); return this._runProcessingJob({}, () => {
return browserJPEG.encode(data, opts);
});
} }
@Processor._processingJob()
browserWebpEncode(data: ImageData, opts: BrowserWebpEncodeOptions): Promise<Blob> { browserWebpEncode(data: ImageData, opts: BrowserWebpEncodeOptions): Promise<Blob> {
return browserWebP.encode(data, opts); return this._runProcessingJob({}, () => {
return browserWebP.encode(data, opts);
});
} }
@Processor._processingJob()
browserGifEncode(data: ImageData): Promise<Blob> { browserGifEncode(data: ImageData): Promise<Blob> {
return browserGIF.encode(data); return this._runProcessingJob({}, () => {
return browserGIF.encode(data);
});
} }
@Processor._processingJob()
browserTiffEncode(data: ImageData): Promise<Blob> { browserTiffEncode(data: ImageData): Promise<Blob> {
return browserTIFF.encode(data); return this._runProcessingJob({}, () => {
return browserTIFF.encode(data);
});
} }
@Processor._processingJob()
browserJp2Encode(data: ImageData): Promise<Blob> { browserJp2Encode(data: ImageData): Promise<Blob> {
return browserJP2.encode(data); return this._runProcessingJob({}, () => {
return browserJP2.encode(data);
});
} }
@Processor._processingJob()
browserPdfEncode(data: ImageData): Promise<Blob> { browserPdfEncode(data: ImageData): Promise<Blob> {
return browserPDF.encode(data); return this._runProcessingJob({}, () => {
return browserPDF.encode(data);
});
} }
// Synchronous jobs // Synchronous jobs

View File

@@ -1,11 +1,11 @@
import { h, Component } from 'preact'; import { h, Component } from 'preact';
import linkState from 'linkstate'; import linkState from 'linkstate';
import { bind, linkRef } from '../../lib/initial-util'; import { linkRef } from '../../lib/initial-util';
import { import {
inputFieldValueAsNumber, inputFieldValue, preventDefault, inputFieldChecked, inputFieldValueAsNumber, inputFieldValue, preventDefault, inputFieldChecked,
} from '../../lib/util'; } from '../../lib/util';
import { ResizeOptions, isWorkerOptions } from './processor-meta'; import { ResizeOptions, isWorkerOptions } from './processor-meta';
import * as style from '../../components/Options/style.scss'; import * as style from '../../components/Options/style.module.scss';
import Checkbox from '../../components/checkbox'; import Checkbox from '../../components/checkbox';
import Expander from '../../components/expander'; import Expander from '../../components/expander';
import Select from '../../components/select'; import Select from '../../components/select';
@@ -58,8 +58,7 @@ export default class ResizerOptions extends Component<Props, State> {
this.props.onChange(newOptions); this.props.onChange(newOptions);
} }
@bind private onChange = () => {
private onChange() {
this.reportOptions(); this.reportOptions();
} }
@@ -83,8 +82,7 @@ export default class ResizerOptions extends Component<Props, State> {
} }
} }
@bind private onWidthInput = () => {
private onWidthInput() {
if (this.state.maintainAspect) { if (this.state.maintainAspect) {
const width = inputFieldValueAsNumber(this.form!.width); const width = inputFieldValueAsNumber(this.form!.width);
this.form!.height.value = Math.round(width / this.getAspect()); this.form!.height.value = Math.round(width / this.getAspect());
@@ -93,8 +91,7 @@ export default class ResizerOptions extends Component<Props, State> {
this.reportOptions(); this.reportOptions();
} }
@bind private onHeightInput = () => {
private onHeightInput() {
if (this.state.maintainAspect) { if (this.state.maintainAspect) {
const height = inputFieldValueAsNumber(this.form!.height); const height = inputFieldValueAsNumber(this.form!.height);
this.form!.width.value = Math.round(height * this.getAspect()); this.form!.width.value = Math.round(height * this.getAspect());
@@ -123,8 +120,7 @@ export default class ResizerOptions extends Component<Props, State> {
return 'custom'; return 'custom';
} }
@bind private onPresetChange = (event: Event) => {
private onPresetChange(event: Event) {
const select = event.target as HTMLSelectElement; const select = event.target as HTMLSelectElement;
if (select.value === 'custom') return; if (select.value === 'custom') return;
const multiplier = Number(select.value); const multiplier = Number(select.value);

View File

@@ -1,4 +1,4 @@
import wasmUrl from '../../../codecs/rotate/rotate.wasm'; import wasmUrl from 'url:../../codec-impl/rotate/rotate.wasm';
import { RotateOptions, RotateModuleInstance } from './processor-meta'; import { RotateOptions, RotateModuleInstance } from './processor-meta';
// We are loading a 500B module here. Loading the code to feature-detect // We are loading a 500B module here. Loading the code to feature-detect

View File

@@ -1,5 +1,5 @@
import webp_dec, { WebPModule } from '../../../codecs/webp/dec/webp_dec'; import webp_dec, { WebPModule } from '../../codec-impl/webp/dec/webp_dec';
import wasmUrl from '../../../codecs/webp/dec/webp_dec.wasm'; import wasmUrl from 'url:../../codec-impl/webp/dec/webp_dec.wasm';
import { initEmscriptenModule } from '../util'; import { initEmscriptenModule } from '../util';
let emscriptenModule: Promise<WebPModule>; let emscriptenModule: Promise<WebPModule>;

View File

@@ -1,5 +1,5 @@
import webp_enc, { WebPModule } from '../../../codecs/webp/enc/webp_enc'; import webp_enc, { WebPModule } from '../../codec-impl/webp/enc/webp_enc';
import wasmUrl from '../../../codecs/webp/enc/webp_enc.wasm'; import wasmUrl from 'url:../../codec-impl/webp/enc/webp_enc.wasm';
import { EncodeOptions } from './encoder-meta'; import { EncodeOptions } from './encoder-meta';
import { initEmscriptenModule } from '../util'; import { initEmscriptenModule } from '../util';

View File

@@ -1,8 +1,7 @@
import { h, Component } from 'preact'; import { h, Component } from 'preact';
import { bind } from '../../lib/initial-util';
import { inputFieldCheckedAsNumber, inputFieldValueAsNumber, preventDefault } from '../../lib/util'; import { inputFieldCheckedAsNumber, inputFieldValueAsNumber, preventDefault } from '../../lib/util';
import { EncodeOptions, WebPImageHint } from './encoder-meta'; import { EncodeOptions, WebPImageHint } from './encoder-meta';
import * as style from '../../components/Options/style.scss'; import * as style from '../../components/Options/style.module.scss';
import Checkbox from '../../components/checkbox'; import Checkbox from '../../components/checkbox';
import Expander from '../../components/expander'; import Expander from '../../components/expander';
import Select from '../../components/select'; import Select from '../../components/select';
@@ -41,8 +40,7 @@ export default class WebPEncoderOptions extends Component<Props, State> {
showAdvanced: false, showAdvanced: false,
}; };
@bind onChange = (event: Event) => {
onChange(event: Event) {
const form = (event.currentTarget as HTMLInputElement).closest('form') as HTMLFormElement; const form = (event.currentTarget as HTMLInputElement).closest('form') as HTMLFormElement;
const lossless = inputFieldCheckedAsNumber(form.lossless); const lossless = inputFieldCheckedAsNumber(form.lossless);
const { options } = this.props; const { options } = this.props;

View File

@@ -1,22 +1,20 @@
import { h, Component } from 'preact'; import { h, Component } from 'preact';
import { bind, linkRef, Fileish } from '../../lib/initial-util'; import { linkRef, Fileish } from '../../lib/initial-util';
import * as style from './style.scss'; import * as style from './style.module.scss';
import { FileDropEvent } from 'file-drop-element'; import { FileDropEvent } from 'file-drop-element';
import 'file-drop-element'; import 'file-drop-element';
import SnackBarElement, { SnackOptions } from '../../lib/SnackBar'; import SnackBarElement, { SnackOptions } from '../../lib/SnackBar/index';
import '../../lib/SnackBar'; import '../../lib/SnackBar/index';
import Intro from '../intro'; import Intro from '../intro/index';
import '../custom-els/LoadingSpinner'; import '../custom-els/LoadingSpinner/index';
const ROUTE_EDITOR = '/editor'; const ROUTE_EDITOR = '/editor';
const compressPromise = import( const compressPromise = import(
/* webpackChunkName: "main-app" */
'../compress'); '../compress');
const swBridgePromise = import( const swBridgePromise = import(
/* webpackChunkName: "sw-bridge" */
'../../lib/sw-bridge'); '../../lib/sw-bridge');
function back() { function back() {
@@ -45,10 +43,17 @@ export default class App extends Component<Props, State> {
constructor() { constructor() {
super(); super();
this.onFileDrop = this.onFileDrop.bind(this);
this.onIntroPickFile = this.onIntroPickFile.bind(this);
this.showSnack = this.showSnack.bind(this);
this.onPopState = this.onPopState.bind(this);
this.openEditor = this.openEditor.bind(this);
compressPromise.then((module) => { compressPromise.then((module) => {
this.setState({ Compress: module.default }); this.setState({ Compress: module.default });
}).catch(() => { }).catch((e) => {
this.showSnack('Failed to load app'); this.showSnack('Failed to load app');
throw e;
}); });
swBridgePromise.then(async ({ offliner, getSharedImage }) => { swBridgePromise.then(async ({ offliner, getSharedImage }) => {
@@ -62,7 +67,8 @@ export default class App extends Component<Props, State> {
}); });
// 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') {
if (module.hot) {
this.setState(window.STATE); this.setState(window.STATE);
const oldCDU = this.componentDidUpdate; const oldCDU = this.componentDidUpdate;
this.componentDidUpdate = (props, state, prev) => { this.componentDidUpdate = (props, state, prev) => {
@@ -82,7 +88,6 @@ export default class App extends Component<Props, State> {
window.addEventListener('popstate', this.onPopState); window.addEventListener('popstate', this.onPopState);
} }
@bind
private onFileDrop({ files }: FileDropEvent) { private onFileDrop({ files }: FileDropEvent) {
if (!files || files.length === 0) return; if (!files || files.length === 0) return;
const file = files[0]; const file = files[0];
@@ -90,24 +95,20 @@ export default class App extends Component<Props, State> {
this.setState({ file }); this.setState({ file });
} }
@bind
private onIntroPickFile(file: File | Fileish) { private onIntroPickFile(file: File | Fileish) {
this.openEditor(); this.openEditor();
this.setState({ file }); this.setState({ file });
} }
@bind
private showSnack(message: string, options: SnackOptions = {}): Promise<string> { private showSnack(message: string, options: SnackOptions = {}): Promise<string> {
if (!this.snackbar) throw Error('Snackbar missing'); if (!this.snackbar) throw Error('Snackbar missing');
return this.snackbar.showSnackbar(message, options); return this.snackbar.showSnackbar(message, options);
} }
@bind
private onPopState() { private onPopState() {
this.setState({ isEditorOpen: location.pathname === ROUTE_EDITOR }); this.setState({ isEditorOpen: location.pathname === ROUTE_EDITOR });
} }
@bind
private openEditor() { private openEditor() {
if (this.state.isEditorOpen) return; if (this.state.isEditorOpen) return;
// Change path, but preserve query string. // Change path, but preserve query string.

View File

@@ -24,7 +24,7 @@
right: 10px; right: 10px;
bottom: 10px; bottom: 10px;
border: 2px dashed #fff; border: 2px dashed #fff;
background-color:rgba(88, 116, 88, 0.2); background-color: rgba(88, 116, 88, 0.2);
border-color: rgba(65, 129, 65, 0.5); border-color: rgba(65, 129, 65, 0.5);
border-radius: 10px; border-radius: 10px;
opacity: 0; opacity: 0;

View File

@@ -1,7 +1,6 @@
import { h, Component } from 'preact'; import { h, Component } from 'preact';
import * as style from './style.scss'; import * as style from './style.module.scss';
import { bind } from '../../lib/initial-util';
import { cleanSet, cleanMerge } from '../../lib/clean-modify'; import { cleanSet, cleanMerge } from '../../lib/clean-modify';
import OxiPNGEncoderOptions from '../../codecs/oxipng/options'; import OxiPNGEncoderOptions from '../../codecs/oxipng/options';
import MozJpegEncoderOptions from '../../codecs/mozjpeg/options'; import MozJpegEncoderOptions from '../../codecs/mozjpeg/options';
@@ -82,8 +81,7 @@ export default class Options extends Component<Props, State> {
encodersSupported.then(encoderSupportMap => this.setState({ encoderSupportMap })); encodersSupported.then(encoderSupportMap => this.setState({ encoderSupportMap }));
} }
@bind private onEncoderTypeChange = (event: Event) => {
private onEncoderTypeChange(event: Event) {
const el = event.currentTarget as HTMLSelectElement; const el = event.currentTarget as HTMLSelectElement;
// The select element only has values matching encoder types, // The select element only has values matching encoder types,
@@ -92,8 +90,7 @@ export default class Options extends Component<Props, State> {
this.props.onEncoderTypeChange(type); this.props.onEncoderTypeChange(type);
} }
@bind private onPreprocessorEnabledChange = (event: Event) => {
private onPreprocessorEnabledChange(event: Event) {
const el = event.currentTarget as HTMLInputElement; const el = event.currentTarget as HTMLInputElement;
const preprocessor = el.name.split('.')[0] as keyof PreprocessorState; const preprocessor = el.name.split('.')[0] as keyof PreprocessorState;
@@ -102,15 +99,13 @@ export default class Options extends Component<Props, State> {
); );
} }
@bind private onQuantizerOptionsChange = (opts: QuantizeOptions) => {
private onQuantizerOptionsChange(opts: QuantizeOptions) {
this.props.onPreprocessorOptionsChange( this.props.onPreprocessorOptionsChange(
cleanMerge(this.props.preprocessorState, 'quantizer', opts), cleanMerge(this.props.preprocessorState, 'quantizer', opts),
); );
} }
@bind private onResizeOptionsChange = (opts: ResizeOptions) => {
private onResizeOptionsChange(opts: ResizeOptions) {
this.props.onPreprocessorOptionsChange( this.props.onPreprocessorOptionsChange(
cleanMerge(this.props.preprocessorState, 'resize', opts), cleanMerge(this.props.preprocessorState, 'resize', opts),
); );

View File

@@ -1,5 +1,5 @@
import PointerTracker, { Pointer } from 'pointer-tracker'; import PointerTracker, { Pointer } from 'pointer-tracker';
import * as styles from './styles.css'; import * as styles from './styles.module.css';
const legacyClipCompatAttr = 'legacy-clip-compat'; const legacyClipCompatAttr = 'legacy-clip-compat';
const orientationAttr = 'orientation'; const orientationAttr = 'orientation';

View File

@@ -2,8 +2,8 @@ import { h, Component } from 'preact';
import PinchZoom, { ScaleToOpts } from './custom-els/PinchZoom'; import PinchZoom, { ScaleToOpts } from './custom-els/PinchZoom';
import './custom-els/PinchZoom'; import './custom-els/PinchZoom';
import './custom-els/TwoUp'; import './custom-els/TwoUp';
import * as style from './style.scss'; import * as style from './style.module.scss';
import { bind, linkRef } from '../../lib/initial-util'; import { linkRef } from '../../lib/initial-util';
import { shallowEqual, drawDataToCanvas } from '../../lib/util'; import { shallowEqual, drawDataToCanvas } from '../../lib/util';
import { import {
ToggleBackgroundIcon, ToggleBackgroundIcon,
@@ -13,7 +13,7 @@ import {
ToggleBackgroundActiveIcon, ToggleBackgroundActiveIcon,
RotateIcon, RotateIcon,
} from '../../lib/icons'; } from '../../lib/icons';
import { twoUpHandle } from './custom-els/TwoUp/styles.css'; import { twoUpHandle } from './custom-els/TwoUp/styles.module.css';
import { InputProcessorState } from '../../codecs/input-processors'; import { InputProcessorState } from '../../codecs/input-processors';
import { cleanSet } from '../../lib/clean-modify'; import { cleanSet } from '../../lib/clean-modify';
import { SourceImage } from '../compress'; import { SourceImage } from '../compress';
@@ -135,29 +135,25 @@ export default class Output extends Component<Props, State> {
return props.rightCompressed || (props.source && props.source.processed); return props.rightCompressed || (props.source && props.source.processed);
} }
@bind private toggleBackground = () => {
private toggleBackground() {
this.setState({ this.setState({
altBackground: !this.state.altBackground, altBackground: !this.state.altBackground,
}); });
} }
@bind private zoomIn = () => {
private zoomIn() {
if (!this.pinchZoomLeft) throw Error('Missing pinch-zoom element'); if (!this.pinchZoomLeft) throw Error('Missing pinch-zoom element');
this.pinchZoomLeft.scaleTo(this.state.scale * 1.25, scaleToOpts); this.pinchZoomLeft.scaleTo(this.state.scale * 1.25, scaleToOpts);
} }
@bind private zoomOut = () => {
private zoomOut() {
if (!this.pinchZoomLeft) throw Error('Missing pinch-zoom element'); if (!this.pinchZoomLeft) throw Error('Missing pinch-zoom element');
this.pinchZoomLeft.scaleTo(this.state.scale / 1.25, scaleToOpts); this.pinchZoomLeft.scaleTo(this.state.scale / 1.25, scaleToOpts);
} }
@bind private onRotateClick = () => {
private onRotateClick() {
const { inputProcessorState } = this.props; const { inputProcessorState } = this.props;
if (!inputProcessorState) return; if (!inputProcessorState) return;
@@ -170,8 +166,7 @@ export default class Output extends Component<Props, State> {
this.props.onInputProcessorChange(newState); this.props.onInputProcessorChange(newState);
} }
@bind private onScaleValueFocus = () => {
private onScaleValueFocus() {
this.setState({ editingScale: true }, () => { this.setState({ editingScale: true }, () => {
if (this.scaleInput) { if (this.scaleInput) {
// Firefox unfocuses the input straight away unless I force a style calculation here. I have // Firefox unfocuses the input straight away unless I force a style calculation here. I have
@@ -182,13 +177,11 @@ export default class Output extends Component<Props, State> {
}); });
} }
@bind private onScaleInputBlur = () => {
private onScaleInputBlur() {
this.setState({ editingScale: false }); this.setState({ editingScale: false });
} }
@bind private onScaleInputChanged = (event: Event) => {
private onScaleInputChanged(event: Event) {
const target = event.target as HTMLInputElement; const target = event.target as HTMLInputElement;
const percent = parseFloat(target.value); const percent = parseFloat(target.value);
if (isNaN(percent)) return; if (isNaN(percent)) return;
@@ -197,8 +190,7 @@ export default class Output extends Component<Props, State> {
this.pinchZoomLeft.scaleTo(percent / 100, scaleToOpts); this.pinchZoomLeft.scaleTo(percent / 100, scaleToOpts);
} }
@bind private onPinchZoomLeftChange = (event: Event) => {
private onPinchZoomLeftChange(event: Event) {
if (!this.pinchZoomRight || !this.pinchZoomLeft) throw Error('Missing pinch-zoom element'); if (!this.pinchZoomRight || !this.pinchZoomLeft) throw Error('Missing pinch-zoom element');
this.setState({ this.setState({
scale: this.pinchZoomLeft.scale, scale: this.pinchZoomLeft.scale,
@@ -218,8 +210,7 @@ export default class Output extends Component<Props, State> {
* *
* @param event Event to redirect * @param event Event to redirect
*/ */
@bind private onRetargetableEvent = (event: Event) => {
private onRetargetableEvent(event: Event) {
const targetEl = event.target as HTMLElement; const targetEl = event.target as HTMLElement;
if (!this.pinchZoomLeft) throw Error('Missing pinch-zoom element'); if (!this.pinchZoomLeft) throw Error('Missing pinch-zoom element');
// If the event is on the handle of the two-up, let it through, // If the event is on the handle of the two-up, let it through,

View File

@@ -1,5 +1,5 @@
import { h, Component } from 'preact'; import { h, Component } from 'preact';
import * as style from './style.scss'; import * as style from './style.module.scss';
import { UncheckedIcon, CheckedIcon } from '../../lib/icons'; import { UncheckedIcon, CheckedIcon } from '../../lib/icons';
interface Props extends JSX.HTMLAttributes {} interface Props extends JSX.HTMLAttributes {}

View File

@@ -1,4 +1,4 @@
import * as style from './styles.css'; import * as style from './styles.module.css';
import { transitionHeight } from '../../../../lib/util'; import { transitionHeight } from '../../../../lib/util';
interface CloseAllOptions { interface CloseAllOptions {

View File

@@ -1,8 +1,8 @@
import { h, Component } from 'preact'; import { h, Component } from 'preact';
import { bind, Fileish } from '../../lib/initial-util'; import { Fileish } from '../../lib/initial-util';
import { blobToImg, drawableToImageData, blobToText } from '../../lib/util'; import { blobToImg, drawableToImageData, blobToText } from '../../lib/util';
import * as style from './style.scss'; import * as style from './style.module.scss';
import Output from '../Output'; import Output from '../Output';
import Options from '../Options'; import Options from '../Options';
import ResultCache from './result-cache'; import ResultCache from './result-cache';
@@ -254,8 +254,7 @@ export default class Compress extends Component<Props, State> {
import('../../lib/sw-bridge').then(({ mainAppLoaded }) => mainAppLoaded()); import('../../lib/sw-bridge').then(({ mainAppLoaded }) => mainAppLoaded());
} }
@bind private onMobileWidthChange = () => {
private onMobileWidthChange() {
this.setState({ mobileView: this.widthQuery.matches }); this.setState({ mobileView: this.widthQuery.matches });
} }
@@ -344,8 +343,7 @@ export default class Compress extends Component<Props, State> {
}); });
} }
@bind private onInputProcessorChange = async (options: InputProcessorState): Promise<void> => {
private async onInputProcessorChange(options: InputProcessorState): Promise<void> {
const source = this.state.source; const source = this.state.source;
if (!source) return; if (!source) return;
@@ -396,8 +394,7 @@ export default class Compress extends Component<Props, State> {
} }
} }
@bind private updateFile = async (file: File | Fileish) => {
private async updateFile(file: File | Fileish) {
const loadingCounter = this.state.loadingCounter + 1; const loadingCounter = this.state.loadingCounter + 1;
// Either processor is good enough here. // Either processor is good enough here.
const processor = this.leftProcessor; const processor = this.leftProcessor;

View File

@@ -1,4 +1,4 @@
import * as styles from './styles.css'; import * as styles from './styles.module.css';
/** /**
* A simple spinner. This custom element has no JS API. Just put it in the document, and it'll * A simple spinner. This custom element has no JS API. Just put it in the document, and it'll

View File

@@ -1,5 +1,5 @@
import { h, Component, ComponentChild, ComponentChildren } from 'preact'; import { h, Component, ComponentChild, ComponentChildren } from 'preact';
import * as style from './style.scss'; import * as style from './style.module.scss';
import { transitionHeight } from '../../lib/util'; import { transitionHeight } from '../../lib/util';
interface Props { interface Props {

View File

@@ -1,17 +1,17 @@
import { h, Component } from 'preact'; import { h, Component } from 'preact';
import { bind, linkRef, Fileish } from '../../lib/initial-util'; import { linkRef, Fileish } from '../../lib/initial-util';
import '../custom-els/LoadingSpinner'; import '../custom-els/LoadingSpinner';
import logo from './imgs/logo.svg'; import logo from 'url:./imgs/logo.svg';
import largePhoto from './imgs/demos/demo-large-photo.jpg'; import largePhoto from 'url:./imgs/demos/demo-large-photo.jpg';
import artwork from './imgs/demos/demo-artwork.jpg'; import artwork from 'url:./imgs/demos/demo-artwork.jpg';
import deviceScreen from './imgs/demos/demo-device-screen.png'; import deviceScreen from 'url:./imgs/demos/demo-device-screen.png';
import largePhotoIcon from './imgs/demos/icon-demo-large-photo.jpg'; import largePhotoIcon from 'url:./imgs/demos/icon-demo-large-photo.jpg';
import artworkIcon from './imgs/demos/icon-demo-artwork.jpg'; import artworkIcon from 'url:./imgs/demos/icon-demo-artwork.jpg';
import deviceScreenIcon from './imgs/demos/icon-demo-device-screen.jpg'; import deviceScreenIcon from 'url:./imgs/demos/icon-demo-device-screen.jpg';
import logoIcon from './imgs/demos/icon-demo-logo.png'; import logoIcon from 'url:./imgs/demos/icon-demo-logo.png';
import * as style from './style.scss'; import * as style from './style.module.scss';
import SnackBarElement from '../../lib/SnackBar'; import SnackBarElement from '../../lib/SnackBar';
const demos = [ const demos = [
@@ -67,13 +67,11 @@ export default class Intro extends Component<Props, State> {
window.addEventListener('appinstalled', this.onAppInstalled); window.addEventListener('appinstalled', this.onAppInstalled);
} }
@bind private resetFileInput = () => {
private resetFileInput() {
this.fileInput!.value = ''; this.fileInput!.value = '';
} }
@bind private onFileChange = (event: Event) => {
private onFileChange(event: Event): void {
const fileInput = event.target as HTMLInputElement; const fileInput = event.target as HTMLInputElement;
const file = fileInput.files && fileInput.files[0]; const file = fileInput.files && fileInput.files[0];
if (!file) return; if (!file) return;
@@ -81,13 +79,11 @@ export default class Intro extends Component<Props, State> {
this.props.onFile(file); this.props.onFile(file);
} }
@bind private onButtonClick = () => {
private onButtonClick() {
this.fileInput!.click(); this.fileInput!.click();
} }
@bind private onDemoClick = async (index: number, event: Event) => {
private async onDemoClick(index: number, event: Event) {
try { try {
this.setState({ fetchingDemoIndex: index }); this.setState({ fetchingDemoIndex: index });
const demo = demos[index]; const demo = demos[index];
@@ -104,8 +100,7 @@ export default class Intro extends Component<Props, State> {
} }
} }
@bind private onBeforeInstallPromptEvent = (event: BeforeInstallPromptEvent) => {
private onBeforeInstallPromptEvent(event: BeforeInstallPromptEvent) {
// Don't show the mini-infobar on mobile // Don't show the mini-infobar on mobile
event.preventDefault(); event.preventDefault();
@@ -121,8 +116,7 @@ export default class Intro extends Component<Props, State> {
ga('send', 'event', gaEventInfo); ga('send', 'event', gaEventInfo);
} }
@bind private onInstallClick = async (event: Event) => {
private async onInstallClick(event: Event) {
// Get the deferred beforeinstallprompt event // Get the deferred beforeinstallprompt event
const beforeInstallEvent = this.state.beforeInstallEvent; const beforeInstallEvent = this.state.beforeInstallEvent;
// If there's no deferred prompt, bail. // If there's no deferred prompt, bail.
@@ -150,8 +144,7 @@ export default class Intro extends Component<Props, State> {
} }
} }
@bind private onAppInstalled = () => {
private onAppInstalled() {
// We don't need the install button, if it's shown // We don't need the install button, if it's shown
this.setState({ beforeInstallEvent: undefined }); this.setState({ beforeInstallEvent: undefined });

View File

@@ -1,8 +1,8 @@
import { h, Component } from 'preact'; import { h, Component } from 'preact';
import * as style from './style.scss'; import * as style from './style.module.scss';
import RangeInputElement from '../../custom-els/RangeInput'; import RangeInputElement from '../../custom-els/RangeInput';
import '../../custom-els/RangeInput'; import '../../custom-els/RangeInput';
import { linkRef, bind } from '../../lib/initial-util'; import { linkRef } from '../../lib/initial-util';
interface Props extends JSX.HTMLAttributes {} interface Props extends JSX.HTMLAttributes {}
interface State {} interface State {}
@@ -10,8 +10,7 @@ interface State {}
export default class Range extends Component<Props, State> { export default class Range extends Component<Props, State> {
rangeWc?: RangeInputElement; rangeWc?: RangeInputElement;
@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(); const value = input.value.trim();
if (!value) return; if (!value) return;

View File

@@ -1,6 +1,6 @@
import { h, Component } from 'preact'; import { h, Component } from 'preact';
import * as prettyBytes from 'pretty-bytes'; import prettyBytes from 'pretty-bytes';
import * as style from './style.scss'; import * as style from './style.module.scss';
interface Props { interface Props {
blob: Blob; blob: Blob;

View File

@@ -1,11 +1,11 @@
import { h, Component, ComponentChildren, ComponentChild } from 'preact'; import { h, Component, ComponentChildren, ComponentChild } from 'preact';
import * as style from './style.scss'; import * as style from './style.module.scss';
import FileSize from './FileSize'; import FileSize from './FileSize';
import { DownloadIcon, CopyAcrossIcon, CopyAcrossIconProps } from '../../lib/icons'; import { DownloadIcon, CopyAcrossIcon, CopyAcrossIconProps } from '../../lib/icons';
import '../custom-els/LoadingSpinner'; import '../custom-els/LoadingSpinner';
import { SourceImage } from '../compress'; import { SourceImage } from '../compress';
import { Fileish, bind } from '../../lib/initial-util'; import { Fileish } from '../../lib/initial-util';
interface Props { interface Props {
loading: boolean; loading: boolean;
@@ -52,14 +52,12 @@ export default class Results extends Component<Props, State> {
} }
} }
@bind private onCopyToOtherClick = (event: Event) => {
private onCopyToOtherClick(event: Event) {
event.preventDefault(); event.preventDefault();
this.props.onCopyToOtherClick(); this.props.onCopyToOtherClick();
} }
@bind onDownload = () => {
onDownload() {
// GA cant do floats. So we round to ints. We're deliberately rounding to nearest kilobyte to // 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. // avoid cases where exact image sizes leak something interesting about the user.
const before = Math.round(this.props.source!.file.size / 1024); const before = Math.round(this.props.source!.file.size / 1024);

View File

@@ -1,5 +1,5 @@
import { h, Component } from 'preact'; import { h, Component } from 'preact';
import * as style from './style.scss'; import * as style from './style.module.scss';
interface Props extends JSX.HTMLAttributes { interface Props extends JSX.HTMLAttributes {
large?: boolean; large?: boolean;

View File

@@ -1,6 +1,5 @@
import PointerTracker from 'pointer-tracker'; import PointerTracker from 'pointer-tracker';
import { bind } from '../../lib/initial-util'; import * as style from './styles.module.css';
import * as style from './styles.css';
const RETARGETED_EVENTS = ['focus', 'blur']; const RETARGETED_EVENTS = ['focus', 'blur'];
const UPDATE_EVENTS = ['input', 'change']; const UPDATE_EVENTS = ['input', 'change'];
@@ -23,6 +22,7 @@ class RangeInputElement extends HTMLElement {
constructor() { constructor() {
super(); super();
this._input = document.createElement('input'); this._input = document.createElement('input');
this._input.type = 'range'; this._input.type = 'range';
this._input.className = style.input; this._input.className = style.input;
@@ -38,10 +38,12 @@ class RangeInputElement extends HTMLElement {
}, },
}); });
this._retargetEvent = this._retargetEvent.bind(this);
for (const event of RETARGETED_EVENTS) { for (const event of RETARGETED_EVENTS) {
this._input.addEventListener(event, this._retargetEvent, true); this._input.addEventListener(event, this._retargetEvent, true);
} }
this._update = this._update.bind(this);
for (const event of UPDATE_EVENTS) { for (const event of UPDATE_EVENTS) {
this._input.addEventListener(event, this._update, true); this._input.addEventListener(event, this._update, true);
} }
@@ -80,14 +82,12 @@ class RangeInputElement extends HTMLElement {
this._update(); this._update();
} }
@bind
private _retargetEvent(event: Event) { private _retargetEvent(event: Event) {
event.stopImmediatePropagation(); event.stopImmediatePropagation();
const retargetted = new Event(event.type, event); const retargetted = new Event(event.type, event);
this.dispatchEvent(retargetted); this.dispatchEvent(retargetted);
} }
@bind
private _update() { private _update() {
const value = Number(this.value) || 0; const value = Number(this.value) || 0;
const min = Number(this.min) || 0; const min = Number(this.min) || 0;

View File

@@ -12,5 +12,6 @@
<link rel="manifest" href="/manifest.json"> <link rel="manifest" href="/manifest.json">
</head> </head>
<body> <body>
<script type="module" src="./index.ts"></script>
</body> </body>
</html> </html>

View File

@@ -1,18 +1,16 @@
declare module '@webcomponents/custom-elements'; declare module '@webcomponents/custom-elements';
function init() { function init() {
require('./init-app.tsx'); import('./init-app.tsx');
} }
if (!('customElements' in self)) { if (!('customElements' in self)) {
import( import('@webcomponents/custom-elements').then(init);
/* webpackChunkName: "wc-polyfill" */
'@webcomponents/custom-elements').then(init);
} else { } else {
init(); init();
} }
if (typeof PRERENDER === 'undefined') { if (typeof window !== 'undefined') {
// Determine the current display mode. // Determine the current display mode.
let displayMode = 'browser'; let displayMode = 'browser';
const mqStandAlone = '(display-mode: standalone)'; const mqStandAlone = '(display-mode: standalone)';

View File

@@ -1,7 +1,7 @@
import { h, render } from 'preact'; import { h, render } from 'preact';
import './lib/fix-pmc'; import './lib/fix-pmc.mjs';
import './style'; import './style/index.scss';
import App from './components/App'; import App from './components/App/index.tsx';
// 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.getElementById('app_root') as Element;
@@ -10,16 +10,7 @@ let root = document.getElementById('app_root') as Element;
root = render(<App />, document.body, root); root = render(<App />, document.body, root);
root.setAttribute('id', 'app_root'); root.setAttribute('id', 'app_root');
if (process.env.NODE_ENV !== 'production') { if (module.hot) {
// Enable support for React DevTools and some helpful console warnings: // Enable support for React DevTools and some helpful console warnings:
require('preact/debug'); import('preact/debug');
// When an update to any module is received, re-import the app and trigger a full re-render:
module.hot.accept('./components/App', () => {
// The linter doesn't like the capital A in App. It is wrong.
// tslint:disable-next-line variable-name
import('./components/App').then(({ default: App }) => {
root = render(<App />, document.body, root);
});
});
} }

View File

@@ -1,4 +1,4 @@
import * as style from './styles.css'; import * as style from './styles.module.css';
export interface SnackOptions { export interface SnackOptions {
timeout?: number; timeout?: number;

View File

@@ -1,33 +1,6 @@
// This file contains the utils that are needed for the very first rendering of the page. They're // This file contains the utils that are needed for the very first rendering of the page. They're
// here because WebPack isn't quite smart enough to split things in the same file. // here because WebPack isn't quite smart enough to split things in the same file.
/**
* A decorator that binds values to their class instance.
* @example
* class C {
* @bind
* foo () {
* return this;
* }
* }
* let f = new C().foo;
* f() instanceof C; // true
*/
export function bind(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
return {
// the first time the prototype property is accessed for an instance,
// define an instance property pointing to the bound function.
// This effectively "caches" the bound prototype method as an instance property.
get() {
const bound = descriptor.value.bind(this);
Object.defineProperty(this, propertyKey, {
value: bound,
});
return bound;
},
};
}
/** Creates a function ref that assigns its value to a given property of an object. /** Creates a function ref that assigns its value to a given property of an object.
* @example * @example
* // element is stored as `this.foo` when rendered. * // element is stored as `this.foo` when rendered.

View File

@@ -60,9 +60,11 @@ export function getSharedImage(): Promise<File> {
/** Set up the service worker and monitor changes */ /** Set up the service worker and monitor changes */
export async function offliner(showSnack: SnackBarElement['showSnackbar']) { export async function offliner(showSnack: SnackBarElement['showSnackbar']) {
// This needs to be a typeof because Webpack. // This needs to be a typeof because Webpack.
if (typeof PRERENDER === 'boolean') return; if (typeof window === 'undefined') return;
if (process.env.NODE_ENV === 'production') { if (!navigator.serviceWorker) return;
if (process.env.NODE_ENV === 'production' && !module.hot) {
navigator.serviceWorker.register('../sw'); navigator.serviceWorker.register('../sw');
} }

View File

@@ -2,6 +2,13 @@ interface CanvasRenderingContext2D {
filter: string; filter: string;
} }
declare module '*.module.scss' {
const classNameMapping: Record<string, string> & {
default: Record<string, string>
};
export = classNameMapping;
}
// Handling file-loader imports: // Handling file-loader imports:
declare module '*.png' { declare module '*.png' {
const content: string; const content: string;
@@ -28,7 +35,17 @@ declare module '*.wasm' {
export default content; export default content;
} }
declare module 'url-loader!*' { declare module 'url:*' {
const value: string;
export default value;
}
declare module 'data-url:*' {
const value: string;
export default value;
}
declare module 'bundle:*' {
const value: string; const value: string;
export default value; export default value;
} }
@@ -41,5 +58,5 @@ declare var ga: {
}; };
interface Navigator { interface Navigator {
readonly standalone: boolean; readonly standalone: boolean;
} }

View File

@@ -1,4 +1,4 @@
import webpDataUrl from 'url-loader!../codecs/tiny.webp'; import webpDataUrl from 'url:../codecs/tiny.webp';
// Give TypeScript the correct global. // Give TypeScript the correct global.
declare var self: ServiceWorkerGlobalScope; declare var self: ServiceWorkerGlobalScope;

View File

@@ -6,15 +6,24 @@
"module": "esnext", "module": "esnext",
"moduleResolution": "node", "moduleResolution": "node",
"experimentalDecorators": true, "experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"sourceMap": true, "sourceMap": true,
"jsx": "react", "jsx": "react",
"jsxFactory": "h", "jsxFactory": "h",
"allowJs": false, "allowJs": false,
"baseUrl": "." "baseUrl": ".",
"paths": {
"codecs/*": [
"codecs/*"
]
}
}, },
"exclude": [ "exclude": [
"src/sw/**/*", "src/sw/**/*",
"src/codecs/processor-worker/**/*" "src/codecs/processor-worker/**/*",
".cache",
"public",
"node_modules"
] ]
} }