First bit of real UI code landed

This commit is contained in:
Jake Archibald
2020-09-23 14:38:41 +01:00
parent 6573103755
commit 455c868e55
23 changed files with 839 additions and 141 deletions

View File

@@ -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
View File

@@ -21,3 +21,5 @@ declare module 'omt:*' {
const value: string;
export default value;
}
declare const __PRODUCTION__: boolean;

72
package-lock.json generated
View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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'),
],
};

View File

@@ -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();

View 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>
);
}
}

View 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;
}

View 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);

View 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 {};

View 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;
}

View 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 {};

View 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);

View 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 {};

View 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);
}
}

View 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);
}

View 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;
}

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -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;

View File

@@ -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;