mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-14 09:39:15 +00:00
First bit of real UI code landed
This commit is contained in:
@@ -101,7 +101,7 @@ export default function (resolveFileUrl) {
|
||||
hashToId = new Map();
|
||||
pathToResult = new Map();
|
||||
|
||||
const cssPaths = await globP('src/static-build/**/*.css', {
|
||||
const cssPaths = await globP('src/**/*.css', {
|
||||
nodir: true,
|
||||
absolute: true,
|
||||
});
|
||||
@@ -126,11 +126,11 @@ export default function (resolveFileUrl) {
|
||||
|
||||
const cssClassExports = Object.entries(moduleJSON).map(
|
||||
([key, val]) =>
|
||||
`export const $${camelCase(key)} = ${JSON.stringify(val)};`,
|
||||
`export const ${camelCase(key)} = ${JSON.stringify(val)};`,
|
||||
);
|
||||
|
||||
const defs = Object.keys(moduleJSON)
|
||||
.map((key) => `export const $${camelCase(key)}: string;`)
|
||||
.map((key) => `export const ${camelCase(key)}: string;`)
|
||||
.join('\n');
|
||||
|
||||
const defPath = path + '.d.ts';
|
||||
|
||||
2
missing-types.d.ts
vendored
2
missing-types.d.ts
vendored
@@ -21,3 +21,5 @@ declare module 'omt:*' {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare const __PRODUCTION__: boolean;
|
||||
|
||||
72
package-lock.json
generated
72
package-lock.json
generated
@@ -109,9 +109,9 @@
|
||||
}
|
||||
},
|
||||
"@rollup/plugin-commonjs": {
|
||||
"version": "15.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-15.0.0.tgz",
|
||||
"integrity": "sha512-8uAdikHqVyrT32w1zB9VhW6uGwGjhKgnDNP4pQJsjdnyF4FgCj6/bmv24c7v2CuKhq32CcyCwRzMPEElaKkn0w==",
|
||||
"version": "15.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-15.1.0.tgz",
|
||||
"integrity": "sha512-xCQqz4z/o0h2syQ7d9LskIMvBSH4PX5PjYdpSSvgS+pQik3WahkQVNWg3D8XJeYjZoVWnIUQYDghuEMRGrmQYQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@rollup/pluginutils": "^3.1.0",
|
||||
@@ -145,6 +145,16 @@
|
||||
"resolve": "^1.17.0"
|
||||
}
|
||||
},
|
||||
"@rollup/plugin-replace": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.3.3.tgz",
|
||||
"integrity": "sha512-XPmVXZ7IlaoWaJLkSCDaa0Y6uVo5XQYHhiMFzOd5qSv5rE+t/UJToPIOE56flKIxBFQI27ONsxb7dqHnwSsjKQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@rollup/pluginutils": "^3.0.8",
|
||||
"magic-string": "^0.25.5"
|
||||
}
|
||||
},
|
||||
"@rollup/pluginutils": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz",
|
||||
@@ -195,9 +205,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "14.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.10.1.tgz",
|
||||
"integrity": "sha512-aYNbO+FZ/3KGeQCEkNhHFRIzBOUgc7QvcVNKXbfnhDkSfwUv91JsQQa10rDgKSTSLkXZ1UIyPe4FJJNVgw1xWQ==",
|
||||
"version": "14.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.2.tgz",
|
||||
"integrity": "sha512-jiE3QIxJ8JLNcb1Ps6rDbysDhN4xa8DJJvuC9prr6w+1tIh+QAbYyNF3tyiZNLDBIuBCf4KEcV2UvQm/V60xfA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/parse-json": {
|
||||
@@ -1157,12 +1167,12 @@
|
||||
"dev": true
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
|
||||
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
|
||||
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
"ms": "2.1.2"
|
||||
}
|
||||
},
|
||||
"dedent": {
|
||||
@@ -1457,6 +1467,12 @@
|
||||
"escape-string-regexp": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"file-drop-element": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-drop-element/-/file-drop-element-1.0.0.tgz",
|
||||
"integrity": "sha512-4T+hoNZR7hMumVcCUbmg2XtjGph15thvsT40+Xu8snMBpnDsRFhBnZ6Nhxbnwot451gg8EfJzQRS+Wmr4j7Ytw==",
|
||||
"dev": true
|
||||
},
|
||||
"fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
@@ -1923,9 +1939,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"lint-staged": {
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-10.3.0.tgz",
|
||||
"integrity": "sha512-an3VgjHqmJk0TORB/sdQl0CTkRg4E5ybYCXTTCSJ5h9jFwZbcgKIx5oVma5e7wp/uKt17s1QYFmYqT9MGVosGw==",
|
||||
"version": "10.4.0",
|
||||
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-10.4.0.tgz",
|
||||
"integrity": "sha512-uaiX4U5yERUSiIEQc329vhCTDDwUcSvKdRLsNomkYLRzijk3v8V9GWm2Nz0RMVB87VcuzLvtgy6OsjoH++QHIg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chalk": "^4.1.0",
|
||||
@@ -2399,9 +2415,9 @@
|
||||
}
|
||||
},
|
||||
"postcss": {
|
||||
"version": "7.0.32",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.32.tgz",
|
||||
"integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==",
|
||||
"version": "7.0.34",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.34.tgz",
|
||||
"integrity": "sha512-H/7V2VeNScX9KE83GDrDZNiGT1m2H+UTnlinIzhjlLX9hfMUn1mHNnGeX81a1c8JSBdBvqk7c2ZOG6ZPn5itGw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chalk": "^2.4.2",
|
||||
@@ -3063,9 +3079,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"preact": {
|
||||
"version": "10.4.8",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.4.8.tgz",
|
||||
"integrity": "sha512-uVLeEAyRsCkUEFhVHlOu17OxcrwC7+hTGZ08kBoLBiGHiZooUZuibQnphgMKftw/rqYntNMyhVCPqQhcyAGHag==",
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.5.0.tgz",
|
||||
"integrity": "sha512-CuhSq2uq1lUy9442j9Jlucapt8+9SFyNl1+evzbMb8dTF4GCPrc1XMvf9Hai7XbeXG/wIxR0TVhhEFKJ3DkY6Q==",
|
||||
"dev": true
|
||||
},
|
||||
"preact-render-to-string": {
|
||||
@@ -3078,9 +3094,9 @@
|
||||
}
|
||||
},
|
||||
"prettier": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.1.1.tgz",
|
||||
"integrity": "sha512-9bY+5ZWCfqj3ghYBLxApy2zf6m+NJo5GzmLTpr9FsApsfjriNnS2dahWReHMi7qNPhhHl9SYHJs2cHZLgexNIw==",
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.1.2.tgz",
|
||||
"integrity": "sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg==",
|
||||
"dev": true
|
||||
},
|
||||
"pretty-format": {
|
||||
@@ -3225,9 +3241,9 @@
|
||||
}
|
||||
},
|
||||
"rollup": {
|
||||
"version": "2.26.11",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.26.11.tgz",
|
||||
"integrity": "sha512-xyfxxhsE6hW57xhfL1I+ixH8l2bdoIMaAecdQiWF3N7IgJEMu99JG+daBiSZQjnBpzFxa0/xZm+3pbCdAQehHw==",
|
||||
"version": "2.28.1",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.28.1.tgz",
|
||||
"integrity": "sha512-DOtVoqOZt3+FjPJWLU8hDIvBjUylc9s6IZvy76XklxzcLvAQLtVAG/bbhsMhcWnYxC0TKKcf1QQ/tg29zeID0Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fsevents": "~2.1.2"
|
||||
@@ -3812,9 +3828,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"typescript": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.2.tgz",
|
||||
"integrity": "sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.3.tgz",
|
||||
"integrity": "sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg==",
|
||||
"dev": true
|
||||
},
|
||||
"uniq": {
|
||||
|
||||
18
package.json
18
package.json
@@ -10,29 +10,31 @@
|
||||
"serve": "serve --config server.json .tmp/build/static"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^15.0.0",
|
||||
"@rollup/plugin-commonjs": "^15.1.0",
|
||||
"@rollup/plugin-node-resolve": "^9.0.0",
|
||||
"@rollup/plugin-replace": "^2.3.3",
|
||||
"@surma/rollup-plugin-off-main-thread": "^1.4.1",
|
||||
"@types/node": "^14.10.1",
|
||||
"@types/node": "^14.11.2",
|
||||
"comlink": "^4.3.0",
|
||||
"cssnano": "^4.1.10",
|
||||
"del": "^5.1.0",
|
||||
"file-drop-element": "^1.0.0",
|
||||
"husky": "^4.3.0",
|
||||
"lint-staged": "^10.3.0",
|
||||
"lint-staged": "^10.4.0",
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"postcss": "^7.0.32",
|
||||
"postcss": "^7.0.34",
|
||||
"postcss-import": "^12.0.1",
|
||||
"postcss-modules": "^3.2.2",
|
||||
"postcss-nested": "^4.2.3",
|
||||
"postcss-simple-vars": "^5.0.2",
|
||||
"postcss-url": "^8.0.0",
|
||||
"preact": "^10.4.8",
|
||||
"preact": "^10.5.0",
|
||||
"preact-render-to-string": "^5.1.10",
|
||||
"prettier": "^2.1.1",
|
||||
"rollup": "^2.26.11",
|
||||
"prettier": "^2.1.2",
|
||||
"rollup": "^2.28.1",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"serve": "^11.3.2",
|
||||
"typescript": "^4.0.2"
|
||||
"typescript": "^4.0.3"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
|
||||
@@ -17,6 +17,7 @@ import resolve from '@rollup/plugin-node-resolve';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import { terser } from 'rollup-plugin-terser';
|
||||
import OMT from '@surma/rollup-plugin-off-main-thread';
|
||||
import replace from '@rollup/plugin-replace';
|
||||
|
||||
import simpleTS from './lib/simple-ts';
|
||||
import clientBundlePlugin from './lib/client-bundle-plugin';
|
||||
@@ -46,6 +47,8 @@ export default async function ({ watch }) {
|
||||
);
|
||||
await del('.tmp/build');
|
||||
|
||||
const isProduction = !watch;
|
||||
|
||||
const tsPluginInstance = simpleTS('.', {
|
||||
watch,
|
||||
});
|
||||
@@ -84,7 +87,8 @@ export default async function ({ watch }) {
|
||||
...commonPlugins(),
|
||||
commonjs(),
|
||||
resolve(),
|
||||
terser({ module: true }),
|
||||
replace({ __PRERENDER__: false, __PRODUCTION__: isProduction }),
|
||||
//terser({ module: true }),
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -99,6 +103,7 @@ export default async function ({ watch }) {
|
||||
emitFiles({ include: '**/*', root: path.join(__dirname, 'src', 'copy') }),
|
||||
nodeExternalPlugin(),
|
||||
imageWorkerPlugin(),
|
||||
replace({ __PRERENDER__: true, __PRODUCTION__: isProduction }),
|
||||
runScript(dir + '/index.js'),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { wrap } from 'comlink';
|
||||
import workerURL from 'omt:features/worker';
|
||||
import imgURL from 'url:./tmp.png';
|
||||
|
||||
import type { ProcessorWorkerApi } from 'features/worker';
|
||||
const worker = new Worker(workerURL);
|
||||
const api = wrap<ProcessorWorkerApi>(worker);
|
||||
|
||||
async function demo() {
|
||||
const img = document.createElement('img');
|
||||
img.src = imgURL;
|
||||
await img.decode();
|
||||
// Make canvas same size as image
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
ctx.drawImage(img, 0, 0);
|
||||
const data = ctx.getImageData(0, 0, img.width, img.height);
|
||||
const result = await api.rotate(data, {
|
||||
rotate: 180,
|
||||
});
|
||||
|
||||
{
|
||||
/*const resultUrl = URL.createObjectURL(new Blob([result]));
|
||||
const img = new Image();
|
||||
img.src = resultUrl;
|
||||
document.body.append(img);*/
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = result.width;
|
||||
canvas.height = result.height;
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
ctx.putImageData(result, 0, 0);
|
||||
document.body.append(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
demo();
|
||||
137
src/client/initial-app/App/index.tsx
Normal file
137
src/client/initial-app/App/index.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import type { FileDropEvent } from 'file-drop-element';
|
||||
import type SnackBarElement from 'client/initial-app/custom-els/snack-bar';
|
||||
import type { SnackOptions } from 'client/initial-app/custom-els/snack-bar';
|
||||
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
import { linkRef } from 'client/initial-app/util';
|
||||
import * as style from './style.css';
|
||||
import 'file-drop-element';
|
||||
import 'client/initial-app/custom-els/snack-bar';
|
||||
//import Intro from '../intro';
|
||||
import 'client/initial-app/custom-els/loading-spinner';
|
||||
|
||||
const ROUTE_EDITOR = '/editor';
|
||||
|
||||
//const compressPromise = import('../compress');
|
||||
//const swBridgePromise = import('../../lib/sw-bridge');
|
||||
|
||||
function back() {
|
||||
window.history.back();
|
||||
}
|
||||
|
||||
interface Props {}
|
||||
|
||||
interface State {
|
||||
awaitingShareTarget: boolean;
|
||||
file?: File;
|
||||
isEditorOpen: Boolean;
|
||||
Compress?: undefined; // typeof import('../compress').default;
|
||||
}
|
||||
|
||||
export default class App extends Component<Props, State> {
|
||||
state: State = {
|
||||
awaitingShareTarget: new URL(location.href).searchParams.has(
|
||||
'share-target',
|
||||
),
|
||||
isEditorOpen: false,
|
||||
file: undefined,
|
||||
Compress: undefined,
|
||||
};
|
||||
|
||||
snackbar?: SnackBarElement;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
/*compressPromise
|
||||
.then((module) => {
|
||||
this.setState({ Compress: module.default });
|
||||
})
|
||||
.catch(() => {
|
||||
this.showSnack('Failed to load app');
|
||||
});
|
||||
|
||||
swBridgePromise.then(async ({ offliner, getSharedImage }) => {
|
||||
offliner(this.showSnack);
|
||||
if (!this.state.awaitingShareTarget) return;
|
||||
const file = await getSharedImage();
|
||||
// Remove the ?share-target from the URL
|
||||
history.replaceState('', '', '/');
|
||||
this.openEditor();
|
||||
this.setState({ file, awaitingShareTarget: false });
|
||||
});*/
|
||||
|
||||
// Since iOS 10, Apple tries to prevent disabling pinch-zoom. This is great in theory, but
|
||||
// really breaks things on Squoosh, as you can easily end up zooming the UI when you mean to
|
||||
// zoom the image. Once you've done this, it's really difficult to undo. Anyway, this seems to
|
||||
// prevent it.
|
||||
document.body.addEventListener('gesturestart', (event) => {
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
window.addEventListener('popstate', this.onPopState);
|
||||
}
|
||||
|
||||
private onFileDrop = ({ files }: FileDropEvent) => {
|
||||
if (!files || files.length === 0) return;
|
||||
const file = files[0];
|
||||
this.openEditor();
|
||||
this.setState({ file });
|
||||
};
|
||||
|
||||
private onIntroPickFile = (file: File) => {
|
||||
this.openEditor();
|
||||
this.setState({ file });
|
||||
};
|
||||
|
||||
private showSnack = (
|
||||
message: string,
|
||||
options: SnackOptions = {},
|
||||
): Promise<string> => {
|
||||
if (!this.snackbar) throw Error('Snackbar missing');
|
||||
return this.snackbar.showSnackbar(message, options);
|
||||
};
|
||||
|
||||
private onPopState = () => {
|
||||
this.setState({ isEditorOpen: location.pathname === ROUTE_EDITOR });
|
||||
};
|
||||
|
||||
private openEditor = () => {
|
||||
if (this.state.isEditorOpen) return;
|
||||
// Change path, but preserve query string.
|
||||
const editorURL = new URL(location.href);
|
||||
editorURL.pathname = ROUTE_EDITOR;
|
||||
history.pushState(null, '', editorURL.href);
|
||||
this.setState({ isEditorOpen: true });
|
||||
};
|
||||
|
||||
render(
|
||||
{}: Props,
|
||||
{ file, isEditorOpen, Compress, awaitingShareTarget }: State,
|
||||
) {
|
||||
const showSpinner = awaitingShareTarget || (isEditorOpen && !Compress);
|
||||
|
||||
return (
|
||||
<div class={style.app}>
|
||||
<file-drop
|
||||
accept="image/*"
|
||||
onfiledrop={this.onFileDrop}
|
||||
class={style.drop}
|
||||
>
|
||||
{showSpinner ? (
|
||||
<loading-spinner class={style.appLoader} />
|
||||
) : isEditorOpen ? (
|
||||
Compress &&
|
||||
//<Compress file={file!} showSnack={this.showSnack} onBack={back} />
|
||||
'TODO: uncomment above'
|
||||
) : (
|
||||
//<Intro onFile={this.onIntroPickFile} showSnack={this.showSnack} />
|
||||
'TODO: show intro here'
|
||||
)}
|
||||
<snack-bar ref={linkRef(this, 'snackbar')} />
|
||||
</file-drop>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
68
src/client/initial-app/App/style.css
Normal file
68
src/client/initial-app/App/style.css
Normal file
@@ -0,0 +1,68 @@
|
||||
.app {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
contain: strict;
|
||||
}
|
||||
|
||||
.drop {
|
||||
overflow: hidden;
|
||||
touch-action: none;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
&:global {
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
display: block;
|
||||
left: 10px;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
border: 2px dashed #fff;
|
||||
background-color: rgba(88, 116, 88, 0.2);
|
||||
border-color: rgba(65, 129, 65, 0.5);
|
||||
border-radius: 10px;
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
transition: all 200ms ease-in;
|
||||
transition-property: transform, opacity;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.drop-valid::after {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
transition-timing-function: ease-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.option-pair {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
&.horizontal {
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
&.vertical {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.app-loader {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
--size: 225px;
|
||||
--stroke-width: 26px;
|
||||
}
|
||||
62
src/client/initial-app/custom-els/loading-spinner/index.ts
Normal file
62
src/client/initial-app/custom-els/loading-spinner/index.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import * as styles from './styles.css';
|
||||
|
||||
/**
|
||||
* A simple spinner. This custom element has no JS API. Just put it in the document, and it'll
|
||||
* spin. You can configure the following using CSS custom properties:
|
||||
*
|
||||
* --size: Size of the spinner element (it's always square). Default: 28px.
|
||||
* --color: Color of the spinner. Default: #4285f4.
|
||||
* --stroke-width: Width of the stroke of the spinner. Default: 3px.
|
||||
* --delay: Once the spinner enters the DOM, how long until it shows. This prevents the spinner
|
||||
* appearing on the screen for short operations. Default: 300ms.
|
||||
*/
|
||||
export default class LoadingSpinner extends HTMLElement {
|
||||
private _delayTimeout: number = 0;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Ideally we'd use shadow DOM here, but we're targeting browsers without shadow DOM support.
|
||||
// You can't set attributes/content in a custom element constructor, so I'm waiting a microtask.
|
||||
Promise.resolve().then(() => {
|
||||
this.style.display = 'none';
|
||||
// prettier-ignore
|
||||
this.innerHTML = '' +
|
||||
`<div class="${styles.spinnerContainer}">` +
|
||||
`<div class="${styles.spinnerLayer}">` +
|
||||
`<div class="${styles.spinnerCircleClipper} ${styles.spinnerLeft}">` +
|
||||
`<div class="${styles.spinnerCircle}"></div>` +
|
||||
'</div>' +
|
||||
`<div class="${styles.spinnerGapPatch}">` +
|
||||
`<div class="${styles.spinnerCircle}"></div>` +
|
||||
'</div>' +
|
||||
`<div class="${styles.spinnerCircleClipper} ${styles.spinnerRight}">` +
|
||||
`<div class="${styles.spinnerCircle}"></div>` +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
});
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.style.display = 'none';
|
||||
clearTimeout(this._delayTimeout);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
const delayStr = getComputedStyle(this).getPropertyValue('--delay').trim();
|
||||
let delayNum = parseFloat(delayStr);
|
||||
|
||||
// If seconds…
|
||||
if (/\ds$/.test(delayStr)) {
|
||||
// Convert to ms.
|
||||
delayNum *= 1000;
|
||||
}
|
||||
|
||||
this._delayTimeout = self.setTimeout(() => {
|
||||
this.style.display = '';
|
||||
}, delayNum);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('loading-spinner', LoadingSpinner);
|
||||
13
src/client/initial-app/custom-els/loading-spinner/missing-types.d.ts
vendored
Normal file
13
src/client/initial-app/custom-els/loading-spinner/missing-types.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
interface LoadingSpinner extends preact.JSX.HTMLAttributes {}
|
||||
|
||||
declare module 'preact' {
|
||||
namespace createElement.JSX {
|
||||
interface IntrinsicElements {
|
||||
'loading-spinner': LoadingSpinner;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Thing break unless this file is a module.
|
||||
// Don't ask me why. I don't know.
|
||||
export {};
|
||||
158
src/client/initial-app/custom-els/loading-spinner/styles.css
Normal file
158
src/client/initial-app/custom-els/loading-spinner/styles.css
Normal file
@@ -0,0 +1,158 @@
|
||||
@keyframes spinner-left-spin {
|
||||
from {
|
||||
transform: rotate(130deg);
|
||||
}
|
||||
50% {
|
||||
transform: rotate(-5deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(130deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spinner-right-spin {
|
||||
from {
|
||||
transform: rotate(-130deg);
|
||||
}
|
||||
50% {
|
||||
transform: rotate(5deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(-130deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spinner-fade-out {
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spinner-container-rotate {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spinner-fill-unfill-rotate {
|
||||
12.5% {
|
||||
transform: rotate(135deg);
|
||||
} /* 0.5 * ARCSIZE */
|
||||
25% {
|
||||
transform: rotate(270deg);
|
||||
} /* 1 * ARCSIZE */
|
||||
37.5% {
|
||||
transform: rotate(405deg);
|
||||
} /* 1.5 * ARCSIZE */
|
||||
50% {
|
||||
transform: rotate(540deg);
|
||||
} /* 2 * ARCSIZE */
|
||||
62.5% {
|
||||
transform: rotate(675deg);
|
||||
} /* 2.5 * ARCSIZE */
|
||||
75% {
|
||||
transform: rotate(810deg);
|
||||
} /* 3 * ARCSIZE */
|
||||
87.5% {
|
||||
transform: rotate(945deg);
|
||||
} /* 3.5 * ARCSIZE */
|
||||
to {
|
||||
transform: rotate(1080deg);
|
||||
} /* 4 * ARCSIZE */
|
||||
}
|
||||
|
||||
loading-spinner {
|
||||
--size: 28px;
|
||||
--color: #4285f4;
|
||||
--stroke-width: 3px;
|
||||
--delay: 300ms;
|
||||
|
||||
pointer-events: none;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
border-color: var(--color);
|
||||
}
|
||||
|
||||
loading-spinner .spinner-circle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
width: 200%;
|
||||
border-width: var(--stroke-width);
|
||||
border-style: solid;
|
||||
border-color: inherit;
|
||||
border-bottom-color: transparent !important;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/*
|
||||
Patch the gap that appear between the two adjacent div.circle-clipper while the
|
||||
spinner is rotating (appears on Chrome 38, Safari 7.1, and IE 11).
|
||||
*/
|
||||
loading-spinner .spinner-gap-patch {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
top: 0;
|
||||
left: 45%;
|
||||
width: 10%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
border-color: inherit;
|
||||
}
|
||||
|
||||
loading-spinner .spinner-gap-patch .spinner-circle {
|
||||
width: 1000%;
|
||||
left: -450%;
|
||||
}
|
||||
|
||||
loading-spinner .spinner-circle-clipper {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
border-color: inherit;
|
||||
}
|
||||
|
||||
loading-spinner .spinner-left .spinner-circle {
|
||||
border-right-color: transparent !important;
|
||||
transform: rotate(129deg);
|
||||
animation: spinner-left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;
|
||||
}
|
||||
|
||||
loading-spinner .spinner-right .spinner-circle {
|
||||
left: -100%;
|
||||
border-left-color: transparent !important;
|
||||
transform: rotate(-129deg);
|
||||
animation: spinner-right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite
|
||||
both;
|
||||
}
|
||||
|
||||
loading-spinner.spinner-fadeout {
|
||||
animation: spinner-fade-out 400ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||
}
|
||||
|
||||
loading-spinner .spinner-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-color: inherit;
|
||||
|
||||
/* duration: 360 * ARCTIME / (ARCSTARTROT + (360-ARCSIZE)) */
|
||||
animation: spinner-container-rotate 1568ms linear infinite;
|
||||
}
|
||||
|
||||
loading-spinner .spinner-layer {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-color: inherit;
|
||||
/* durations: 4 * ARCTIME */
|
||||
animation: spinner-fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1)
|
||||
infinite both;
|
||||
}
|
||||
16
src/client/initial-app/custom-els/missing-types.d.ts
vendored
Normal file
16
src/client/initial-app/custom-els/missing-types.d.ts
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { FileDropElement, FileDropEvent } from 'file-drop-element';
|
||||
|
||||
interface FileDropAttributes extends preact.JSX.HTMLAttributes {
|
||||
accept?: string;
|
||||
onfiledrop?: ((this: FileDropElement, ev: FileDropEvent) => any) | null;
|
||||
}
|
||||
|
||||
declare module 'preact' {
|
||||
namespace createElement.JSX {
|
||||
interface IntrinsicElements {
|
||||
'file-drop': FileDropAttributes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
95
src/client/initial-app/custom-els/snack-bar/index.ts
Normal file
95
src/client/initial-app/custom-els/snack-bar/index.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import * as style from './styles.css';
|
||||
|
||||
export interface SnackOptions {
|
||||
timeout?: number;
|
||||
actions?: string[];
|
||||
}
|
||||
|
||||
function createSnack(
|
||||
message: string,
|
||||
options: SnackOptions,
|
||||
): [Element, Promise<string>] {
|
||||
const { timeout = 0, actions = ['dismiss'] } = options;
|
||||
|
||||
const el = document.createElement('div');
|
||||
el.className = style.snackbar;
|
||||
el.setAttribute('aria-live', 'assertive');
|
||||
el.setAttribute('aria-atomic', 'true');
|
||||
el.setAttribute('aria-hidden', 'false');
|
||||
|
||||
const text = document.createElement('div');
|
||||
text.className = style.text;
|
||||
text.textContent = message;
|
||||
el.appendChild(text);
|
||||
|
||||
const result = new Promise<string>((resolve) => {
|
||||
let timeoutId: number;
|
||||
|
||||
// Add action buttons
|
||||
for (const action of actions) {
|
||||
const button = document.createElement('button');
|
||||
button.className = style.button;
|
||||
button.textContent = action;
|
||||
button.addEventListener('click', () => {
|
||||
clearTimeout(timeoutId);
|
||||
resolve(action);
|
||||
});
|
||||
el.appendChild(button);
|
||||
}
|
||||
|
||||
// Add timeout
|
||||
if (timeout) {
|
||||
timeoutId = self.setTimeout(() => resolve(''), timeout);
|
||||
}
|
||||
});
|
||||
|
||||
return [el, result];
|
||||
}
|
||||
|
||||
export default class SnackBarElement extends HTMLElement {
|
||||
private _snackbars: [
|
||||
string,
|
||||
SnackOptions,
|
||||
(action: Promise<string>) => void,
|
||||
][] = [];
|
||||
private _processingQueue = false;
|
||||
|
||||
/**
|
||||
* Show a snackbar. Returns a promise for the name of the action clicked, or an empty string if no
|
||||
* action is clicked.
|
||||
*/
|
||||
showSnackbar(message: string, options: SnackOptions = {}): Promise<string> {
|
||||
return new Promise<string>((resolve) => {
|
||||
this._snackbars.push([message, options, resolve]);
|
||||
if (!this._processingQueue) this._processQueue();
|
||||
});
|
||||
}
|
||||
|
||||
private async _processQueue() {
|
||||
this._processingQueue = true;
|
||||
|
||||
while (this._snackbars[0]) {
|
||||
const [message, options, resolver] = this._snackbars[0];
|
||||
const [el, result] = createSnack(message, options);
|
||||
// Pass the result back to the original showSnackbar call.
|
||||
resolver(result);
|
||||
this.appendChild(el);
|
||||
|
||||
// Wait for the user to click an action, or for the snack to timeout.
|
||||
await result;
|
||||
|
||||
// Transition the snack away.
|
||||
el.setAttribute('aria-hidden', 'true');
|
||||
await new Promise((resolve) => {
|
||||
el.addEventListener('animationend', () => resolve());
|
||||
});
|
||||
el.remove();
|
||||
|
||||
this._snackbars.shift();
|
||||
}
|
||||
|
||||
this._processingQueue = false;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('snack-bar', SnackBarElement);
|
||||
15
src/client/initial-app/custom-els/snack-bar/missing-types.d.ts
vendored
Normal file
15
src/client/initial-app/custom-els/snack-bar/missing-types.d.ts
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { SnackOptions } from '.';
|
||||
|
||||
interface SnackBarAttributes extends preact.JSX.HTMLAttributes {
|
||||
showSnackbar?: (options: SnackOptions) => Promise<string>;
|
||||
}
|
||||
|
||||
declare module 'preact' {
|
||||
namespace createElement.JSX {
|
||||
interface IntrinsicElements {
|
||||
'snack-bar': SnackBarAttributes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
105
src/client/initial-app/custom-els/snack-bar/styles.css
Normal file
105
src/client/initial-app/custom-els/snack-bar/styles.css
Normal file
@@ -0,0 +1,105 @@
|
||||
snack-bar {
|
||||
display: block;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.snackbar {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
left: 50%;
|
||||
bottom: 24px;
|
||||
width: 344px;
|
||||
margin-left: -172px;
|
||||
background: #2a2a2a;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
|
||||
transform-origin: center;
|
||||
color: #eee;
|
||||
z-index: 100;
|
||||
cursor: default;
|
||||
will-change: transform;
|
||||
animation: snackbar-show 300ms ease forwards 1;
|
||||
}
|
||||
.snackbar[aria-hidden='true'] {
|
||||
animation: snackbar-hide 300ms ease forwards 1;
|
||||
}
|
||||
@keyframes snackbar-show {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.5);
|
||||
}
|
||||
}
|
||||
@keyframes snackbar-hide {
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateY(100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
.snackbar {
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
margin-left: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
flex: 1 1 auto;
|
||||
padding: 16px;
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
.button {
|
||||
position: relative;
|
||||
flex: 0 1 auto;
|
||||
padding: 8px;
|
||||
height: 36px;
|
||||
margin: auto 8px auto -8px;
|
||||
min-width: 5em;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
color: lightgreen;
|
||||
font-weight: inherit;
|
||||
letter-spacing: 0.05em;
|
||||
font-size: 100%;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
transition: background-color 200ms ease;
|
||||
outline: none;
|
||||
}
|
||||
.button:hover {
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.button:focus:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 120%;
|
||||
height: 0;
|
||||
padding: 0 0 120%;
|
||||
margin: -60% 0 0 -60%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 50%;
|
||||
transform-origin: center;
|
||||
will-change: transform;
|
||||
animation: focus-ring 300ms ease-out forwards 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
@keyframes focus-ring {
|
||||
from {
|
||||
transform: scale(0.01);
|
||||
}
|
||||
}
|
||||
44
src/client/initial-app/index.tsx
Normal file
44
src/client/initial-app/index.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { h, render } from 'preact';
|
||||
import App from './App';
|
||||
|
||||
const root = document.getElementById('app') as HTMLElement;
|
||||
|
||||
async function main() {
|
||||
if (!__PRODUCTION__) await import('preact/debug');
|
||||
render(<App />, root);
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
// Analytics
|
||||
{
|
||||
// Determine the current display mode.
|
||||
const displayMode =
|
||||
navigator.standalone ||
|
||||
window.matchMedia('(display-mode: standalone)').matches
|
||||
? 'standalone'
|
||||
: 'browser';
|
||||
|
||||
// Setup analytics
|
||||
window.ga = window.ga || ((...args) => (ga.q = ga.q || []).push(args));
|
||||
ga('create', 'UA-128752250-1', 'auto');
|
||||
ga('set', 'transport', 'beacon');
|
||||
ga('set', 'dimension1', displayMode);
|
||||
ga('send', 'pageview', '/index.html', { title: 'Squoosh' });
|
||||
// Load the GA script
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://www.google-analytics.com/analytics.js';
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
15
src/client/initial-app/util.ts
Normal file
15
src/client/initial-app/util.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/** Creates a function ref that assigns its value to a given property of an object.
|
||||
* @example
|
||||
* // element is stored as `this.foo` when rendered.
|
||||
* <div ref={linkRef(this, 'foo')} />
|
||||
*/
|
||||
export function linkRef<T>(obj: any, name: string) {
|
||||
const refName = `$$ref_${name}`;
|
||||
let ref = obj[refName];
|
||||
if (!ref) {
|
||||
ref = obj[refName] = (c: T) => {
|
||||
obj[name] = c;
|
||||
};
|
||||
}
|
||||
return ref;
|
||||
}
|
||||
11
src/client/missing-types.d.ts
vendored
11
src/client/missing-types.d.ts
vendored
@@ -11,3 +11,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
/// <reference path="../../missing-types.d.ts" />
|
||||
|
||||
declare var ga: {
|
||||
(...args: any[]): void;
|
||||
q: any[];
|
||||
};
|
||||
|
||||
interface Navigator {
|
||||
readonly standalone: boolean;
|
||||
}
|
||||
|
||||
declare module 'preact/debug' {}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 90 KiB |
BIN
src/static-build/assets/favicon.ico
Normal file
BIN
src/static-build/assets/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -1,44 +0,0 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { h, FunctionalComponent, RenderableProps } from 'preact';
|
||||
import styles from 'css-bundle:./all.css';
|
||||
import clientBundleURL, { imports } from 'client-bundle:client/index.tsx';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const BasePage: FunctionalComponent<Props> = ({
|
||||
children,
|
||||
title,
|
||||
}: RenderableProps<Props>) => {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{title ? `${title} - ` : ''}Squoosh</title>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, minimum-scale=1.0"
|
||||
></meta>
|
||||
<link rel="stylesheet" href={styles} />
|
||||
<script src={clientBundleURL} defer />
|
||||
{imports.map((v) => (
|
||||
<link rel="preload" as="script" href={v} />
|
||||
))}
|
||||
</head>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
};
|
||||
|
||||
export default BasePage;
|
||||
@@ -11,11 +11,40 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { h, FunctionalComponent } from 'preact';
|
||||
import BasePage from 'static-build/components/base';
|
||||
|
||||
const IndexPage: FunctionalComponent<{}> = () => (
|
||||
<BasePage>
|
||||
<h1>Hi</h1>
|
||||
</BasePage>
|
||||
import styles from 'css-bundle:./all.css';
|
||||
import clientBundleURL, { imports } from 'client-bundle:client/initial-app';
|
||||
import favicon from 'url:static-build/assets/favicon.ico';
|
||||
|
||||
interface Props {}
|
||||
|
||||
const Index: FunctionalComponent<Props> = () => (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Squoosh</title>
|
||||
<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="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<link rel="shortcut icon" href={favicon} />
|
||||
<meta name="theme-color" content="#f78f21" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="stylesheet" href={styles} />
|
||||
<script src={clientBundleURL} defer />
|
||||
{imports.map((v) => (
|
||||
<link rel="preload" as="script" href={v} />
|
||||
))}
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
export default IndexPage;
|
||||
|
||||
export default Index;
|
||||
|
||||
Reference in New Issue
Block a user