├── .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 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 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 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/app/components/Dropzone/icons/hand.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/app/components/Dropzone/icons/image.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/app/components/Dropzone/icons/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/components/Dropzone/icons/solid-gif.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/app/components/Dropzone/icons/solid-image.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/app/components/Dropzone/icons/solid-svg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/app/components/Dropzone/icons/solid-tick.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/app/components/Dropzone/icons/stop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/app/components/Dropzone/icons/svg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/app/components/Dropzone/icons/tick.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | 2 | 3 | 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 | } --------------------------------------------------------------------------------