mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-13 01:07:18 +00:00
471 lines
15 KiB
TypeScript
471 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 'url:./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.footerLink}
|
|
href="https://github.com/GoogleChromeLabs/squoosh/tree/dev/cli"
|
|
>
|
|
Squoosh CLI
|
|
</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>
|
|
);
|
|
}
|
|
}
|