Skip to content

Commit

Permalink
Options UI (#135)
Browse files Browse the repository at this point in the history
* Move gzipped size calculations into a worker and wrap it up in a `<GzipSize />` component that will also handle showing % of original size once that info is plumbed

* A couple tweaks for the app welcome (drop files) screen. We don't have mocks for this one, but this is at least a minor improvement.

* Prettier "pop" effect and styling for the drop zone/indicator.

* Styling for the quantization toggle to make it look like a disclosure triangle/button.

* Add controls bar (zoom in/out/to, background toggle). @todo: extract into its own component.

* When clicking/tapping the image area, give it focus.

* Utilities used by this PR

* Add a `two-up-handle` attribute to the handle for easier styling (classname gets mangled so it doesn't make for a good public API)

* Add a dummy comment to test netlify deploy

* Remove commented-out code.

* Fix styling of vertical split (which as it turns out is slightly different in the mocks anyway)

* Use a composited overlay for the dark background instead of animating background-color

* Move grayscale styling into `<two-up>` by default, then set colors via custom properties

* Remove commented-out svg fill

* Remove dummy comment

* Change `<GzipSize>` to be `<FileSize>`, add `compress` option that lets us show gzipped sizes later if we need. Defaults to `false`, and the gzip worker is only lazily instantiated the first time a compressed size calculation is requested.

* Dependency updates

* Remove color animations from dnd overlay

* Don't use a cyclical import for EncodedImage, instead just specify the types of the properties we Options actually uses.

* Pass source image through to FileSize component so it can compute delta

* Stylize size display with colors based on delta amount/direction

* Remove box-shadow animation.

* Simplify font stack

* Remove commented out code

* Remove gzip compression from size component

* Remove memoization bits

* Use specific flattend props instead of passing large context objects around.

* Remove unused packages.

* Remove unreachable String case in FileSize, and omit redundant File type

* Simplify calculateSize()

* Fix types for FileSize!

* Remove FileSize title

* Make delta variable consistent.

* Skip passing compareTo value for original image

* Remove manual focus

* Fix whitespace

* remove unused keyframes

* remove pointless flex-wrap property

* Remove unused resetZoom() method

* Remove pointless flex properties

* Use `on` prefix for event handling

* Remove pointless justify-self property

* Use an inline SVG for TwoUp's handle icon so it can be colored from outside the component..

* Move orientation state up from `<Output>` into `<App>` and share it with `<Options>`.

* Make the options panels responsive :)

* Show a plus sign for size increases `(+8%)`

* Use inline SVG for the zoom +/- icons, collect SVG icons into one file now that I've verified they get tree-shaken properly.

* Fix top/bottom options panels being reversed

* remove commented out code

* lockfile

* Revert quanitzation toggle styles so it's just a checkbox.

* Remove minimum delta for compare size

* Rename data prop to file.

* scale int -> float

* remove tabIndex

* Remove old icon files

* Add width to options panels

* Add vertical scrolling when options are taller than 80% of the screen height.
  • Loading branch information
developit authored and jakearchibald committed Sep 5, 2018
1 parent 54ad30a commit 32f6f8b
Show file tree
Hide file tree
Showing 15 changed files with 770 additions and 1,761 deletions.
4 changes: 0 additions & 4 deletions global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,6 @@ declare namespace JSX {
interface IntrinsicElements { }
}

declare module 'preact-i18n';
declare module 'preact-material-components-drawer';
declare module 'material-radial-progress';

declare module 'classnames' {
export default function classnames(...args: any[]): string;
}
1,640 changes: 52 additions & 1,588 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 2 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
},
"devDependencies": {
"@types/node": "^9.6.23",
"@types/pretty-bytes": "^5.1.0",
"@types/webassembly-js-api": "0.0.1",
"babel-loader": "^7.1.5",
"babel-plugin-jsx-pragmatic": "^1.0.2",
Expand Down Expand Up @@ -62,15 +63,10 @@
"webpack-plugin-replace": "^1.1.1"
},
"dependencies": {
"@types/filesize": "^3.6.0",
"classnames": "^2.2.6",
"comlink": "^3.0.3",
"comlink-loader": "^1.0.0",
"filesize": "^3.6.1",
"material-components-web": "^0.32.0",
"preact": "^8.3.1",
"preact-i18n": "^1.2.2",
"preact-material-components": "^1.4.7",
"preact-router": "^2.6.1"
"pretty-bytes": "^5.1.0"
}
}
53 changes: 31 additions & 22 deletions src/components/App/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { h, Component } from 'preact';
import { partial } from 'filesize';

