Files
squoosh/src/shared/prerendered-app/Intro/index.tsx
Jake Archibald 13a185def2 Remove CLI / libsquoosh (#1321)
Unfortunately, due to a few people leaving the team, and staffing issues resulting from the current economic climate (ugh), I'm deprecating the CLI and libsquoosh parts of Squoosh. The web app will continue to be supported and improved. I know that sucks, but there simply isn't the time & people to work on this. If anyone from the community wants to fork it, you have my blessing.
2023-01-03 14:44:54 +00:00

465 lines
15 KiB
TypeScript

import { h, Component } from 'preact';
import { linkRef } from 'shared/prerendered-app/util';
import '../../custom-els/loading-spinner';
import logo from 'url:./imgs/logo.svg';
import githubLogo from 'url:./imgs/github-logo.svg';
import largePhoto from 'url:./imgs/demos/demo-large-photo.jpg';
import artwork from 'url:./imgs/demos/demo-artwork.jpg';
import deviceScreen from 'url:./imgs/demos/demo-device-screen.png';
import largePhotoIcon from 'url:./imgs/demos/icon-demo-large-photo.jpg';
import artworkIcon from 'url:./imgs/demos/icon-demo-artwork.jpg';
import deviceScreenIcon from 'url:./imgs/demos/icon-demo-device-screen.jpg';
import smallSectionAsset from 'url:./imgs/info-content/small.svg';
import simpleSectionAsset from 'url:./imgs/info-content/simple.svg';
import secureSectionAsset from 'url:./imgs/info-content/secure.svg';
import logoIcon from 'url:./imgs/demos/icon-demo-logo.png';
import logoWithText from 'data-url-text:./imgs/logo-with-text.svg';
import * as style from './style.css';
import type SnackBarElement from 'shared/custom-els/snack-bar';
import 'shared/custom-els/snack-bar';
import { startBlobs } from './blob-anim/meta';
import SlideOnScroll from './SlideOnScroll';
const demos = [
{
description: 'Large photo',
size: '2.8mb',
filename: 'photo.jpg',
url: largePhoto,
iconUrl: largePhotoIcon,
},
{
description: 'Artwork',
size: '2.9mb',
filename: 'art.jpg',
url: artwork,
iconUrl: artworkIcon,
},
{
description: 'Device screen',
size: '1.6mb',
filename: 'pixel3.png',
url: deviceScreen,
iconUrl: deviceScreenIcon,
},
{
description: 'SVG icon',
size: '13k',
filename: 'squoosh.svg',
url: logo,
iconUrl: logoIcon,
},
] as const;
const blobAnimImport =
!__PRERENDER__ && matchMedia('(prefers-reduced-motion: reduce)').matches
? undefined
: import('./blob-anim');
const installButtonSource = 'introInstallButton-Purple';
const supportsClipboardAPI =
!__PRERENDER__ && navigator.clipboard && navigator.clipboard.read;
async function getImageClipboardItem(
items: ClipboardItem[],
): Promise<undefined | Blob> {
for (const item of items) {
const type = item.types.find((type) => type.startsWith('image/'));
if (type) return item.getType(type);
}
}
interface Props {
onFile?: (file: File) => void;
showSnack?: SnackBarElement['showSnackbar'];
}
interface State {
fetchingDemoIndex?: number;
beforeInstallEvent?: BeforeInstallPromptEvent;
showBlobSVG: boolean;
}
export default class Intro extends Component<Props, State> {
state: State = {
showBlobSVG: true,
};
private fileInput?: HTMLInputElement;
private blobCanvas?: HTMLCanvasElement;
private installingViaButton = false;
componentDidMount() {
// Listen for beforeinstallprompt events, indicating Squoosh is installable.
window.addEventListener(
'beforeinstallprompt',
this.onBeforeInstallPromptEvent,
);
// Listen for the appinstalled event, indicating Squoosh has been installed.
window.addEventListener('appinstalled', this.onAppInstalled);
if (blobAnimImport) {
blobAnimImport.then((module) => {
this.setState(
{
showBlobSVG: false,
},
() => module.startBlobAnim(this.blobCanvas!),
);
});
}
}
componentWillUnmount() {
window.removeEventListener(
'beforeinstallprompt',
this.onBeforeInstallPromptEvent,
);
window.removeEventListener('appinstalled', this.onAppInstalled);
}
private onFileChange = (event: Event): void => {
const fileInput = event.target as HTMLInputElement;
const file = fileInput.files && fileInput.files[0];
if (!file) return;
this.fileInput!.value = '';
this.props.onFile!(file);
};
private onOpenClick = () => {
this.fileInput!.click();
};
private onDemoClick = async (index: number, event: Event) => {
try {
this.setState({ fetchingDemoIndex: index });
const demo = demos[index];
const blob = await fetch(demo.url).then((r) => r.blob());
const file = new File([blob], demo.filename, { type: blob.type });
this.props.onFile!(file);
} catch (err) {
this.setState({ fetchingDemoIndex: undefined });
this.props.showSnack!("Couldn't fetch demo image");
}
};
private onBeforeInstallPromptEvent = (event: BeforeInstallPromptEvent) => {
// Don't show the mini-infobar on mobile
event.preventDefault();
// Save the beforeinstallprompt event so it can be called later.
this.setState({ beforeInstallEvent: event });
// Log the event.
const gaEventInfo = {
eventCategory: 'pwa-install',
eventAction: 'promo-shown',
nonInteraction: true,
};
ga('send', 'event', gaEventInfo);
};
private onInstallClick = async (event: Event) => {
// Get the deferred beforeinstallprompt event
const beforeInstallEvent = this.state.beforeInstallEvent;
// If there's no deferred prompt, bail.
if (!beforeInstallEvent) return;
this.installingViaButton = true;
// Show the browser install prompt
beforeInstallEvent.prompt();
// Wait for the user to accept or dismiss the install prompt
const { outcome } = await beforeInstallEvent.userChoice;
// Send the analytics data
const gaEventInfo = {
eventCategory: 'pwa-install',
eventAction: 'promo-clicked',
eventLabel: installButtonSource,
eventValue: outcome === 'accepted' ? 1 : 0,
};
ga('send', 'event', gaEventInfo);
// If the prompt was dismissed, we aren't going to install via the button.
if (outcome === 'dismissed') {
this.installingViaButton = false;
}
};
private onAppInstalled = () => {
// We don't need the install button, if it's shown
this.setState({ beforeInstallEvent: undefined });
// Don't log analytics if page is not visible
if (document.hidden) return;
// Try to get the install, if it's not set, use 'browser'
const source = this.installingViaButton ? installButtonSource : 'browser';
ga('send', 'event', 'pwa-install', 'installed', source);
// Clear the install method property
this.installingViaButton = false;
};
private onPasteClick = async () => {
let clipboardItems: ClipboardItem[];
try {
clipboardItems = await navigator.clipboard.read();
} catch (err) {
this.props.showSnack!(`No permission to access clipboard`);
return;
}
const blob = await getImageClipboardItem(clipboardItems);
if (!blob) {
this.props.showSnack!(`No image found in the clipboard`);
return;
}
this.props.onFile!(new File([blob], 'image.unknown'));
};
render(
{}: Props,
{ fetchingDemoIndex, beforeInstallEvent, showBlobSVG }: State,
) {
return (
<div class={style.intro}>
<input
class={style.hide}
ref={linkRef(this, 'fileInput')}
type="file"
onChange={this.onFileChange}
/>
<div class={style.main}>
{!__PRERENDER__ && (
<canvas
ref={linkRef(this, 'blobCanvas')}
class={style.blobCanvas}
/>
)}
<h1 class={style.logoContainer}>
<img
class={style.logo}
src={logoWithText}
alt="Squoosh"
width="539"
height="162"
/>
</h1>
<div class={style.loadImg}>
{showBlobSVG && (
<svg
class={style.blobSvg}
viewBox="-1.25 -1.25 2.5 2.5"
preserveAspectRatio="xMidYMid slice"
>
{startBlobs.map((points) => (
<path
d={points
.map((point, i) => {
const nextI = i === points.length - 1 ? 0 : i + 1;
let d = '';
if (i === 0) {
d += `M${point[2]} ${point[3]}`;
}
return (
d +
`C${point[4]} ${point[5]} ${points[nextI][0]} ${points[nextI][1]} ${points[nextI][2]} ${points[nextI][3]}`
);
})
.join('')}
/>
))}
</svg>
)}
<div
class={style.loadImgContent}
style={{ visibility: __PRERENDER__ ? 'hidden' : '' }}
>
<button class={style.loadBtn} onClick={this.onOpenClick}>
<svg viewBox="0 0 24 24" class={style.loadIcon}>
<path d="M19 7v3h-2V7h-3V5h3V2h2v3h3v2h-3zm-3 4V8h-3V5H5a2 2 0 00-2 2v12c0 1.1.9 2 2 2h12a2 2 0 002-2v-8h-3zM5 19l3-4 2 3 3-4 4 5H5z" />
</svg>
</button>
<div>
<span class={style.dropText}>Drop </span>OR{' '}
{supportsClipboardAPI ? (
<button class={style.pasteBtn} onClick={this.onPasteClick}>
Paste
</button>
) : (
'Paste'
)}
</div>
</div>
</div>
</div>
<div class={style.demosContainer}>
<svg viewBox="0 0 1920 140" class={style.topWave}>
<path
d="M1920 0l-107 28c-106 29-320 85-533 93-213 7-427-36-640-50s-427 0-533 7L0 85v171h1920z"
class={style.subWave}
/>
<path
d="M0 129l64-26c64-27 192-81 320-75 128 5 256 69 384 64 128-6 256-80 384-91s256 43 384 70c128 26 256 26 320 26h64v96H0z"
class={style.mainWave}
/>
</svg>
<div class={style.contentPadding}>
<p class={style.demoTitle}>
Or <strong>try one</strong> of these:
</p>
<ul class={style.demos}>
{demos.map((demo, i) => (
<li>
<button
class="unbutton"
onClick={(event) => this.onDemoClick(i, event)}
>
<div>
<div class={style.demoIconContainer}>
<img
class={style.demoIcon}
src={demo.iconUrl}
alt={demo.description}
/>
{fetchingDemoIndex === i && (
<div class={style.demoLoader}>
<loading-spinner />
</div>
)}
</div>
<div class={style.demoSize}>{demo.size}</div>
</div>
</button>
</li>
))}
</ul>
</div>
</div>
<div class={style.bottomWave}>
<svg viewBox="0 0 1920 79" class={style.topWave}>
<path
d="M0 59l64-11c64-11 192-34 320-43s256-5 384 4 256 23 384 34 256 21 384 14 256-30 320-41l64-11v94H0z"
class={style.infoWave}
/>
</svg>
</div>
<section class={style.info}>
<div class={style.infoContainer}>
<SlideOnScroll>
<div class={style.infoContent}>
<div class={style.infoTextWrapper}>
<h2 class={style.infoTitle}>Small</h2>
<p class={style.infoCaption}>
Smaller images mean faster load times. Squoosh can reduce
file size and maintain high quality.
</p>
</div>
<div class={style.infoImgWrapper}>
<img
class={style.infoImg}
src={smallSectionAsset}
alt="silhouette of a large 1.4 megabyte image shrunk into a smaller 80 kilobyte image"
width="536"
height="522"
/>
</div>
</div>
</SlideOnScroll>
</div>
</section>
<section class={style.info}>
<div class={style.infoContainer}>
<SlideOnScroll>
<div class={style.infoContent}>
<div class={style.infoTextWrapper}>
<h2 class={style.infoTitle}>Simple</h2>
<p class={style.infoCaption}>
Open your image, inspect the differences, then save
instantly. Feeling adventurous? Adjust the settings for even
smaller files.
</p>
</div>
<div class={style.infoImgWrapper}>
<img
class={style.infoImg}
src={simpleSectionAsset}
alt="grid of multiple shrunk images displaying various options"
width="538"
height="384"
/>
</div>
</div>
</SlideOnScroll>
</div>
</section>
<section class={style.info}>
<div class={style.infoContainer}>
<SlideOnScroll>
<div class={style.infoContent}>
<div class={style.infoTextWrapper}>
<h2 class={style.infoTitle}>Secure</h2>
<p class={style.infoCaption}>
Worried about privacy? Images never leave your device since
Squoosh does all the work locally.
</p>
</div>
<div class={style.infoImgWrapper}>
<img
class={style.infoImg}
src={secureSectionAsset}
alt="silhouette of a cloud with a 'no' symbol on top"
width="498"
height="333"
/>
</div>
</div>
</SlideOnScroll>
</div>
</section>
<footer class={style.footer}>
<div class={style.footerContainer}>
<svg viewBox="0 0 1920 79" class={style.topWave}>
<path
d="M0 59l64-11c64-11 192-34 320-43s256-5 384 4 256 23 384 34 256 21 384 14 256-30 320-41l64-11v94H0z"
class={style.footerWave}
/>
</svg>
<div class={style.footerPadding}>
<footer class={style.footerItems}>
<a
class={style.footerLink}
href="https://github.com/GoogleChromeLabs/squoosh/blob/dev/README.md#privacy"
>
Privacy
</a>
<a
class={style.footerLinkWithLogo}
href="https://github.com/GoogleChromeLabs/squoosh"
>
<img src={githubLogo} alt="" width="10" height="10" />
Source on Github
</a>
</footer>
</div>
</div>
</footer>
{beforeInstallEvent && (
<button class={style.installBtn} onClick={this.onInstallClick}>
Install
</button>
)}
</div>
);
}
}