├── .babelrc
├── .compilerc
├── .editorconfig
├── .eslintrc
├── .gitignore
├── LICENSE.md
├── README.md
├── assets
└── crimp.icns
├── package.json
├── src
├── app
│ ├── App.js
│ ├── components
│ │ ├── Busy
│ │ │ └── index.js
│ │ ├── Button
│ │ │ └── index.js
│ │ ├── Dropzone
│ │ │ ├── DropzoneIcons.js
│ │ │ ├── DropzoneSubtitle.js
│ │ │ ├── DropzoneTitle.js
│ │ │ ├── icons
│ │ │ │ ├── gif.svg
│ │ │ │ ├── hand.svg
│ │ │ │ ├── image.svg
│ │ │ │ ├── logo.svg
│ │ │ │ ├── solid-gif.svg
│ │ │ │ ├── solid-image.svg
│ │ │ │ ├── solid-svg.svg
│ │ │ │ ├── solid-tick.svg
│ │ │ │ ├── stop.svg
│ │ │ │ ├── svg.svg
│ │ │ │ └── tick.svg
│ │ │ └── index.js
│ │ ├── Loader
│ │ │ ├── CheckIcon.jsx
│ │ │ ├── FileIcon.jsx
│ │ │ ├── PlayIcon.jsx
│ │ │ ├── Processor.jsx
│ │ │ ├── SVGIcon.jsx
│ │ │ └── index.js
│ │ ├── Report
│ │ │ ├── Report.jsx
│ │ │ ├── ReportList.jsx
│ │ │ ├── ReportSummary.jsx
│ │ │ ├── icons
│ │ │ │ └── check.svg
│ │ │ ├── image.png
│ │ │ └── index.jsx
│ │ ├── Title
│ │ │ └── index.js
│ │ ├── Utils
│ │ │ └── index.jsx
│ │ └── Widget
│ │ │ ├── Widget.js
│ │ │ ├── WidgetBody.js
│ │ │ ├── WidgetFooter.js
│ │ │ ├── WidgetHeader.js
│ │ │ └── index.js
│ └── index.html
└── main
│ ├── index.js
│ ├── ipc
│ ├── FilesHandler.js
│ └── index.js
│ └── menu
│ ├── About.js
│ ├── Dev.js
│ ├── Help.js
│ ├── Window.js
│ └── index.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015"]
3 | }
--------------------------------------------------------------------------------
/.compilerc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "development": {
4 | "application/javascript": {
5 | "presets": [
6 | ["env", { "targets": { "electron": "1.6.0" } }],
7 | "react"
8 | ],
9 | "plugins": ["transform-async-to-generator", "transform-es2015-classes", "react-hot-loader/babel"],
10 | "sourceMaps": "inline"
11 | }
12 | },
13 | "production": {
14 | "application/javascript": {
15 | "presets": [
16 | ["env", { "targets": { "electron": "1.6.0" } }],
17 | "react"
18 | ],
19 | "plugins": ["transform-async-to-generator", "transform-es2015-classes"],
20 | "sourceMaps": "none"
21 | }
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "eslint-config-airbnb",
3 | "rules": {
4 | "import/extensions": 0,
5 | "import/no-extraneous-dependencies": 0,
6 | "import/no-unresolved": [2, { "ignore": ["electron"] }],
7 | "linebreak-style": 0,
8 | "react/prefer-stateless-function": 0
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | out
3 | yarn-error.log
4 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Indrashish Ghosh
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Crimp
6 |
7 |
8 |
9 | A mac app for asset optimization and compression
10 |
11 |
12 | ---
13 |
14 |
15 |
16 |
17 | ## Installation
18 |
19 | Crimp is currently in beta. Please feel free to report any bugs or error you may come across.
20 |
21 |
22 |
23 |
24 |
25 | v0.2.0 Beta
26 |
27 |
28 |
29 | ## Contributing
30 |
31 | Crimp is built on web technologies using [Electron](https://electron.atom.io/) and [React](https://reactjs.org/). Incase, you wish to contribute please submit a [Issue](https://github.com/ghosh/Crimp/issues) first outlining the changes you would like to make.
32 |
33 | #### Development setup
34 | 1. Clone Github repo `$ git clone https://github.com/ghosh/Crimp.git`
35 | 2. Install `yarn` package manager, if you don't have it already (Read [installation guide](https://yarnpkg.com/en/docs/install#mac-tab))
36 | 3. Run `yarn install` in the root folder to install all dependencies
37 | 4. Run `yarn start` to start the local electron app. This comes with Hot Module Replacement for react for instant changes.
38 |
39 | Test your changes by compiling the app via
40 | ```
41 | yarn package
42 | ```
43 |
44 |
45 |
46 | ## Licensing
47 | This project is licensed under [MIT license](https://opensource.org/licenses/MIT).
48 |
49 |
50 |
51 | ## Made by
52 | Indrashish Ghosh – [@_ighosh](https://twitter.com/_ighosh) 🇮🇳
53 |
--------------------------------------------------------------------------------
/assets/crimp.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ghosh/Crimp/4d71fa79af7cedd8787087c9d885786497649379/assets/crimp.icns
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "crimp",
3 | "productName": "Crimp",
4 | "version": "0.2.0",
5 | "private": true,
6 | "description": "Mac app for image, gif and document compression and minification",
7 | "main": "src/main/index.js",
8 | "scripts": {
9 | "start": "electron-forge start",
10 | "package": "electron-forge package",
11 | "make": "electron-forge make",
12 | "publish": "electron-forge publish",
13 | "lint": "echo \"No linting configured\""
14 | },
15 | "keywords": [
16 | "compression",
17 | "mac",
18 | "electron",
19 | "jpg",
20 | "electron",
21 | "minification",
22 | "mac app"
23 | ],
24 | "author": "ghosh",
25 | "license": "MIT",
26 | "config": {
27 | "forge": {
28 | "make_targets": {
29 | "darwin": [
30 | "zip"
31 | ]
32 | },
33 | "publish_targets": {
34 | "darwin": [
35 | "github"
36 | ]
37 | },
38 | "electronPackagerConfig": {
39 | "appCopyright": "Created by Indrashish Ghosh\nCopyright © 2017 www.ghosh.io",
40 | "appBundleId": "io.ghosh.crimp",
41 | "appCategoryType": "public.app-category.productivity",
42 | "packageManager": "yarn",
43 | "icon": "assets/crimp.icns"
44 | },
45 | "electronInstallerDMG": {
46 | "name": "Crimp"
47 | },
48 | "github_repository": {
49 | "owner": "Ghosh",
50 | "name": "Crimp",
51 | "draft": true
52 | }
53 | }
54 | },
55 | "dependencies": {
56 | "datauri": "^1.1.0",
57 | "electron-compile": "^6.4.2",
58 | "fs-extra": "^5.0.0",
59 | "imagemin": "^5.3.1",
60 | "imagemin-gifsicle": "^5.2.0",
61 | "imagemin-jpegtran": "^5.0.2",
62 | "imagemin-pngquant": "^5.0.1",
63 | "imagemin-svgo": "^6.0.0",
64 | "plur": "^2.1.2",
65 | "pretty-bytes": "^4.0.2",
66 | "react": "^16.2.0",
67 | "react-dom": "^16.2.0",
68 | "react-dropzone": "^4.2.9",
69 | "react-hot-loader": "^4.0.0",
70 | "styled-components": "^3.2.0"
71 | },
72 | "devDependencies": {
73 | "babel-plugin-transform-es2015-classes": "^6.24.1",
74 | "electron-prebuilt-compile": "1.8.2"
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/app/App.js:
--------------------------------------------------------------------------------
1 | import { ipcRenderer } from 'electron';
2 | import React, { Component } from 'react';
3 | import Dropzone from 'react-dropzone';
4 |
5 | import Title from './components/Title';
6 | import Button from './components/Button';
7 | import Loader from './components/Loader';
8 | import { Report, ReportSummary, ReportList } from './components/Report';
9 | import { Widget, WidgetHeader, WidgetBody, WidgetFooter } from './components/Widget';
10 | import { DropzoneIcons, DropzoneTitle, DropzoneSubtitle, DropzoneStyles } from './components/Dropzone';
11 |
12 | const READY = 'READY';
13 | const OPTIMIZING = 'OPTIMIZING';
14 | const REPORTING = 'REPORTING';
15 |
16 | class App extends Component {
17 |
18 | constructor(props, context) {
19 | super(props, context);
20 | this.onDrop = this.onDrop.bind(this);
21 | this.onConversion = this.onConversion.bind(this);
22 | this.state = {
23 | status: READY,
24 | delta: null,
25 | files: null
26 | };
27 | }
28 |
29 | componentDidMount() {
30 | ipcRenderer.on('files:optimized', this.onConversion);
31 | }
32 |
33 | componentWillUnmount() {
34 | ipcRenderer.removeListener('files:optimized', this.onConversion);
35 | }
36 |
37 | shouldComponentUpdate() {
38 | return true;
39 | }
40 |
41 | onConversion(event, fileData, delta) {
42 | this.setState({
43 | status: REPORTING,
44 | delta: delta,
45 | files: fileData
46 | });
47 | }
48 |
49 | onDrop(acceptedFiles) {
50 | if (acceptedFiles.length < 1) return;
51 | const filePaths = acceptedFiles.map( file => file.path );
52 | ipcRenderer.send('files:submit', filePaths);
53 | this.setState({ status: OPTIMIZING });
54 | }
55 |
56 | render() {
57 |
58 | let dropzoneRef;
59 |
60 | return (
61 |
62 |
63 |
64 | Drop files to optimize
65 |
66 |
67 |
68 | {this.state.status === OPTIMIZING ? (
69 |
70 | Optimizing...
71 |
72 | ) : ''}
73 |
74 | {this.state.status === REPORTING ? (
75 |
76 |
80 |
81 |
82 | ) : ''}
83 |
84 | {this.state.status === READY ? (
85 | { dropzoneRef = node; }}
88 | accept="image/jpeg, image/png, image/gif, .svg"
89 | style={ DropzoneStyles.dropzone }
90 | activeStyle={ DropzoneStyles.active }
91 | rejectStyle={ DropzoneStyles.reject }
92 | >
93 | {({ isDragActive, isDragReject, acceptedFiles, rejectedFiles }) => {
94 | if (isDragReject) return (
95 |
96 |
97 | Only .png, .jpg, .gif and .svg files allowed
98 |
99 | );
100 | if (isDragActive) return (
101 |
102 |
103 | Drop file(s) to start optimization
104 |
105 | );
106 | return (
107 |
108 |
109 | Drop files here to optimize
110 | .jpg, .png, .gif and .svg accepted
111 |
112 | );
113 | }}
114 |
115 | ) : ''}
116 |
117 |
118 |
119 |
120 | {this.state.status === READY ? (
121 |
124 | ) : ''}
125 |
126 | {this.state.status === OPTIMIZING ? (
127 |
130 | ) : ''}
131 |
132 | {this.state.status === REPORTING ? (
133 |
136 | ) : ''}
137 |
138 |
139 |
140 | );
141 | }
142 | }
143 |
144 | export default App;
145 |
--------------------------------------------------------------------------------
/src/app/components/Busy/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled, { keyframes } from 'styled-components';
3 |
4 |
5 | const rotateFirst = keyframes`
6 | from { transform: rotateX(35deg) rotateY(-45deg) rotateZ(0deg); }
7 | to { transform: rotateX(35deg) rotateY(-45deg) rotateZ(360deg); }
8 | `;
9 |
10 | const rotateSecond = keyframes`
11 | from { transform: rotateX(50deg) rotateY(10deg) rotateZ(0deg); }
12 | to { transform: rotateX(50deg) rotateY(10deg) rotateZ(360deg); }
13 | `;
14 |
15 | const rotateThird = keyframes`
16 | from { transform: rotateX(35deg) rotateY(55deg) rotateZ(0deg); }
17 | to { transform: rotateX(35deg) rotateY(55deg) rotateZ(360deg); }
18 | `;
19 |
20 |
21 | const StyledBusy = styled.div`
22 | display: flex;
23 | flex-direction: column;
24 | height: 336px;
25 | border-width: 2px;
26 | border-color: #b8bfda;
27 | border-style: dashed;
28 | border-radius: 3px;
29 | justify-content: center;
30 | align-items: center;
31 | `;
32 |
33 | const StyledBusyText = styled.p`
34 | -webkit-font-smoothing: antialiased;
35 | -moz-osx-font-smoothing: grayscale;
36 | font-size: 14px;
37 | margin-top: 25px;
38 | color: #5F6185;
39 | `;
40 |
41 | const StyledLoader = styled.div`
42 | width: 64px;
43 | height: 64px;
44 | border-radius: 50%;
45 | perspective: 800px;
46 | `;
47 |
48 | // eslint-disable-next-line
49 | const StyledLoaderArm = styled.div`
50 | position: absolute;
51 | box-sizing: border-box;
52 | width: 100%;
53 | height: 100%;
54 | border-radius: 50%;
55 | `;
56 |
57 | const StyledLoaderArmFirst = StyledLoaderArm.extend`
58 | left: 0%;
59 | top: 0%;
60 | animation: ${rotateFirst} 1s linear infinite;
61 | border-bottom: 3px solid #5F6185;
62 | `;
63 |
64 | const StyledLoaderArmSecond = StyledLoaderArm.extend`
65 | right: 0%;
66 | top: 0%;
67 | animation: ${rotateSecond} 1s linear infinite;
68 | border-right: 3px solid #5F6185;
69 | `;
70 |
71 | const StyledLoaderArmThird = StyledLoaderArm.extend`
72 | right: 0%;
73 | bottom: 0%;
74 | animation: ${rotateThird} 1s linear infinite;
75 | border-top: 3px solid #5F6185;
76 | `;
77 |
78 | const Busy = ({ children }) => (
79 |
80 |
81 |
82 |
83 |
84 |
85 | Optimizing files...
86 |
87 | )
88 |
89 | export default Busy;
--------------------------------------------------------------------------------
/src/app/components/Button/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from "prop-types";
3 | import styled from 'styled-components';
4 |
5 | const StyledButton = styled.button`
6 | cursor: pointer;
7 | background: #705bfb;
8 | border-radius: 3px;
9 | color: white;
10 | font-size: 14px;
11 | -webkit-font-smoothing: antialiased;
12 | outline: 0;
13 | border: none;
14 | width: 100%;
15 | padding: 8px 15px 10px;
16 | line-height: 1.5;
17 | &:hover { background: #6853ef; }
18 |
19 | :disabled {
20 | cursor: no-drop;
21 | background: #cec7ff;
22 | &:hover { background: #cec7ff; }
23 | }
24 | `;
25 |
26 | const Button = ({ children, onClick, disabled }) => (
27 |
28 | { children }
29 |
30 | )
31 |
32 | Button.propTypes = {
33 | children: PropTypes.string,
34 | onClick: PropTypes.func
35 | };
36 |
37 | Button.defaultProps = {
38 | children: "Button",
39 | onClick: () => {}
40 | };
41 |
42 | export default Button;
--------------------------------------------------------------------------------
/src/app/components/Dropzone/DropzoneIcons.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled, { keyframes } from 'styled-components';
3 |
4 | export const SVGcontainer = styled.div`
5 | display: flex;
6 | justify-content: center;
7 | margin-bottom: 10px;
8 | position: relative;
9 | min-height: 50px;
10 | `;
11 |
12 | const rotateHand = keyframes`
13 | from { transform: rotate(-15deg)}
14 | to { transform: rotate(15deg) }
15 | `;
16 |
17 | export const Icon = styled.div`
18 | will-change: transform;
19 | transition: transform 0.3s ease;
20 | position: absolute;
21 | `;
22 |
23 | const GifIcon = Icon.extend`
24 | left: 30px;
25 | transform: rotate(-20deg) scale(0.8);
26 | ${props => {
27 | if (props.disabled) return `display: none;`;
28 | if (props.hover) return `transform: translate3d(-10px,-10px,0) rotate(-20deg) scale(0.8);`;
29 | }}
30 | `;
31 |
32 | const ImageIcon = Icon.extend`
33 | bottom: 10px;
34 | ${props => {
35 | if (props.disabled) return `display: none;`;
36 | if (props.hover) return `transform: translate3d(0,-10px,0);`;
37 | }}
38 | `;
39 |
40 | const SvgIcon = Icon.extend`
41 | right: 30px;
42 | transform: rotate(20deg) scale(0.8);
43 | ${props => {
44 | if (props.disabled) return `display: none;`;
45 | if (props.hover) return `transform: translate3d(10px,-10px,0) rotate(20deg) scale(0.8);`;
46 | }}
47 | `;
48 |
49 | const StopIcon = Icon.extend`
50 | display: none;
51 | bottom: 10px;
52 | ${props => {
53 | if (props.disabled) return `display: block;`;
54 | if (props.hover) return `transform: translate3d(0,-10px,0);`;
55 | }}
56 | `;
57 |
58 | const HandIcon = Icon.extend`
59 | display: none;
60 | bottom: 10px;
61 | transform-origin: bottom center;
62 | animation: ${rotateHand} .6s alternate infinite;
63 | ${props => {
64 | if (props.disabled) return `display: block;`;
65 | }}
66 | `;
67 |
68 | const DropzoneIcons = (props) => (
69 |
70 |
71 |
72 |
79 |
80 |
81 |
82 |
88 |
89 |
90 |
91 |
97 |
98 |
99 |
100 |
106 |
107 |
108 |
109 | )
110 |
111 | export default DropzoneIcons
--------------------------------------------------------------------------------
/src/app/components/Dropzone/DropzoneSubtitle.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyledTitle } from './DropzoneTitle';
3 |
4 | const StyledSubTitle = StyledTitle.extend`
5 | font-size: 12px;
6 | color: #5F6185;
7 | `;
8 |
9 | const DropzoneSubtitle = ({ children }) => (
10 |
11 | { children }
12 |
13 | )
14 |
15 | export default DropzoneSubtitle
--------------------------------------------------------------------------------
/src/app/components/Dropzone/DropzoneTitle.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | export const StyledTitle = styled.p`
5 | font-size: 16px;
6 | width: 200px;
7 | line-height: 1.5;
8 | color: #4C4D6C;
9 | margin: 0;
10 | text-align: center;
11 | -webkit-touch-callout: none;
12 | -webkit-user-select: none;
13 | -webkit-font-smoothing: antialiased;
14 | `;
15 |
16 | const DropzoneTitle = ({ children }) => (
17 |
18 | { children }
19 |
20 | )
21 |
22 | export default DropzoneTitle
--------------------------------------------------------------------------------
/src/app/components/Dropzone/icons/gif.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/app/components/Dropzone/icons/hand.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/app/components/Dropzone/icons/image.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/app/components/Dropzone/icons/logo.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/app/components/Dropzone/icons/solid-gif.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/app/components/Dropzone/icons/solid-image.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/app/components/Dropzone/icons/solid-svg.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/app/components/Dropzone/icons/solid-tick.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/app/components/Dropzone/icons/stop.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/app/components/Dropzone/icons/svg.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/app/components/Dropzone/icons/tick.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/app/components/Dropzone/index.js:
--------------------------------------------------------------------------------
1 | import DropzoneIcons from './DropzoneIcons';
2 | import DropzoneTitle from './DropzoneTitle';
3 | import DropzoneSubtitle from './DropzoneSubtitle';
4 |
5 | const DropzoneStyles = {
6 | dropzone: {
7 | display: 'flex',
8 | height: '336px',
9 | borderWidth: '2px',
10 | borderColor: '#b8bfda',
11 | borderStyle: 'dashed',
12 | borderRadius: '3px',
13 | justifyContent: 'center',
14 | alignItems: 'center'
15 | },
16 | active: {
17 | // borderColor: 'rgba(244, 254, 255, 0.36)',
18 | background: '#d8ddef'
19 | },
20 | reject: {
21 | cursor: 'not-allowed',
22 | borderColor: 'rgba(255, 37, 37, 0.4)',
23 | background: '#f3e1e1'
24 | }
25 | };
26 |
27 | export { DropzoneIcons, DropzoneTitle, DropzoneSubtitle, DropzoneStyles }
--------------------------------------------------------------------------------
/src/app/components/Loader/CheckIcon.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled, { keyframes } from 'styled-components';
3 |
4 | export const Icon = styled.svg`
5 | width: 30px;
6 | height: 35px;
7 | margin: 0 5px;
8 | `;
9 |
10 | const CheckIcon = (props) => (
11 |
12 |
13 |
14 |
15 |
16 |
17 | )
18 |
19 | export default CheckIcon;
--------------------------------------------------------------------------------
/src/app/components/Loader/FileIcon.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled, { keyframes } from 'styled-components';
3 |
4 | export const Icon = styled.svg`
5 | width: 30px;
6 | height: 35px;
7 | margin: 0 5px;
8 | `;
9 |
10 | const FileIcon = (props) => (
11 |
12 |
13 |
14 |
15 |
16 |
17 | )
18 |
19 | export default FileIcon;
--------------------------------------------------------------------------------
/src/app/components/Loader/PlayIcon.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled, { keyframes } from 'styled-components';
3 |
4 | export const Icon = styled.svg`
5 | width: 30px;
6 | height: 35px;
7 | margin: 0 5px;
8 | `;
9 |
10 | const PlayIcon = (props) => (
11 |
12 |
13 |
14 |
15 |
16 |
17 | )
18 |
19 | export default PlayIcon;
--------------------------------------------------------------------------------
/src/app/components/Loader/Processor.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled, { keyframes } from 'styled-components';
3 |
4 | export const StyledProcessor = styled.div`
5 | width: 55px;
6 | height: 55px;
7 | border-radius: 80px;
8 | display: flex;
9 | justify-content: center;
10 | align-items: center;
11 | background-color: #b8bfda;
12 | z-index: 2;
13 | `;
14 |
15 | export const Icon = styled.svg`
16 | width: 25px;
17 | height: 20px;
18 | fill: #e8e8e8;
19 | `;
20 |
21 | const Processor = (props) => (
22 |
23 |
24 |
25 |
26 |
27 | )
28 |
29 | export default Processor;
--------------------------------------------------------------------------------
/src/app/components/Loader/SVGIcon.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled, { keyframes } from 'styled-components';
3 |
4 | export const Icon = styled.svg`
5 | width: 30px;
6 | height: 35px;
7 | margin: 0 5px;
8 | `;
9 |
10 | const SvgIcon = (props) => (
11 |
12 |
13 |
14 |
15 |
16 |
17 | )
18 |
19 | export default SvgIcon;
--------------------------------------------------------------------------------
/src/app/components/Loader/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled, { keyframes } from 'styled-components';
3 |
4 | import FileIcon from './FileIcon';
5 | import PlayIcon from './PlayIcon';
6 | import SVGIcon from './SVGIcon';
7 | import CheckIcon from './CheckIcon';
8 | import Processor from './Processor';
9 |
10 | const StyledLoader = styled.div`
11 | display: flex;
12 | flex-direction: column;
13 | height: 336px;
14 | justify-content: center;
15 | align-items: center;
16 | `;
17 |
18 | const StyledLoaderText = styled.p`
19 | -webkit-font-smoothing: antialiased;
20 | -moz-osx-font-smoothing: grayscale;
21 | font-size: 14px;
22 | margin-top: 15px;
23 | color: #5F6185;
24 | `;
25 |
26 | const Conveyor = styled.div`
27 | display: flex;
28 | justify-content: center;
29 | align-items: center;
30 | &:before {
31 | content: '';
32 | position: absolute;
33 | width: 100%;
34 | height: 50px;
35 | left: 0;
36 | background: linear-gradient(to right, #e6eaf5, rgba(230, 234, 245, 0), rgba(230, 234, 245, 0), #e6eaf5);
37 | z-index: 1;
38 | }
39 | `;
40 |
41 | const moveBelt = keyframes`
42 | from { transform: translate3d(0, 0, 0); }
43 | from { transform: translate3d(-40px, 0, 0); }
44 | `;
45 |
46 | const Belt = styled.div`
47 | display: flex;
48 | `;
49 |
50 | const LeftBelt = Belt.extend`
51 | position: absolute;
52 | left: 0px;
53 | animation: ${moveBelt} 1s linear infinite;
54 | `;
55 |
56 | const RightBelt = Belt.extend`
57 | position: absolute;
58 | right: -40px;
59 | animation: ${moveBelt} 1s linear infinite;
60 | `;
61 |
62 | const Loader = ({ children }) => (
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | Optimizing files...
82 |
83 | )
84 |
85 | export default Loader;
--------------------------------------------------------------------------------
/src/app/components/Report/Report.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled, { keyframes } from 'styled-components';
3 |
4 | const StyledReport = styled.div`
5 | display: flex;
6 | flex-direction: column;
7 | height: 340px;
8 | `;
9 |
10 | const Report = ({ children }) => (
11 |
12 | { children }
13 |
14 | )
15 |
16 | export default Report;
17 |
--------------------------------------------------------------------------------
/src/app/components/Report/ReportList.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled, { keyframes } from 'styled-components';
3 |
4 | const List = styled.ul`
5 | margin: 0;
6 | padding: 1px 10px;
7 | height: 100%;
8 | border-radius: 3px;
9 | list-style-type: none;
10 | background-color: #d8ddef;
11 | color: #4C4D6C;
12 | height: 283px;
13 | overflow-y: scroll;
14 | -webkit-overflow-scrolling: touch;
15 | `;
16 |
17 | const ListItem = styled.li`
18 | display: flex;
19 | align-items: center;
20 | padding: 13px 0;
21 | border-bottom: solid 1px #d0d5e8;
22 |
23 | &:last-child {
24 | border-bottom: 0;
25 | }
26 | `;
27 |
28 | const ListData = styled.div`
29 | display: flex;
30 | flex-direction: column;
31 | `;
32 |
33 | const FileName = styled.p`
34 | margin: 0;
35 | color: #4C4D6C;
36 | font-size: 13px;
37 | margin-bottom: 5px;
38 | line-height: 1;
39 | white-space: nowrap;
40 | overflow: hidden;
41 | text-overflow: ellipsis;
42 | width: 210px;
43 | `;
44 |
45 | const FileSummary = styled.p`
46 | margin: 0;
47 | font-size: 10px;
48 | line-height: 1;
49 | color: #8182a5;
50 | `;
51 |
52 | const FileImage = styled.div`
53 | width: 35px;
54 | height: 35px;
55 | background-size: cover;
56 | background-position: top center;
57 | margin-right: 8px;
58 | `;
59 |
60 |
61 | const ReportList = ({ files }) => (
62 |
63 |
64 | {Object.keys(files).map((key) => {
65 | const file = files[key];
66 | // const fileImgStyles = { backgroundImage: `url(${file.path})` }
67 | const fileImgStyles = { backgroundImage: `url("${file.dataUri}")` }
68 | return (
69 |
70 |
71 |
72 | {file.fileName}
73 | {file.originalSize} → {file.optimizedSize}, ▼ {file.deltaPerct}%
74 |
75 |
76 | );
77 | })}
78 |
79 |
80 | )
81 |
82 | export default ReportList;
83 |
--------------------------------------------------------------------------------
/src/app/components/Report/ReportSummary.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled, { keyframes } from 'styled-components';
3 |
4 | const StyledSummary = styled.div`
5 | display: flex;
6 | align-items: center;
7 | height: 55px;
8 | `;
9 |
10 | const Tick = styled.svg`
11 | width: 20px;
12 | height: 20px;
13 | fill: #72b12a;
14 | margin: 0 10px;
15 | `;
16 |
17 | const Summary = styled.h1`
18 | font-weight: bold;
19 | margin-right: 10px;
20 | color: #4C4D6C;
21 | font-size: 17px;
22 | `;
23 |
24 | const Delta = styled.p`
25 | color: #72b12a;
26 | font-size: 13px;
27 | `;
28 |
29 | const ReportSummary = ({deltaBytes, deltaPerct}) => (
30 |
31 |
32 |
33 |
34 |
35 | {deltaBytes} saved
36 |
37 |
38 | ▼ {deltaPerct}%
39 |
40 |
41 | )
42 |
43 | export default ReportSummary;
44 |
--------------------------------------------------------------------------------
/src/app/components/Report/icons/check.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/app/components/Report/image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ghosh/Crimp/4d71fa79af7cedd8787087c9d885786497649379/src/app/components/Report/image.png
--------------------------------------------------------------------------------
/src/app/components/Report/index.jsx:
--------------------------------------------------------------------------------
1 | import Report from './Report';
2 | import ReportList from './ReportList';
3 | import ReportSummary from './ReportSummary';
4 |
5 | export { Report, ReportList, ReportSummary };
--------------------------------------------------------------------------------
/src/app/components/Title/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | const StyledTitle = styled.p`
5 | padding-top: 4px;
6 | font-size: 12px;
7 | color: #4C4D6C;
8 | margin: 0;
9 | text-align: center;
10 | -webkit-touch-callout: none;
11 | -webkit-user-select: none;
12 | -webkit-font-smoothing: antialiased;
13 | `;
14 |
15 | const Title = ({ children }) => (
16 |
17 | { children }
18 |
19 | )
20 |
21 | export default Title;
--------------------------------------------------------------------------------
/src/app/components/Utils/index.jsx:
--------------------------------------------------------------------------------
1 | const Aux = props => props.children;
2 | export default Aux;
--------------------------------------------------------------------------------
/src/app/components/Widget/Widget.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 |
5 | const StyledApp = styled.section`
6 | display: flex;
7 | flex-direction: column;
8 | justify-content: space-between;
9 | `;
10 |
11 | const Widget = ({ children }) => (
12 |
13 | { children }
14 |
15 | )
16 |
17 | export default Widget;
--------------------------------------------------------------------------------
/src/app/components/Widget/WidgetBody.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | const StyledBody = styled.section`
5 | padding: 5px 10px 10px 10px;
6 | flex: 1;
7 | `;
8 |
9 | const WidgetBody = ({ children }) => (
10 |
11 | { children }
12 |
13 | )
14 |
15 | export default WidgetBody
--------------------------------------------------------------------------------
/src/app/components/Widget/WidgetFooter.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | const StyledFooter = styled.section`
5 | padding: 0 10px;
6 | `;
7 |
8 | const WidgetFooter = ({ children }) => (
9 |
10 | { children }
11 |
12 | )
13 |
14 | export default WidgetFooter;
--------------------------------------------------------------------------------
/src/app/components/Widget/WidgetHeader.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | const StyledHeader = styled.section`
5 | height: 22px;
6 | -webkit-app-region: drag;
7 | `;
8 |
9 | const WidgetHeader = ({ children }) => (
10 |
11 | { children }
12 |
13 | )
14 |
15 | export default WidgetHeader
--------------------------------------------------------------------------------
/src/app/components/Widget/index.js:
--------------------------------------------------------------------------------
1 | import Widget from './Widget';
2 | import WidgetHeader from './WidgetHeader';
3 | import WidgetBody from './WidgetBody';
4 | import WidgetFooter from './WidgetFooter';
5 |
6 | export { Widget, WidgetHeader, WidgetBody, WidgetFooter };
--------------------------------------------------------------------------------
/src/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
15 |
16 |
17 |
18 |
19 |
20 |
35 |
--------------------------------------------------------------------------------
/src/main/index.js:
--------------------------------------------------------------------------------
1 | import { app, BrowserWindow, ipcMain } from 'electron';
2 | import { enableLiveReload } from 'electron-compile';
3 |
4 | import MenuBuilder from './menu';
5 | import FilesHandler from './ipc';
6 |
7 | let mainWindow;
8 |
9 | const isDevMode = process.execPath.match(/[\\/]electron/);
10 |
11 | if (isDevMode) enableLiveReload({strategy: 'react-hmr'});
12 |
13 | const createWindow = async () => {
14 | mainWindow = new BrowserWindow({
15 | title: 'Crimp',
16 | width: 300,
17 | height: 422,
18 | titleBarStyle: 'hidden',
19 | maximizable: false,
20 | resizable: false,
21 | acceptFirstMouse: true,
22 | frame: false,
23 | backgroundColor: '#e6eaf5',
24 | show: false,
25 | // vibrancy: 'dark'
26 | })
27 |
28 | mainWindow.loadURL(`file://${__dirname}/../app/index.html`);
29 |
30 | const menuBuilder = new MenuBuilder(mainWindow);
31 | menuBuilder.buildMenu(isDevMode);
32 |
33 |
34 | if (isDevMode) {
35 | BrowserWindow.addDevToolsExtension(
36 | '/Users/ghosh/Library/Application Support/Google/Chrome/' +
37 | 'default/Extensions/fmkadmapgofadopljbjfkapdkoienihi/2.5.2_0'
38 | );
39 | }
40 |
41 | mainWindow.once('ready-to-show', () => {
42 | mainWindow.show()
43 | })
44 |
45 | mainWindow.on('closed', () => mainWindow = null );
46 | };
47 |
48 | app.on('ready', createWindow)
49 |
50 | app.on('window-all-closed', () => {
51 | if (process.platform !== 'darwin') app.quit()
52 | })
53 |
54 | app.on('activate', () => {
55 | if (mainWindow === null) createWindow()
56 | })
57 |
58 | const filesHandler = new FilesHandler();
59 | ipcMain.on('files:submit', filesHandler.optimize);
60 |
--------------------------------------------------------------------------------
/src/main/ipc/FilesHandler.js:
--------------------------------------------------------------------------------
1 | import { Notification } from 'electron';
2 |
3 | import plur from 'plur';
4 | import fs from 'fs-extra';
5 | import imagemin from 'imagemin';
6 | import prettyBytes from 'pretty-bytes';
7 | import { sync as DataURI } from 'datauri';
8 | import imageminJpegtran from 'imagemin-jpegtran';
9 | import imageminPngquant from 'imagemin-pngquant';
10 | import imageminGifsicle from 'imagemin-gifsicle';
11 | import imageminSvgo from 'imagemin-svgo';
12 |
13 | const imageMinPlugins = [
14 | imageminJpegtran(),
15 | imageminPngquant({quality: '65-80'}),
16 | imageminGifsicle({optimizationLevel: 2}),
17 | imageminSvgo({ plugins: [ {
18 | removeTitle: true,
19 | removeDimensions: true
20 | } ]})
21 | ];
22 |
23 | export default class FilesHandler {
24 |
25 | constructor(props, context) {
26 | this.notify = this.notify.bind(this);
27 | this.optimize = this.optimize.bind(this);
28 | this.msToHuman = this.msToHuman.bind(this);
29 | this.optimizeFile = this.optimizeFile.bind(this);
30 | this.calculateDelta = this.calculateDelta.bind(this);
31 | this.calculateTotDelta = this.calculateTotDelta.bind(this);
32 | }
33 |
34 |
35 | async msToHuman(ms) {
36 | const hours = Math.floor(ms / 3600000); // 1 Hour = 36000 Milliseconds
37 | const minutes = Math.floor((ms % 3600000) / 60000); // 1 Minutes = 60000 Milliseconds
38 | const seconds = Math.floor(((ms % 360000) % 60000) / 1000); // 1 Second = 1000 Milliseconds
39 |
40 | if ( seconds == '0' ) return `${ms}ms`;
41 | return (minutes > 60000) ? `${minutes}m ${seconds}s` : `${seconds} seconds`;
42 | }
43 |
44 |
45 | async calculateDelta(originalSize, optimizedSize) {
46 | const saved = originalSize - optimizedSize;
47 | const percent = originalSize > 0 ? (saved / originalSize) * 100 : 0;
48 | const zeroSum = saved > 0 ? false : true;
49 |
50 | const deltaPerct = percent.toFixed(1).replace(/\.0$/, '');
51 | const deltaBytes = prettyBytes(saved);
52 |
53 | return { deltaPerct, deltaBytes };
54 | }
55 |
56 |
57 | async calculateTotDelta(totalOriginalSize, totalOptimizedSize) {
58 | const originalSize = totalOriginalSize.reduce( (acc, num) => acc + num );
59 | const optimizedSize = totalOptimizedSize.reduce( (acc, num) => acc + num );
60 |
61 | const { deltaPerct, deltaBytes } = await this.calculateDelta(originalSize, optimizedSize);
62 | return { deltaPerct, deltaBytes };
63 | }
64 |
65 |
66 | async notify({numFiles, timeTaken, bytesSaved, perctSaved, imagePath}) {
67 | const notification = new Notification({
68 | title: `${numFiles} ${plur('file', numFiles)} optimizated`,
69 | subtitle: `Saved ${bytesSaved}, thats a ${perctSaved}% ↓ in size`,
70 | body: `Processed in ${timeTaken}`,
71 | icon: imagePath
72 | })
73 | notification.show();
74 | }
75 |
76 |
77 | async optimize(event, files) {
78 | const startTime = new Date();
79 | let totalOriginalSize = [];
80 | let totalOptimizedSize = [];
81 | let fileData = {};
82 |
83 | // Runs file optimizations in parallel
84 | // Stackoverflow:- https://goo.gl/wGdkog
85 | await Promise.all(files.map(async (file, index) => {
86 | const {dataUri, originalSize, optimizedSize} = await this.optimizeFile(file);
87 |
88 | totalOriginalSize.push(originalSize);
89 | totalOptimizedSize.push(optimizedSize);
90 |
91 | const { deltaPerct, deltaBytes } = await this.calculateDelta(originalSize, optimizedSize);
92 |
93 | fileData[index] = {};
94 | fileData[index]['path'] = file;
95 | fileData[index]['dataUri'] = dataUri;
96 | fileData[index]['fileName'] = file.replace(/^.*[\\\/]/, '');
97 | fileData[index]['originalSize'] = prettyBytes(originalSize);
98 | fileData[index]['optimizedSize'] = prettyBytes(optimizedSize);
99 | fileData[index]['deltaPerct'] = deltaPerct;
100 | fileData[index]['deltaBytes'] = deltaBytes;
101 | }));
102 |
103 | const { deltaPerct, deltaBytes } = await this.calculateTotDelta(totalOriginalSize, totalOptimizedSize);
104 | const deltaTime = await this.msToHuman( new Date() - startTime );
105 |
106 | console.log(`Saved ${deltaBytes}, thats a ${deltaPerct}% ↓ in size`);
107 | event.sender.send('files:optimized', fileData, { deltaBytes, deltaPerct });
108 |
109 | this.notify({
110 | numFiles: files.length,
111 | timeTaken: deltaTime,
112 | bytesSaved: deltaBytes,
113 | perctSaved: deltaPerct,
114 | imagePath: files[0]
115 | })
116 |
117 | }
118 |
119 |
120 | /**
121 | * Optimizes individual image files and returns
122 | * compression data.
123 | * @param {[type]} path Absolute path to the file
124 | * @return {object} Status object of conversion
125 | */
126 | async optimizeFile(path) {
127 | try {
128 | const dataUri = await DataURI(path);
129 |
130 | const originalBuffer = await fs.readFile(path);
131 | const optimizedBuffer = await imagemin.buffer(originalBuffer, { plugins: imageMinPlugins });
132 | await fs.writeFile(path, optimizedBuffer);
133 |
134 | return {
135 | dataUri: dataUri,
136 | originalSize: originalBuffer.length,
137 | optimizedSize: optimizedBuffer.length
138 | }
139 |
140 | } catch (error) {
141 | return { status: false, originalSize: null, optimizedSize: null }
142 | console.error('Error: ', error);
143 | }
144 | }
145 |
146 | }
147 |
--------------------------------------------------------------------------------
/src/main/ipc/index.js:
--------------------------------------------------------------------------------
1 | import FilesHandler from "./FilesHandler";
2 |
3 | export default FilesHandler;
--------------------------------------------------------------------------------
/src/main/menu/About.js:
--------------------------------------------------------------------------------
1 | import { app } from 'electron';
2 |
3 | export default {
4 | label: 'Crimp',
5 | submenu: [
6 | {
7 | label: 'About ' + 'Crimp',
8 | role: 'about'
9 | },
10 | {
11 | type: 'separator'
12 | },
13 | {
14 | label: 'Services',
15 | role: 'services',
16 | submenu: []
17 | },
18 | {
19 | type: 'separator'
20 | },
21 | {
22 | label: 'Hide ' + 'Crimp',
23 | accelerator: 'Command+H',
24 | role: 'hide'
25 | },
26 | {
27 | label: 'Hide Others',
28 | accelerator: 'Command+Shift+H',
29 | role: 'hideothers'
30 | },
31 | {
32 | label: 'Show All',
33 | role: 'unhide'
34 | },
35 | {
36 | type: 'separator'
37 | },
38 | {
39 | label: 'Quit',
40 | accelerator: 'Command+Q',
41 | click: function() { app.quit(); }
42 | },
43 | ]
44 | }
--------------------------------------------------------------------------------
/src/main/menu/Dev.js:
--------------------------------------------------------------------------------
1 | import { app, BrowserWindow } from 'electron';
2 |
3 | export default {
4 | label: 'Development',
5 | submenu: [{
6 | label: 'Reload',
7 | accelerator: 'CmdOrCtrl+R',
8 | click: () => {
9 | BrowserWindow.getFocusedWindow().webContents.reloadIgnoringCache();
10 | },
11 | },
12 | {
13 | label: 'Toggle DevTools',
14 | accelerator: 'Alt+CmdOrCtrl+I',
15 | click: () => {
16 | BrowserWindow.getFocusedWindow().toggleDevTools();
17 | },
18 | }],
19 | };
--------------------------------------------------------------------------------
/src/main/menu/Help.js:
--------------------------------------------------------------------------------
1 | import electron from 'electron';
2 |
3 | export default {
4 | label: 'Help',
5 | submenu: [
6 | { label: 'Crimp Website', click: () => electron.shell.openExternal('https://crimp.now.sh') },
7 | { type: 'separator' },
8 | { label: 'Report Issue', click: () => electron.shell.openExternal('https://github.com/Ghosh/crimp/issues') },
9 | { label: 'Suggest Feature', click: () => electron.shell.openExternal('https://github.com/Ghosh/crimp/issues') }
10 | ],
11 | };
--------------------------------------------------------------------------------
/src/main/menu/Window.js:
--------------------------------------------------------------------------------
1 | export default {
2 | label: 'Window',
3 | submenu: [
4 | { label: 'Minimize', accelerator: 'Command+M', selector: 'performMiniaturize:' },
5 | { label: 'Zoom', accelerator: 'Alt+Command+Ctrl+M', selector: 'zoom:' },
6 | { type: 'separator' },
7 | { label: 'Close', accelerator: 'Command+W', selector: 'performClose:' }
8 | ]
9 | }
--------------------------------------------------------------------------------
/src/main/menu/index.js:
--------------------------------------------------------------------------------
1 | import { app, Menu, shell, BrowserWindow } from 'electron';
2 | import aboutMenuTemplate from './About.js';
3 | import devMenuTemplate from './Dev.js';
4 | import helpMenuTemplate from './Help.js';
5 | import windowMenuTemplate from './Window.js';
6 |
7 | export default class MenuBuilder {
8 | constructor(mainWindow = BrowserWindow) {
9 | this.mainWindow = mainWindow;
10 | }
11 |
12 | buildMenu(isDevMode) {
13 | let menuTemplate = this.buildMenuTemplate();
14 | if (isDevMode) menuTemplate.push(devMenuTemplate);
15 | const menu = Menu.buildFromTemplate(menuTemplate);
16 |
17 | Menu.setApplicationMenu(menu);
18 | return menu;
19 | }
20 |
21 | buildMenuTemplate() {
22 | return [
23 | aboutMenuTemplate,
24 | windowMenuTemplate,
25 | helpMenuTemplate
26 | ];
27 | }
28 |
29 | }
--------------------------------------------------------------------------------