mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-17 19:19:47 +00:00
wip
# Conflicts: # codecs/cpp.Dockerfile # codecs/imagequant/example.html # codecs/webp/dec/webp_dec.d.ts # codecs/webp/dec/webp_dec.js # codecs/webp/dec/webp_dec.wasm # codecs/webp/enc/webp_enc.d.ts # codecs/webp/enc/webp_enc.js # codecs/webp/enc/webp_enc.wasm # package-lock.json # package.json # src/codecs/tiny.webp # src_old/codecs/encoders.ts # src_old/codecs/processor-worker/tiny.avif # src_old/codecs/processor-worker/tiny.webp # src_old/codecs/tiny.webp # src_old/components/compress/index.tsx # src_old/lib/util.ts # src_old/sw/util.ts
This commit is contained in:
95
src_old/lib/SnackBar/index.ts
Normal file
95
src_old/lib/SnackBar/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);
|
||||
13
src_old/lib/SnackBar/missing-types.d.ts
vendored
Normal file
13
src_old/lib/SnackBar/missing-types.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
import { SnackOptions, SnackShowResult } from '.';
|
||||
|
||||
declare global {
|
||||
namespace JSX {
|
||||
interface IntrinsicElements {
|
||||
'snack-bar': SnackBarAttributes;
|
||||
}
|
||||
|
||||
interface SnackBarAttributes extends HTMLAttributes {
|
||||
showSnackbar?: (options: SnackOptions) => Promise<SnackShowResult>;
|
||||
}
|
||||
}
|
||||
}
|
||||
105
src_old/lib/SnackBar/styles.css
Normal file
105
src_old/lib/SnackBar/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);
|
||||
}
|
||||
}
|
||||
62
src_old/lib/clean-modify.ts
Normal file
62
src_old/lib/clean-modify.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
function cleanSetOrMerge<A extends any[] | object>(
|
||||
source: A,
|
||||
keys: string | number | string[],
|
||||
toSetOrMerge: any[] | object,
|
||||
merge: boolean,
|
||||
): A {
|
||||
const splitKeys = Array.isArray(keys) ? keys : ('' + keys).split('.');
|
||||
|
||||
// Going off road in terms of types, otherwise TypeScript doesn't like the access-by-index.
|
||||
// The assumptions in this code break if the object contains things which aren't arrays or
|
||||
// plain objects.
|
||||
let last = copy(source) as any;
|
||||
const newObject = last;
|
||||
|
||||
const lastIndex = splitKeys.length - 1;
|
||||
|
||||
for (const [i, key] of splitKeys.entries()) {
|
||||
if (i !== lastIndex) {
|
||||
// Copy everything along the path.
|
||||
last = last[key] = copy(last[key]);
|
||||
} else {
|
||||
// Merge or set.
|
||||
last[key] = merge
|
||||
? Object.assign(copy(last[key]), toSetOrMerge)
|
||||
: toSetOrMerge;
|
||||
}
|
||||
}
|
||||
|
||||
return newObject;
|
||||
}
|
||||
|
||||
function copy<A extends any[] | object>(source: A): A {
|
||||
// Some type cheating here, as TypeScript can't infer between generic types.
|
||||
if (Array.isArray(source)) return [...source] as any;
|
||||
return { ...(source as any) };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param source Object to copy from.
|
||||
* @param keys Path to modify, eg "foo.bar.baz".
|
||||
* @param toMerge A value to merge into the value at the path.
|
||||
*/
|
||||
export function cleanMerge<A extends any[] | object>(
|
||||
source: A,
|
||||
keys: string | number | string[],
|
||||
toMerge: any[] | object,
|
||||
): A {
|
||||
return cleanSetOrMerge(source, keys, toMerge, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param source Object to copy from.
|
||||
* @param keys Path to modify, eg "foo.bar.baz".
|
||||
* @param newValue A value to set at the path.
|
||||
*/
|
||||
export function cleanSet<A extends any[] | object>(
|
||||
source: A,
|
||||
keys: string | number | string[],
|
||||
newValue: any,
|
||||
): A {
|
||||
return cleanSetOrMerge(source, keys, newValue, false);
|
||||
}
|
||||
26
src_old/lib/fix-pmc.mjs
Normal file
26
src_old/lib/fix-pmc.mjs
Normal file
@@ -0,0 +1,26 @@
|
||||
import { options } from 'preact';
|
||||
|
||||
const classNameDescriptor = {
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
get () {
|
||||
return this.class;
|
||||
},
|
||||
set (value) {
|
||||
this.class = value;
|
||||
}
|
||||
};
|
||||
|
||||
const old = options.vnode;
|
||||
options.vnode = vnode => {
|
||||
const a = vnode.attributes;
|
||||
if (a != null) {
|
||||
if ('className' in a) {
|
||||
a.class = a.className;
|
||||
}
|
||||
if ('class' in a) {
|
||||
Object.defineProperty(a, 'className', classNameDescriptor);
|
||||
}
|
||||
}
|
||||
if (old != null) old(vnode);
|
||||
};
|
||||
107
src_old/lib/icons.tsx
Normal file
107
src_old/lib/icons.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import { h } from 'preact';
|
||||
|
||||
// tslint:disable:max-line-length variable-name
|
||||
|
||||
const Icon = (props: JSX.HTMLAttributes) => (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export const DownloadIcon = (props: JSX.HTMLAttributes) => (
|
||||
<Icon {...props}>
|
||||
<path d="M19 12v7H5v-7H3v7c0 1.1.9 2 2 2h14a2 2 0 0 0 2-2v-7h-2zm-6 .7l2.6-2.6 1.4 1.4-5 5-5-5 1.4-1.4 2.6 2.6V3h2z" />
|
||||
</Icon>
|
||||
);
|
||||
|
||||
export const ToggleBackgroundIcon = (props: JSX.HTMLAttributes) => (
|
||||
<Icon {...props}>
|
||||
<path d="M3 13h2v-2H3v2zm0 4h2v-2H3v2zm2 4v-2H3c0 1.1.9 2 2 2zM3 9h2V7H3v2zm12 12h2v-2h-2v2zm4-18H9a2 2 0 0 0-2 2v10c0 1.1.9 2 2 2h10a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2zm0 12H9V5h10v10zm-8 6h2v-2h-2v2zm-4 0h2v-2H7v2z" />
|
||||
</Icon>
|
||||
);
|
||||
|
||||
export const ToggleBackgroundActiveIcon = (props: JSX.HTMLAttributes) => (
|
||||
<Icon {...props}>
|
||||
<path d="M9 7H7v2h2V7zm0 4H7v2h2v-2zm0-8a2 2 0 0 0-2 2h2V3zm4 12h-2v2h2v-2zm6-12v2h2a2 2 0 0 0-2-2zm-6 0h-2v2h2V3zM9 17v-2H7c0 1.1.9 2 2 2zm10-4h2v-2h-2v2zm0-4h2V7h-2v2zm0 8a2 2 0 0 0 2-2h-2v2zM5 7H3v12c0 1.1.9 2 2 2h12v-2H5V7zm10-2h2V3h-2v2zm0 12h2v-2h-2v2z" />
|
||||
</Icon>
|
||||
);
|
||||
|
||||
export const RotateIcon = (props: JSX.HTMLAttributes) => (
|
||||
<Icon {...props}>
|
||||
<path d="M15.6 5.5L11 1v3a8 8 0 0 0 0 16v-2a6 6 0 0 1 0-12v4l4.5-4.5zm4.3 5.5a8 8 0 0 0-1.6-3.9L17 8.5c.5.8.9 1.6 1 2.5h2zM13 17.9v2a8 8 0 0 0 3.9-1.6L15.5 17c-.8.5-1.6.9-2.5 1zm3.9-2.4l1.4 1.4A8 8 0 0 0 20 13h-2c-.1.9-.5 1.7-1 2.5z" />
|
||||
</Icon>
|
||||
);
|
||||
|
||||
export const AddIcon = (props: JSX.HTMLAttributes) => (
|
||||
<Icon {...props}>
|
||||
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
|
||||
</Icon>
|
||||
);
|
||||
|
||||
export const RemoveIcon = (props: JSX.HTMLAttributes) => (
|
||||
<Icon {...props}>
|
||||
<path d="M19 13H5v-2h14v2z" />
|
||||
</Icon>
|
||||
);
|
||||
|
||||
export const UncheckedIcon = (props: JSX.HTMLAttributes) => (
|
||||
<Icon {...props}>
|
||||
<path d="M21.3 2.7v18.6H2.7V2.7h18.6m0-2.7H2.7A2.7 2.7 0 0 0 0 2.7v18.6A2.7 2.7 0 0 0 2.7 24h18.6a2.7 2.7 0 0 0 2.7-2.7V2.7A2.7 2.7 0 0 0 21.3 0z" />
|
||||
</Icon>
|
||||
);
|
||||
|
||||
export const CheckedIcon = (props: JSX.HTMLAttributes) => (
|
||||
<Icon {...props}>
|
||||
<path d="M21.3 0H2.7A2.7 2.7 0 0 0 0 2.7v18.6A2.7 2.7 0 0 0 2.7 24h18.6a2.7 2.7 0 0 0 2.7-2.7V2.7A2.7 2.7 0 0 0 21.3 0zm-12 18.7L2.7 12l1.8-1.9L9.3 15 19.5 4.8l1.8 1.9z" />
|
||||
</Icon>
|
||||
);
|
||||
|
||||
export const ExpandIcon = (props: JSX.HTMLAttributes) => (
|
||||
<Icon {...props}>
|
||||
<path d="M16.6 8.6L12 13.2 7.4 8.6 6 10l6 6 6-6z" />
|
||||
</Icon>
|
||||
);
|
||||
|
||||
export const BackIcon = (props: JSX.HTMLAttributes) => (
|
||||
<Icon {...props}>
|
||||
<path d="M20 11H7.8l5.6-5.6L12 4l-8 8 8 8 1.4-1.4L7.8 13H20v-2z" />
|
||||
</Icon>
|
||||
);
|
||||
|
||||
const copyAcrossRotations = {
|
||||
up: 90,
|
||||
right: 180,
|
||||
down: -90,
|
||||
left: 0,
|
||||
};
|
||||
|
||||
export interface CopyAcrossIconProps extends JSX.HTMLAttributes {
|
||||
copyDirection: keyof typeof copyAcrossRotations;
|
||||
}
|
||||
|
||||
export const CopyAcrossIcon = (props: CopyAcrossIconProps) => {
|
||||
const { copyDirection, ...otherProps } = props;
|
||||
const id = 'point-' + copyDirection;
|
||||
const rotation = copyAcrossRotations[copyDirection];
|
||||
|
||||
return (
|
||||
<Icon {...otherProps}>
|
||||
<defs>
|
||||
<clipPath id={id}>
|
||||
<path
|
||||
d="M-12-12v24h24v-24zM4.5 2h-4v3l-5-5 5-5v3h4z"
|
||||
transform={`translate(12 13) rotate(${rotation})`}
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<path
|
||||
clip-path={`url(#${id})`}
|
||||
d="M19 3h-4.2c-.4-1.2-1.5-2-2.8-2s-2.4.8-2.8 2H5a2 2 0 0 0-2 2v14c0 1.1.9 2 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2zm-7 0a1 1 0 0 1 0 2c-.6 0-1-.4-1-1s.4-1 1-1z"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
};
|
||||
57
src_old/lib/initial-util.ts
Normal file
57
src_old/lib/initial-util.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @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;
|
||||
}
|
||||
|
||||
// Edge doesn't support `new File`, so here's a hacky alternative.
|
||||
// https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/9551546/
|
||||
export class Fileish extends Blob {
|
||||
constructor(data: any[], public name: string, opts?: BlobPropertyBag) {
|
||||
super(data, opts);
|
||||
}
|
||||
}
|
||||
7
src_old/lib/missing-types.d.ts
vendored
Normal file
7
src_old/lib/missing-types.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
interface HTMLImageElement {
|
||||
decode: (() => Promise<void>) | undefined;
|
||||
}
|
||||
|
||||
interface CanvasRenderingContext2D {
|
||||
imageSmoothingQuality: 'low' | 'medium' | 'high';
|
||||
}
|
||||
115
src_old/lib/sw-bridge.ts
Normal file
115
src_old/lib/sw-bridge.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { get, set } from 'idb-keyval';
|
||||
|
||||
// Just for TypeScript
|
||||
import SnackBarElement from './SnackBar';
|
||||
|
||||
/** Tell the service worker to skip waiting */
|
||||
async function skipWaiting() {
|
||||
const reg = await navigator.serviceWorker.getRegistration();
|
||||
if (!reg || !reg.waiting) return;
|
||||
reg.waiting.postMessage('skip-waiting');
|
||||
}
|
||||
|
||||
/** Find the service worker that's 'active' or closest to 'active' */
|
||||
async function getMostActiveServiceWorker() {
|
||||
const reg = await navigator.serviceWorker.getRegistration();
|
||||
if (!reg) return null;
|
||||
return reg.active || reg.waiting || reg.installing;
|
||||
}
|
||||
|
||||
/** Wait for an installing worker */
|
||||
async function installingWorker(
|
||||
reg: ServiceWorkerRegistration,
|
||||
): Promise<ServiceWorker> {
|
||||
if (reg.installing) return reg.installing;
|
||||
return new Promise<ServiceWorker>((resolve) => {
|
||||
reg.addEventListener('updatefound', () => resolve(reg.installing!), {
|
||||
once: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Wait a service worker to become waiting */
|
||||
async function updateReady(reg: ServiceWorkerRegistration): Promise<void> {
|
||||
if (reg.waiting) return;
|
||||
const installing = await installingWorker(reg);
|
||||
return new Promise<void>((resolve) => {
|
||||
installing.addEventListener('statechange', () => {
|
||||
if (installing.state === 'installed') resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Wait for a shared image */
|
||||
export function getSharedImage(): Promise<File> {
|
||||
return new Promise((resolve) => {
|
||||
const onmessage = (event: MessageEvent) => {
|
||||
if (event.data.action !== 'load-image') return;
|
||||
resolve(event.data.file);
|
||||
navigator.serviceWorker.removeEventListener('message', onmessage);
|
||||
};
|
||||
|
||||
navigator.serviceWorker.addEventListener('message', onmessage);
|
||||
|
||||
// This message is picked up by the service worker - it's how it knows we're ready to receive
|
||||
// the file.
|
||||
navigator.serviceWorker.controller!.postMessage('share-ready');
|
||||
});
|
||||
}
|
||||
|
||||
/** Set up the service worker and monitor changes */
|
||||
export async function offliner(showSnack: SnackBarElement['showSnackbar']) {
|
||||
// This needs to be a typeof because Webpack.
|
||||
if (typeof PRERENDER === 'boolean') return;
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
navigator.serviceWorker.register('../sw');
|
||||
}
|
||||
|
||||
const hasController = !!navigator.serviceWorker.controller;
|
||||
|
||||
// Look for changes in the controller
|
||||
navigator.serviceWorker.addEventListener('controllerchange', async () => {
|
||||
// Is it the first install?
|
||||
if (!hasController) {
|
||||
showSnack('Ready to work offline', { timeout: 5000 });
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise reload (the user will have agreed to this).
|
||||
location.reload();
|
||||
});
|
||||
|
||||
// If we don't have a controller, we don't need to check for updates – we've just loaded from the
|
||||
// network.
|
||||
if (!hasController) return;
|
||||
|
||||
const reg = await navigator.serviceWorker.getRegistration();
|
||||
// Service worker not registered yet.
|
||||
if (!reg) return;
|
||||
// Look for updates
|
||||
await updateReady(reg);
|
||||
|
||||
// Ask the user if they want to update.
|
||||
const result = await showSnack('Update available', {
|
||||
actions: ['reload', 'dismiss'],
|
||||
});
|
||||
|
||||
// Tell the waiting worker to activate, this will change the controller and cause a reload (see
|
||||
// 'controllerchange')
|
||||
if (result === 'reload') skipWaiting();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell the service worker the main app has loaded. If it's the first time the service worker has
|
||||
* heard about this, cache the heavier assets like codecs.
|
||||
*/
|
||||
export async function mainAppLoaded() {
|
||||
// If the user has already interacted, no need to tell the service worker anything.
|
||||
const userInteracted = await get<boolean | undefined>('user-interacted');
|
||||
if (userInteracted) return;
|
||||
set('user-interacted', true);
|
||||
const serviceWorker = await getMostActiveServiceWorker();
|
||||
if (!serviceWorker) return; // Service worker not installing yet.
|
||||
serviceWorker.postMessage('cache-all');
|
||||
}
|
||||
22
src_old/lib/util.scss
Normal file
22
src_old/lib/util.scss
Normal file
@@ -0,0 +1,22 @@
|
||||
.abs-fill {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
contain: strict;
|
||||
}
|
||||
|
||||
.unbutton {
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: none;
|
||||
font: inherit;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
355
src_old/lib/util.ts
Normal file
355
src_old/lib/util.ts
Normal file
@@ -0,0 +1,355 @@
|
||||
/** Compare two objects, returning a boolean indicating if
|
||||
* they have the same properties and strictly equal values.
|
||||
*/
|
||||
export function shallowEqual(one: any, two: any) {
|
||||
for (const i in one) if (one[i] !== two[i]) return false;
|
||||
for (const i in two) if (!(i in one)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Replace the contents of a canvas with the given data */
|
||||
export function drawDataToCanvas(canvas: HTMLCanvasElement, data: ImageData) {
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) throw Error('Canvas not initialized');
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.putImageData(data, 0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode some image data in a given format using the browser's encoders
|
||||
*
|
||||
* @param {ImageData} data
|
||||
* @param {string} type A mime type, eg image/jpeg.
|
||||
* @param {number} [quality] Between 0-1.
|
||||
*/
|
||||
export async function canvasEncode(
|
||||
data: ImageData,
|
||||
type: string,
|
||||
quality?: number,
|
||||
): Promise<Blob> {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = data.width;
|
||||
canvas.height = data.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) throw Error('Canvas not initialized');
|
||||
ctx.putImageData(data, 0, 0);
|
||||
|
||||
let blob: Blob | null;
|
||||
|
||||
if ('toBlob' in canvas) {
|
||||
blob = await new Promise<Blob | null>((r) =>
|
||||
canvas.toBlob(r, type, quality),
|
||||
);
|
||||
} else {
|
||||
// Welcome to Edge.
|
||||
// TypeScript thinks `canvas` is 'never', so it needs casting.
|
||||
const dataUrl = (canvas as HTMLCanvasElement).toDataURL(type, quality);
|
||||
const result = /data:([^;]+);base64,(.*)$/.exec(dataUrl);
|
||||
|
||||
if (!result) throw Error('Data URL reading failed');
|
||||
|
||||
const outputType = result[1];
|
||||
const binaryStr = atob(result[2]);
|
||||
const data = new Uint8Array(binaryStr.length);
|
||||
|
||||
for (let i = 0; i < data.length; i += 1) {
|
||||
data[i] = binaryStr.charCodeAt(i);
|
||||
}
|
||||
|
||||
blob = new Blob([data], { type: outputType });
|
||||
}
|
||||
|
||||
if (!blob) throw Error('Encoding failed');
|
||||
return blob;
|
||||
}
|
||||
|
||||
async function decodeImage(url: string): Promise<HTMLImageElement> {
|
||||
const img = new Image();
|
||||
img.decoding = 'async';
|
||||
img.src = url;
|
||||
const loaded = new Promise((resolve, reject) => {
|
||||
img.onload = () => resolve();
|
||||
img.onerror = () => reject(Error('Image loading error'));
|
||||
});
|
||||
|
||||
if (img.decode) {
|
||||
// Nice off-thread way supported in Safari/Chrome.
|
||||
// Safari throws on decode if the source is SVG.
|
||||
// https://bugs.webkit.org/show_bug.cgi?id=188347
|
||||
await img.decode().catch(() => null);
|
||||
}
|
||||
|
||||
// Always await loaded, as we may have bailed due to the Safari bug above.
|
||||
await loaded;
|
||||
return img;
|
||||
}
|
||||
|
||||
/** Caches results from canDecodeImageType */
|
||||
const canDecodeCache = new Map<string, Promise<boolean>>();
|
||||
|
||||
/**
|
||||
* Tests whether the browser supports a particular image mime type.
|
||||
*
|
||||
* @param type Mimetype
|
||||
* @example await canDecodeImageType('image/avif')
|
||||
*/
|
||||
export function canDecodeImageType(type: string): Promise<boolean> {
|
||||
if (!canDecodeCache.has(type)) {
|
||||
const resultPromise = (async () => {
|
||||
const picture = document.createElement('picture');
|
||||
const img = document.createElement('img');
|
||||
const source = document.createElement('source');
|
||||
source.srcset = 'data:,x';
|
||||
source.type = type;
|
||||
picture.append(source, img);
|
||||
|
||||
// Wait a single microtick just for the `img.currentSrc` to get populated.
|
||||
await 0;
|
||||
// At this point `img.currentSrc` will contain "data:,x" if format is supported and ""
|
||||
// otherwise.
|
||||
return !!img.currentSrc;
|
||||
})();
|
||||
|
||||
canDecodeCache.set(type, resultPromise);
|
||||
}
|
||||
|
||||
return canDecodeCache.get(type)!;
|
||||
}
|
||||
|
||||
export function blobToArrayBuffer(blob: Blob): Promise<ArrayBuffer> {
|
||||
return new Response(blob).arrayBuffer();
|
||||
}
|
||||
|
||||
export function blobToText(blob: Blob): Promise<string> {
|
||||
return new Response(blob).text();
|
||||
}
|
||||
|
||||
const magicNumberToMimeType = new Map<RegExp, string>([
|
||||
[/^%PDF-/, 'application/pdf'],
|
||||
[/^GIF87a/, 'image/gif'],
|
||||
[/^GIF89a/, 'image/gif'],
|
||||
[/^\x89PNG\x0D\x0A\x1A\x0A/, 'image/png'],
|
||||
[/^\xFF\xD8\xFF/, 'image/jpeg'],
|
||||
[/^BM/, 'image/bmp'],
|
||||
[/^I I/, 'image/tiff'],
|
||||
[/^II*/, 'image/tiff'],
|
||||
[/^MM\x00*/, 'image/tiff'],
|
||||
[/^RIFF....WEBPVP8[LX ]/, 'image/webp'],
|
||||
[/^\x00\x00\x00 ftypavif\x00\x00\x00\x00/, 'image/avif'],
|
||||
]);
|
||||
|
||||
export async function sniffMimeType(blob: Blob): Promise<string> {
|
||||
const firstChunk = await blobToArrayBuffer(blob.slice(0, 16));
|
||||
const firstChunkString = Array.from(new Uint8Array(firstChunk))
|
||||
.map((v) => String.fromCodePoint(v))
|
||||
.join('');
|
||||
for (const [detector, mimeType] of magicNumberToMimeType) {
|
||||
if (detector.test(firstChunkString)) {
|
||||
return mimeType;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
export async function blobToImg(blob: Blob): Promise<HTMLImageElement> {
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
try {
|
||||
return await decodeImage(url);
|
||||
} finally {
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
}
|
||||
|
||||
interface DrawableToImageDataOptions {
|
||||
width?: number;
|
||||
height?: number;
|
||||
sx?: number;
|
||||
sy?: number;
|
||||
sw?: number;
|
||||
sh?: number;
|
||||
}
|
||||
|
||||
export function drawableToImageData(
|
||||
drawable: ImageBitmap | HTMLImageElement,
|
||||
opts: DrawableToImageDataOptions = {},
|
||||
): ImageData {
|
||||
const {
|
||||
width = drawable.width,
|
||||
height = drawable.height,
|
||||
sx = 0,
|
||||
sy = 0,
|
||||
sw = drawable.width,
|
||||
sh = drawable.height,
|
||||
} = opts;
|
||||
|
||||
// Make canvas same size as image
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
// Draw image onto canvas
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) throw new Error('Could not create canvas context');
|
||||
ctx.drawImage(drawable, sx, sy, sw, sh, 0, 0, width, height);
|
||||
return ctx.getImageData(0, 0, width, height);
|
||||
}
|
||||
|
||||
export async function builtinDecode(blob: Blob): Promise<ImageData> {
|
||||
// Prefer createImageBitmap as it's the off-thread option for Firefox.
|
||||
const drawable =
|
||||
'createImageBitmap' in self
|
||||
? await createImageBitmap(blob)
|
||||
: await blobToImg(blob);
|
||||
|
||||
return drawableToImageData(drawable);
|
||||
}
|
||||
|
||||
export type BuiltinResizeMethod = 'pixelated' | 'low' | 'medium' | 'high';
|
||||
|
||||
export function builtinResize(
|
||||
data: ImageData,
|
||||
sx: number,
|
||||
sy: number,
|
||||
sw: number,
|
||||
sh: number,
|
||||
dw: number,
|
||||
dh: number,
|
||||
method: BuiltinResizeMethod,
|
||||
): ImageData {
|
||||
const canvasSource = document.createElement('canvas');
|
||||
canvasSource.width = data.width;
|
||||
canvasSource.height = data.height;
|
||||
drawDataToCanvas(canvasSource, data);
|
||||
|
||||
const canvasDest = document.createElement('canvas');
|
||||
canvasDest.width = dw;
|
||||
canvasDest.height = dh;
|
||||
const ctx = canvasDest.getContext('2d');
|
||||
if (!ctx) throw new Error('Could not create canvas context');
|
||||
|
||||
if (method === 'pixelated') {
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
} else {
|
||||
ctx.imageSmoothingQuality = method;
|
||||
}
|
||||
|
||||
ctx.drawImage(canvasSource, sx, sy, sw, sh, 0, 0, dw, dh);
|
||||
return ctx.getImageData(0, 0, dw, dh);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param field An HTMLInputElement, but the casting is done here to tidy up onChange.
|
||||
* @param defaultVal Value to return if 'field' doesn't exist.
|
||||
*/
|
||||
export function inputFieldValueAsNumber(
|
||||
field: any,
|
||||
defaultVal: number = 0,
|
||||
): number {
|
||||
if (!field) return defaultVal;
|
||||
return Number(inputFieldValue(field));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param field An HTMLInputElement, but the casting is done here to tidy up onChange.
|
||||
* @param defaultVal Value to return if 'field' doesn't exist.
|
||||
*/
|
||||
export function inputFieldCheckedAsNumber(
|
||||
field: any,
|
||||
defaultVal: number = 0,
|
||||
): number {
|
||||
if (!field) return defaultVal;
|
||||
return Number(inputFieldChecked(field));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param field An HTMLInputElement, but the casting is done here to tidy up onChange.
|
||||
* @param defaultVal Value to return if 'field' doesn't exist.
|
||||
*/
|
||||
export function inputFieldChecked(
|
||||
field: any,
|
||||
defaultVal: boolean = false,
|
||||
): boolean {
|
||||
if (!field) return defaultVal;
|
||||
return (field as HTMLInputElement).checked;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param field An HTMLInputElement, but the casting is done here to tidy up onChange.
|
||||
* @param defaultVal Value to return if 'field' doesn't exist.
|
||||
*/
|
||||
export function inputFieldValue(field: any, defaultVal: string = ''): string {
|
||||
if (!field) return defaultVal;
|
||||
return (field as HTMLInputElement).value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a promise that resolves when the user types the konami code.
|
||||
*/
|
||||
export function konami(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
// Keycodes for: ↑ ↑ ↓ ↓ ← → ← → B A
|
||||
const expectedPattern = '38384040373937396665';
|
||||
let rollingPattern = '';
|
||||
|
||||
const listener = (event: KeyboardEvent) => {
|
||||
rollingPattern += event.keyCode;
|
||||
rollingPattern = rollingPattern.slice(-expectedPattern.length);
|
||||
if (rollingPattern === expectedPattern) {
|
||||
window.removeEventListener('keydown', listener);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', listener);
|
||||
});
|
||||
}
|
||||
|
||||
interface TransitionOptions {
|
||||
from?: number;
|
||||
to?: number;
|
||||
duration?: number;
|
||||
easing?: string;
|
||||
}
|
||||
|
||||
export async function transitionHeight(
|
||||
el: HTMLElement,
|
||||
opts: TransitionOptions,
|
||||
): Promise<void> {
|
||||
const {
|
||||
from = el.getBoundingClientRect().height,
|
||||
to = el.getBoundingClientRect().height,
|
||||
duration = 1000,
|
||||
easing = 'ease-in-out',
|
||||
} = opts;
|
||||
|
||||
if (from === to || duration === 0) {
|
||||
el.style.height = to + 'px';
|
||||
return;
|
||||
}
|
||||
|
||||
el.style.height = from + 'px';
|
||||
// Force a style calc so the browser picks up the start value.
|
||||
getComputedStyle(el).transform;
|
||||
el.style.transition = `height ${duration}ms ${easing}`;
|
||||
el.style.height = to + 'px';
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
const listener = (event: Event) => {
|
||||
if (event.target !== el) return;
|
||||
el.style.transition = '';
|
||||
el.removeEventListener('transitionend', listener);
|
||||
el.removeEventListener('transitioncancel', listener);
|
||||
resolve();
|
||||
};
|
||||
|
||||
el.addEventListener('transitionend', listener);
|
||||
el.addEventListener('transitioncancel', listener);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple event listener that prevents the default.
|
||||
*/
|
||||
export function preventDefault(event: Event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
Reference in New Issue
Block a user