import { bind, linkRef, bitmapToImageData } from '../../lib/util';
import * as style from './style.scss';
Expand Down Expand Up @@ -38,6 +37,8 @@ import {
import { decodeImage } from '../../codecs/decoders';
import { cleanMerge, cleanSet } from '../../lib/clean-modify';

type Orientation = 'horizontal' | 'vertical';

interface SourceImage {
file: File;
bmp: ImageBitmap;
Expand Down Expand Up @@ -65,14 +66,13 @@ interface State {
images: [EncodedImage, EncodedImage];
loading: boolean;
error?: string;
orientation: Orientation;
}

interface UpdateImageOptions {
skipPreprocessing?: boolean;
}

const filesize = partial({});

async function preprocessImage(
source: SourceImage,
preprocessData: PreprocessorState,
Expand Down Expand Up @@ -115,6 +115,8 @@ async function compressImage(
}

export default class App extends Component<Props, State> {
widthQuery = window.matchMedia('(min-width: 500px)');

state: State = {
loading: false,
images: [
Expand All @@ -133,6 +135,7 @@ export default class App extends Component<Props, State> {
loading: false,
},
],
orientation: this.widthQuery.matches ? 'horizontal' : 'vertical',
};

private snackbar?: SnackBarElement;
Expand All @@ -148,6 +151,13 @@ export default class App extends Component<Props, State> {
window.STATE = this.state;
};
}

this.widthQuery.addListener(this.onMobileWidthChange);
}

@bind
onMobileWidthChange() {
this.setState({ orientation: this.widthQuery.matches ? 'horizontal' : 'vertical' });
}

onEncoderTypeChange(index: 0 | 1, newType: EncoderType): void {
Expand Down Expand Up @@ -289,33 +299,32 @@ export default class App extends Component<Props, State> {
this.snackbar.showSnackbar({ message: error });
}

render({ }: Props, { loading, images }: State) {
render({ }: Props, { loading, images, source, orientation }: State) {
const [leftImageBmp, rightImageBmp] = images.map(i => i.bmp);
const anyLoading = loading || images.some(image => image.loading);

return (
<file-drop accept="image/*" onfiledrop={this.onFileDrop}>
<div id="app" class={style.app}>
<div id="app" class={`${style.app} ${style[orientation]}`}>
{(leftImageBmp && rightImageBmp) ? (
<Output leftImg={leftImageBmp} rightImg={rightImageBmp} />
<Output
orientation={orientation}
leftImg={leftImageBmp}
rightImg={rightImageBmp}
/>
) : (
<div class={style.welcome}>
<h1>Select an image</h1>
<input type="file" onChange={this.onFileChange} />
</div>
)}
{images.map((image, index) => (
<span class={index ? style.rightLabel : style.leftLabel}>
{encoderMap[image.encoderState.type].label}
{(image.downloadUrl && image.file) && (
<a href={image.downloadUrl} download={image.file.name}>🔻</a>
)}
{image.file && ` - ${filesize(image.file.size)}`}
</span>
))}
{images.map((image, index) => (
<div class={style.welcome}>
<h1>Drop, paste or select an image</h1>
<input type="file" onChange={this.onFileChange} />
</div>
)}
{(leftImageBmp && rightImageBmp) && images.map((image, index) => (
<Options
class={index ? style.rightOptions : style.leftOptions}
orientation={orientation}
imageIndex={index}
imageFile={image.file}
sourceImageFile={source && source.file}
downloadUrl={image.downloadUrl}
preprocessorState={image.preprocessorState}
encoderState={image.encoderState}
onEncoderTypeChange={this.onEncoderTypeChange.bind(this, index)}
Expand Down
82 changes: 44 additions & 38 deletions src/components/App/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,74 +10,80 @@ Note: These styles are temporary. They will be replaced before going live.
height: 100%;
overflow: hidden;
contain: strict;
display: flex;
justify-content: flex-end;

.leftLabel,
.rightLabel {
position: fixed;
bottom: 0;
padding: 5px 10px;
background: rgba(0,0,0,0.5);
color: #fff;
&.horizontal {
justify-content: space-between;
align-items: flex-end;
}

.leftLabel { left: 0; }
.rightLabel { right: 0; }

.leftOptions,
.rightOptions {
position: fixed;
bottom: 40px;
&.vertical {
flex-direction: column;
}

.leftOptions { left: 10px; }
.rightOptions { right: 10px; }
}

.welcome {
position: absolute;
display: inline-block;
left: 50%;
top: 50%;
padding: 20px;
transform: translate(-50%, -50%);
margin: auto;
text-align: center;

h1 {
font-weight: inherit;
font-size: 150%;
text-align: center;
}

input {
display: inline-block;
width: 16em;
padding: 5px;
padding: 10px;
margin: 0 auto;
-webkit-appearance: none;
border: 1px solid #b68c86;
background: #f0d3cf;
box-shadow: inset 0 0 1px #fff;
border: 1px solid var(--button-fg);
background: rgba(var(--button-fg-color), 0.1);
border-radius: 3px;
cursor: pointer;
}
}

:global {
:global {
file-drop {
overflow: hidden;
touch-action: none;
height:100%;
width:100%;

&.drop-valid {
transition: opacity 200ms ease-in-out, background-color 200ms;
opacity: 0.5;
background-color:green;
&:after {
content: '';
position: absolute;
display: block;
left: 10px;
top: 10px;
right: 10px;
bottom: 10px;
border: 2px dashed #fff;
border-radius: 10px;
opacity: 0;
transform: scale(0.95);
transition: opacity 300ms ease, transform 300ms cubic-bezier(.6,2,.6,1), background-color 300ms step-end, border-color 300ms step-end;
pointer-events: none;
}

&.drop-invalid {
transition: opacity 200ms ease-in-out, background-color 200ms;
opacity: 0.5;
background-color:red;
&.drop-valid:after,
&.drop-invalid:after {
opacity: 1;
transform: scale(1);
transition: opacity 300ms ease, transform 300ms cubic-bezier(.6,2,.6,1);
}

&.drop-valid:after {
background-color:rgba(88, 116, 88, 0.2);
border-color: rgba(65, 129, 65, 0.5);
}

&.drop-invalid:after {
background-color:rgba(119, 85, 85, 0.2);
border-color:rgba(129, 63, 63, 0.5);
}
}
}
}
87 changes: 87 additions & 0 deletions src/components/FileSize.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { h, Component } from 'preact';
import * as prettyBytes from 'pretty-bytes';

type FileContents = ArrayBuffer | Blob;

interface Props extends Pick<JSX.HTMLAttributes, Exclude<keyof JSX.HTMLAttributes, 'data'>> {
file?: FileContents;
compareTo?: FileContents;
increaseClass?: string;
decreaseClass?: string;
}

interface State {
size?: number;
sizeFormatted?: string;
compareSize?: number;
compareSizeFormatted?: string;
}

function calculateSize(data: FileContents): number {
return data instanceof ArrayBuffer ? data.byteLength : data.size;
}

export default class FileSize extends Component<Props, State> {
constructor(props: Props) {
super(props);
if (props.file) {
this.computeSize('size', props.file);
}
if (props.compareTo) {
this.computeSize('compareSize', props.compareTo);
}
}

componentWillReceiveProps({ file, compareTo }: Props) {
if (file !== this.props.file) {
this.computeSize('size', file);
}
if (compareTo !== this.props.compareTo) {
this.computeSize('compareSize', compareTo);
}
}

componentDidMount() {
this.applyStyles();
}

componentDidUpdate() {
this.applyStyles();
}

applyStyles() {
const { size, compareSize = 0 } = this.state;
if (size != null && this.base) {
const delta = size && compareSize ? (size - compareSize) / compareSize : 0;
this.base.style.setProperty('--size', '' + size);
this.base.style.setProperty('--size-delta', '' + Math.round(Math.abs(delta * 100)));
}
}

computeSize(prop: keyof State, data?: FileContents) {
const size = data ? calculateSize(data) : 0;
const pretty = prettyBytes(size);
this.setState({
[prop]: size,
[prop + 'Formatted']: pretty,
});
}

render(
{ file, compareTo, increaseClass, decreaseClass, ...props }: Props,
{ size, sizeFormatted = '', compareSize }: State,
) {
const delta = size && compareSize ? (size - compareSize) / compareSize : 0;
return (
<span {...props}>
{sizeFormatted}
{compareTo && (
<span class={delta > 0 ? increaseClass : decreaseClass}>
{delta > 0 && '+'}
{Math.round(delta * 100)}%
</span>
)}
</span>
);
}
}
Loading

0 comments on commit 32f6f8b

Please sign in to comment.