mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-16 18:49:50 +00:00
Abstracting results so it can be used as a heading.
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import { h, Component } from 'preact';
|
import { h, Component } from 'preact';
|
||||||
|
|
||||||
import * as style from './style.scss';
|
import * as style from './style.scss';
|
||||||
import { bind, Fileish } from '../../lib/initial-util';
|
import { bind } from '../../lib/initial-util';
|
||||||
import { cleanSet, cleanMerge } from '../../lib/clean-modify';
|
import { cleanSet, cleanMerge } from '../../lib/clean-modify';
|
||||||
import OptiPNGEncoderOptions from '../../codecs/optipng/options';
|
import OptiPNGEncoderOptions from '../../codecs/optipng/options';
|
||||||
import MozJpegEncoderOptions from '../../codecs/mozjpeg/options';
|
import MozJpegEncoderOptions from '../../codecs/mozjpeg/options';
|
||||||
@@ -35,13 +35,10 @@ import {
|
|||||||
import { QuantizeOptions } from '../../codecs/imagequant/processor-meta';
|
import { QuantizeOptions } from '../../codecs/imagequant/processor-meta';
|
||||||
import { ResizeOptions } from '../../codecs/resize/processor-meta';
|
import { ResizeOptions } from '../../codecs/resize/processor-meta';
|
||||||
import { PreprocessorState } from '../../codecs/preprocessors';
|
import { PreprocessorState } from '../../codecs/preprocessors';
|
||||||
import FileSize from './FileSize';
|
|
||||||
import { DownloadIcon } from '../../lib/icons';
|
|
||||||
import { SourceImage } from '../App';
|
import { SourceImage } from '../App';
|
||||||
import Checkbox from '../checkbox';
|
import Checkbox from '../checkbox';
|
||||||
import Expander from '../expander';
|
import Expander from '../expander';
|
||||||
import Select from '../select';
|
import Select from '../select';
|
||||||
import '../custom-els/LoadingSpinner';
|
|
||||||
|
|
||||||
const encoderOptionsComponentMap = {
|
const encoderOptionsComponentMap = {
|
||||||
[identity.type]: undefined,
|
[identity.type]: undefined,
|
||||||
@@ -61,11 +58,8 @@ const encoderOptionsComponentMap = {
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mobileView: boolean;
|
mobileView: boolean;
|
||||||
loading: boolean;
|
|
||||||
source?: SourceImage;
|
source?: SourceImage;
|
||||||
imageIndex: number;
|
imageIndex: number;
|
||||||
imageFile?: Fileish;
|
|
||||||
downloadUrl?: string;
|
|
||||||
encoderState: EncoderState;
|
encoderState: EncoderState;
|
||||||
preprocessorState: PreprocessorState;
|
preprocessorState: PreprocessorState;
|
||||||
onEncoderTypeChange(newType: EncoderType): void;
|
onEncoderTypeChange(newType: EncoderType): void;
|
||||||
@@ -76,39 +70,18 @@ interface Props {
|
|||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
encoderSupportMap?: EncoderSupportMap;
|
encoderSupportMap?: EncoderSupportMap;
|
||||||
showLoadingState: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadingReactionDelay = 500;
|
|
||||||
|
|
||||||
export default class Options extends Component<Props, State> {
|
export default class Options extends Component<Props, State> {
|
||||||
state: State = {
|
state: State = {
|
||||||
encoderSupportMap: undefined,
|
encoderSupportMap: undefined,
|
||||||
showLoadingState: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** The timeout ID between entering the loading state, and changing UI */
|
|
||||||
private loadingTimeoutId: number = 0;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
encodersSupported.then(encoderSupportMap => this.setState({ encoderSupportMap }));
|
encodersSupported.then(encoderSupportMap => this.setState({ encoderSupportMap }));
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Props, prevState: State) {
|
|
||||||
if (prevProps.loading && !this.props.loading) {
|
|
||||||
// Just stopped loading
|
|
||||||
clearTimeout(this.loadingTimeoutId);
|
|
||||||
this.setState({ showLoadingState: false });
|
|
||||||
} else if (!prevProps.loading && this.props.loading) {
|
|
||||||
// Just started loading
|
|
||||||
this.loadingTimeoutId = self.setTimeout(
|
|
||||||
() => this.setState({ showLoadingState: true }),
|
|
||||||
loadingReactionDelay,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
onEncoderTypeChange(event: Event) {
|
onEncoderTypeChange(event: Event) {
|
||||||
const el = event.currentTarget as HTMLSelectElement;
|
const el = event.currentTarget as HTMLSelectElement;
|
||||||
@@ -153,19 +126,17 @@ export default class Options extends Component<Props, State> {
|
|||||||
{
|
{
|
||||||
source,
|
source,
|
||||||
imageIndex,
|
imageIndex,
|
||||||
imageFile,
|
|
||||||
downloadUrl,
|
|
||||||
encoderState,
|
encoderState,
|
||||||
preprocessorState,
|
preprocessorState,
|
||||||
onEncoderOptionsChange,
|
onEncoderOptionsChange,
|
||||||
}: Props,
|
}: Props,
|
||||||
{ encoderSupportMap, showLoadingState }: State,
|
{ encoderSupportMap }: State,
|
||||||
) {
|
) {
|
||||||
// tslint:disable variable-name
|
// tslint:disable variable-name
|
||||||
const EncoderOptionComponent = encoderOptionsComponentMap[encoderState.type];
|
const EncoderOptionComponent = encoderOptionsComponentMap[encoderState.type];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={style.options}>
|
<div>
|
||||||
<Expander>
|
<Expander>
|
||||||
{encoderState.type === identity.type ? null :
|
{encoderState.type === identity.type ? null :
|
||||||
<div>
|
<div>
|
||||||
@@ -244,33 +215,6 @@ export default class Options extends Component<Props, State> {
|
|||||||
{imageIndex === 0 && ' →'}
|
{imageIndex === 0 && ' →'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class={style.results}>
|
|
||||||
<div class={style.resultData}>
|
|
||||||
{!imageFile || showLoadingState ? 'Working…' :
|
|
||||||
<FileSize
|
|
||||||
blob={imageFile}
|
|
||||||
compareTo={(source && imageFile !== source.file) ? source.file : undefined}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class={style.download}>
|
|
||||||
{(downloadUrl && imageFile) && (
|
|
||||||
<a
|
|
||||||
class={`${style.downloadLink} ${showLoadingState ? style.downloadLinkDisable : ''}`}
|
|
||||||
href={downloadUrl}
|
|
||||||
download={imageFile.name}
|
|
||||||
title="Download"
|
|
||||||
>
|
|
||||||
<DownloadIcon class={style.downloadIcon} />
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
{showLoadingState && <loading-spinner class={style.spinner} />}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,5 @@
|
|||||||
$horizontalPadding: 15px;
|
$horizontalPadding: 15px;
|
||||||
|
|
||||||
.options {
|
|
||||||
color: #fff;
|
|
||||||
opacity: 0.9;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
max-height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column;
|
|
||||||
max-width: 400px;
|
|
||||||
margin: 0 auto;
|
|
||||||
width: calc(100% - 60px);
|
|
||||||
|
|
||||||
@media (min-width: 600px) {
|
|
||||||
width: 300px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.options-title {
|
.options-title {
|
||||||
background: rgba(0, 0, 0, 0.9);
|
background: rgba(0, 0, 0, 0.9);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -73,35 +56,6 @@ $horizontalPadding: 15px;
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.results {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr auto;
|
|
||||||
background: rgba(0, 0, 0, 0.9);
|
|
||||||
font-size: 1.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-data {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0 $horizontalPadding;
|
|
||||||
}
|
|
||||||
|
|
||||||
.size-delta {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-style: italic;
|
|
||||||
position: relative;
|
|
||||||
top: -1px;
|
|
||||||
margin-left: 0.3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.size-increase {
|
|
||||||
color: #e35050;
|
|
||||||
}
|
|
||||||
|
|
||||||
.size-decrease {
|
|
||||||
color: #50e3c2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.options-copy {
|
.options-copy {
|
||||||
display: grid;
|
display: grid;
|
||||||
background: rgba(0, 0, 0, 0.9);
|
background: rgba(0, 0, 0, 0.9);
|
||||||
@@ -116,58 +70,3 @@ $horizontalPadding: 15px;
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes action-enter {
|
|
||||||
from {
|
|
||||||
transform: rotate(-90deg);
|
|
||||||
opacity: 0;
|
|
||||||
animation-timing-function: ease-out;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes action-leave {
|
|
||||||
from {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
opacity: 1;
|
|
||||||
animation-timing-function: ease-out;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.download {
|
|
||||||
background: #34B9EB;
|
|
||||||
--size: 38px;
|
|
||||||
width: var(--size);
|
|
||||||
height: var(--size);
|
|
||||||
display: grid;
|
|
||||||
align-items: center;
|
|
||||||
justify-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-link {
|
|
||||||
animation: action-enter 0.2s;
|
|
||||||
grid-area: 1/1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-link-disable {
|
|
||||||
pointer-events: none;
|
|
||||||
opacity: 0;
|
|
||||||
transform: rotate(90deg);
|
|
||||||
animation: action-leave 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-icon {
|
|
||||||
color: #fff;
|
|
||||||
display: block;
|
|
||||||
--size: 24px;
|
|
||||||
width: var(--size);
|
|
||||||
height: var(--size);
|
|
||||||
padding: 7px;
|
|
||||||
filter: drop-shadow(0 1px 0 rgba(0, 0, 0, 0.7));
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner {
|
|
||||||
--color: #fff;
|
|
||||||
--delay: 0;
|
|
||||||
--size: 22px;
|
|
||||||
grid-area: 1/1;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import { cleanMerge, cleanSet } from '../../lib/clean-modify';
|
|||||||
import Processor from '../../codecs/processor';
|
import Processor from '../../codecs/processor';
|
||||||
import { VectorResizeOptions, BitmapResizeOptions } from '../../codecs/resize/processor-meta';
|
import { VectorResizeOptions, BitmapResizeOptions } from '../../codecs/resize/processor-meta';
|
||||||
import './custom-els/MultiPanel';
|
import './custom-els/MultiPanel';
|
||||||
|
import Results from '../results';
|
||||||
|
|
||||||
export interface SourceImage {
|
export interface SourceImage {
|
||||||
file: File | Fileish;
|
file: File | Fileish;
|
||||||
@@ -398,12 +399,9 @@ export default class Compress extends Component<Props, State> {
|
|||||||
|
|
||||||
const options = images.map((image, index) => (
|
const options = images.map((image, index) => (
|
||||||
<Options
|
<Options
|
||||||
loading={loading || image.loading}
|
|
||||||
source={source}
|
source={source}
|
||||||
mobileView={mobileView}
|
mobileView={mobileView}
|
||||||
imageIndex={index}
|
imageIndex={index}
|
||||||
imageFile={image.file}
|
|
||||||
downloadUrl={image.downloadUrl}
|
|
||||||
preprocessorState={image.preprocessorState}
|
preprocessorState={image.preprocessorState}
|
||||||
encoderState={image.encoderState}
|
encoderState={image.encoderState}
|
||||||
onEncoderTypeChange={this.onEncoderTypeChange.bind(this, index)}
|
onEncoderTypeChange={this.onEncoderTypeChange.bind(this, index)}
|
||||||
@@ -413,6 +411,15 @@ export default class Compress extends Component<Props, State> {
|
|||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
const results = images.map(image => (
|
||||||
|
<Results
|
||||||
|
downloadUrl={image.downloadUrl}
|
||||||
|
imageFile={image.file}
|
||||||
|
source={source}
|
||||||
|
loading={loading || image.loading}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={style.compress}>
|
<div class={style.compress}>
|
||||||
<Output
|
<Output
|
||||||
@@ -425,13 +432,24 @@ export default class Compress extends Component<Props, State> {
|
|||||||
/>
|
/>
|
||||||
{mobileView
|
{mobileView
|
||||||
? (
|
? (
|
||||||
|
<div class={style.options}>
|
||||||
<multi-panel class={style.multiPanel} open-one-only>
|
<multi-panel class={style.multiPanel} open-one-only>
|
||||||
<div>Top</div>
|
{results[0]}
|
||||||
{options[0]}
|
{options[0]}
|
||||||
<div>Bottom</div>
|
{results[1]}
|
||||||
{options[1]}
|
{options[1]}
|
||||||
</multi-panel>
|
</multi-panel>
|
||||||
) : options
|
</div>
|
||||||
|
) : ([
|
||||||
|
<div class={style.options} key="options0">
|
||||||
|
{options[0]}
|
||||||
|
{results[0]}
|
||||||
|
</div>,
|
||||||
|
<div class={style.options} key="options1">
|
||||||
|
{options[1]}
|
||||||
|
{results[1]}
|
||||||
|
</div>,
|
||||||
|
])
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,6 +17,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.options {
|
||||||
|
color: #fff;
|
||||||
|
opacity: 0.9;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
max-height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: calc(100% - 60px);
|
||||||
|
|
||||||
|
@media (min-width: 600px) {
|
||||||
|
width: 300px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.multi-panel {
|
.multi-panel {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|||||||
73
src/components/results/index.tsx
Normal file
73
src/components/results/index.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { h, Component } from 'preact';
|
||||||
|
|
||||||
|
import * as style from './style.scss';
|
||||||
|
import FileSize from './FileSize';
|
||||||
|
import { DownloadIcon } from '../../lib/icons';
|
||||||
|
import '../custom-els/LoadingSpinner';
|
||||||
|
import { SourceImage } from '../compress';
|
||||||
|
import { Fileish } from '../../lib/initial-util';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
loading: boolean;
|
||||||
|
source?: SourceImage;
|
||||||
|
imageFile?: Fileish;
|
||||||
|
downloadUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
showLoadingState: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadingReactionDelay = 500;
|
||||||
|
|
||||||
|
export default class Results extends Component<Props, State> {
|
||||||
|
state: State = {
|
||||||
|
showLoadingState: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** The timeout ID between entering the loading state, and changing UI */
|
||||||
|
private loadingTimeoutId: number = 0;
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: Props, prevState: State) {
|
||||||
|
if (prevProps.loading && !this.props.loading) {
|
||||||
|
// Just stopped loading
|
||||||
|
clearTimeout(this.loadingTimeoutId);
|
||||||
|
this.setState({ showLoadingState: false });
|
||||||
|
} else if (!prevProps.loading && this.props.loading) {
|
||||||
|
// Just started loading
|
||||||
|
this.loadingTimeoutId = self.setTimeout(
|
||||||
|
() => this.setState({ showLoadingState: true }),
|
||||||
|
loadingReactionDelay,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render({ source, imageFile, downloadUrl }: Props, { showLoadingState }: State) {
|
||||||
|
return (
|
||||||
|
<div class={style.results}>
|
||||||
|
<div class={style.resultData}>
|
||||||
|
{!imageFile || showLoadingState ? 'Working…' :
|
||||||
|
<FileSize
|
||||||
|
blob={imageFile}
|
||||||
|
compareTo={(source && imageFile !== source.file) ? source.file : undefined}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class={style.download}>
|
||||||
|
{(downloadUrl && imageFile) && (
|
||||||
|
<a
|
||||||
|
class={`${style.downloadLink} ${showLoadingState ? style.downloadLinkDisable : ''}`}
|
||||||
|
href={downloadUrl}
|
||||||
|
download={imageFile.name}
|
||||||
|
title="Download"
|
||||||
|
>
|
||||||
|
<DownloadIcon class={style.downloadIcon} />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{showLoadingState && <loading-spinner class={style.spinner} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
83
src/components/results/style.scss
Normal file
83
src/components/results/style.scss
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
@keyframes action-enter {
|
||||||
|
from {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
opacity: 0;
|
||||||
|
animation-timing-function: ease-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes action-leave {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
opacity: 1;
|
||||||
|
animation-timing-function: ease-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.results {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
background: rgba(0, 0, 0, 0.9);
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-data {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.size-delta {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-style: italic;
|
||||||
|
position: relative;
|
||||||
|
top: -1px;
|
||||||
|
margin-left: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.size-increase {
|
||||||
|
color: #e35050;
|
||||||
|
}
|
||||||
|
|
||||||
|
.size-decrease {
|
||||||
|
color: #50e3c2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download {
|
||||||
|
background: #34B9EB;
|
||||||
|
--size: 38px;
|
||||||
|
width: var(--size);
|
||||||
|
height: var(--size);
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
justify-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-link {
|
||||||
|
animation: action-enter 0.2s;
|
||||||
|
grid-area: 1/1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-link-disable {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
transform: rotate(90deg);
|
||||||
|
animation: action-leave 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-icon {
|
||||||
|
color: #fff;
|
||||||
|
display: block;
|
||||||
|
--size: 24px;
|
||||||
|
width: var(--size);
|
||||||
|
height: var(--size);
|
||||||
|
padding: 7px;
|
||||||
|
filter: drop-shadow(0 1px 0 rgba(0, 0, 0, 0.7));
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
--color: #fff;
|
||||||
|
--delay: 0;
|
||||||
|
--size: 22px;
|
||||||
|
grid-area: 1/1;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user