├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .github
├── FUNDING.yml
└── ISSUE_TEMPLATE
│ └── bug_report.md
├── .gitignore
├── .husky
├── .gitignore
├── commit-msg
└── pre-commit
├── .prettierrc
├── CHANGELOG.md
├── LICENSE
├── README.md
├── build
└── icons
│ ├── 256x256.png
│ ├── icon.icns
│ └── icon.ico
├── commitlint.config.js
├── demo.gif
├── logo.png
├── package-lock.json
├── package.json
├── src
├── config
│ ├── electron-builder.ts
│ └── vite.ts
├── main
│ ├── image-compressor.ts
│ ├── index.ts
│ ├── menu.ts
│ ├── preload.ts
│ ├── store
│ │ ├── index.ts
│ │ └── module
│ │ │ └── app.ts
│ ├── types
│ │ ├── cwebp-bin.d.ts
│ │ ├── index.d.ts
│ │ └── vite.shim.d.ts
│ ├── update-check.ts
│ └── utils
│ │ └── index.ts
├── renderer
│ ├── App.vue
│ ├── assets
│ │ ├── styles
│ │ │ └── base.css
│ │ └── svg
│ │ │ ├── arrow-circle-up.svg
│ │ │ ├── cog.svg
│ │ │ ├── logo.svg
│ │ │ └── times.svg
│ ├── components
│ │ ├── AppDragArea.vue
│ │ ├── AppFileList.vue
│ │ ├── AppFooter.vue
│ │ ├── AppSettingRow.vue
│ │ └── ui
│ │ │ ├── AppInput.vue
│ │ │ ├── AppPreloader.vue
│ │ │ └── AppToggle.vue
│ ├── electron.ts
│ ├── env.d.ts
│ ├── index.html
│ ├── main.ts
│ ├── router.ts
│ ├── store
│ │ └── index.ts
│ ├── types
│ │ └── index.d.ts
│ └── views
│ │ ├── Main.vue
│ │ └── Settings.vue
└── scripts
│ ├── build-electron.ts
│ ├── build-vue.ts
│ ├── build.ts
│ └── dev-server.ts
├── tsconfig.electron.json
├── tsconfig.json
├── yarn-error.log
└── yarn.lock
/.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 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | dist
2 | build
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | node: true
5 | },
6 | parser: 'vue-eslint-parser',
7 | parserOptions: {
8 | parser: '@typescript-eslint/parser'
9 | },
10 | extends: [
11 | '@vue/eslint-config-standard',
12 | 'plugin:vue/vue3-recommended',
13 | 'plugin:@typescript-eslint/recommended'
14 | ],
15 | plugins: ['@typescript-eslint', 'prettier'],
16 | rules: {
17 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
18 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
19 | 'arrow-parens': ['error', 'as-needed'],
20 | 'no-unused-vars': 'off',
21 | 'no-undef': 'off',
22 | indent: 'off',
23 | 'vue/component-name-in-template-casing': [
24 | 'error',
25 | 'PascalCase',
26 | { registeredComponentsOnly: false }
27 | ],
28 | 'vue/no-v-html': 'off',
29 | 'vue/require-prop-types': 'off',
30 | 'vue/require-default-prop': 'off',
31 | 'vue/multi-word-component-names': 'off',
32 | '@typescript-eslint/semi': ['error', 'never'],
33 | '@typescript-eslint/member-delimiter-style': [
34 | 'error',
35 | { multiline: { delimiter: 'none' } }
36 | ],
37 | '@typescript-eslint/type-annotation-spacing': ['error', {}],
38 | '@typescript-eslint/consistent-type-imports': [
39 | 'error',
40 | { prefer: 'type-imports', disallowTypeAnnotations: false }
41 | ],
42 | '@typescript-eslint/camelcase': 'off',
43 | '@typescript-eslint/explicit-function-return-type': 'off',
44 | '@typescript-eslint/explicit-member-accessibility': 'off',
45 | '@typescript-eslint/no-explicit-any': 'off',
46 | '@typescript-eslint/no-parameter-properties': 'off',
47 | '@typescript-eslint/no-empty-interface': 'off',
48 | '@typescript-eslint/ban-ts-ignore': 'off',
49 | '@typescript-eslint/no-empty-function': 'off',
50 | '@typescript-eslint/no-non-null-assertion': 'off',
51 | '@typescript-eslint/ban-ts-comment': 'off',
52 | '@typescript-eslint/explicit-module-boundary-types': 'off',
53 | '@typescript-eslint/ban-types': 'off',
54 | '@typescript-eslint/no-namespace': 'off',
55 | '@typescript-eslint/indent': ['error', 2]
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: #['antonreshetov']
4 | patreon: antonreshetov
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: antonreshetov
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: ['https://paypal.me/antonreshetov']
13 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: "[Bug]"
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Desktop (please complete the following information):**
24 | - OS: [e.g. macOS Monterey 12.1]
25 | - App Version [e.g. 1.3.2]
26 |
27 | **Additional context**
28 | Add any other context about the problem here.
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | build/*
4 | !build/icons
5 | src/renderer/types/components.d.ts
6 |
7 | .vscode
8 | .idea
9 | .DS_Store
10 | # Snyk
11 | .dccache
12 |
--------------------------------------------------------------------------------
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | yarn commitlint --edit $1
5 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | yarn lint-staged
5 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "singleQuote": true,
4 | "trailingComma": "none"
5 | }
6 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## [1.3.2](https://github.com/antonreshetov/image-optimizer/compare/v1.3.1...v1.3.2) (2022-01-17)
2 |
3 |
4 | ### Bug Fixes
5 |
6 | * jpeg compression in place [#12](https://github.com/antonreshetov/image-optimizer/issues/12) ([#13](https://github.com/antonreshetov/image-optimizer/issues/13)) ([7d1fd7b](https://github.com/antonreshetov/image-optimizer/commit/7d1fd7b913aaccc3a902510f561bfd9a14ecc050))
7 |
8 |
9 |
10 | ## [1.3.1](https://github.com/antonreshetov/image-optimizer/compare/v1.3.0...v1.3.1) (2022-01-11)
11 |
12 |
13 | ### Features
14 |
15 | * add file associations to app ([#11](https://github.com/antonreshetov/image-optimizer/issues/11)) ([3ae9516](https://github.com/antonreshetov/image-optimizer/commit/3ae95168f4d097727c5437df9cea87334125c52e))
16 |
17 |
18 |
19 | # [1.3.0](https://github.com/antonreshetov/image-optimizer/compare/v1.2.0...v1.3.0) (2021-12-04)
20 |
21 |
22 | ### Features
23 |
24 | * **main:** open images from dialog menu ([#7](https://github.com/antonreshetov/image-optimizer/issues/7)) ([e591d35](https://github.com/antonreshetov/image-optimizer/commit/e591d3597e2c58ccbd3790c3911ebd5100d2a05d))
25 |
26 |
27 |
28 | # [1.2.0](https://github.com/antonreshetov/image-optimizer/compare/v1.1.0...v1.2.0) (2021-12-03)
29 |
30 |
31 | ### Features
32 |
33 | * add animation on completion ([#5](https://github.com/antonreshetov/image-optimizer/issues/5)) ([a63fae3](https://github.com/antonreshetov/image-optimizer/commit/a63fae31846baf3df8bdf94bdbd02c5dc37b57f5))
34 | * add check for update ([#6](https://github.com/antonreshetov/image-optimizer/issues/6)) ([ccf3b13](https://github.com/antonreshetov/image-optimizer/commit/ccf3b13ff09dae5e3bb7cdcfc69b7b1988947156))
35 |
36 |
37 |
38 | # [1.1.0](https://github.com/antonreshetov/image-optimizer/compare/v1.0.2...v1.1.0) (2021-11-29)
39 |
40 |
41 | ### Features
42 |
43 | * add gif support ([#3](https://github.com/antonreshetov/image-optimizer/issues/3)) ([e697da3](https://github.com/antonreshetov/image-optimizer/commit/e697da3603344f9064cf062601c81c0872851664))
44 |
45 |
46 |
47 | ## [1.0.2](https://github.com/antonreshetov/image-optimizer/compare/v1.0.1...v1.0.2) (2021-11-27)
48 |
49 |
50 | ### Bug Fixes
51 |
52 | * **main:** re-init menu after close window ([d35c98b](https://github.com/antonreshetov/image-optimizer/commit/d35c98bee92f4ed1ccf1a3b728fd8086acb71c57))
53 |
54 |
55 |
56 | ## [1.0.1](https://github.com/antonreshetov/image-optimizer/compare/v1.0.0...v1.0.1) (2021-11-27)
57 |
58 |
59 | ### Bug Fixes
60 |
61 | * **main:** set window bounds to store [#1](https://github.com/antonreshetov/image-optimizer/issues/1) ([#2](https://github.com/antonreshetov/image-optimizer/issues/2)) ([d568291](https://github.com/antonreshetov/image-optimizer/commit/d568291a9705f5d2b6bea00bcbf683dd0157a27e))
62 |
63 |
64 |
65 | # 1.0.0 (2021-11-25)
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Anton Reshetov
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 | Image Optimizer
6 |
7 |
8 |
9 |
10 |
11 |
12 | Built with Electron, Vue & Vite.
13 |
14 |
15 | A free and open source tool for optimizing images and vector graphics.
16 |
17 |
18 |
19 |
20 |
21 |
22 | ## Core libs
23 | - [mozjpeg](https://github.com/mozilla/mozjpeg)
24 | - [pngquant](https://pngquant.org)
25 | - [cwebp](https://developers.google.com/speed/webp/docs/cwebp)
26 | - [gifsicle](https://www.lcdf.org/gifsicle/)
27 | - [SVGO](https://github.com/svg/svgo)
28 |
29 | ## Download and Installation on macOS
30 |
31 | Go to [Releases](https://github.com/antonreshetov/image-optimizer/releases) get the latest build, download and install.
32 |
33 | ## Development
34 | ```bash
35 | # install dependencies
36 | yarn
37 | # serve with hot reload
38 | yarn dev
39 | ```
40 |
41 | ## Build
42 | ```bash
43 | # build application for production
44 | yarn build
45 | ```
46 |
47 | ## Related
48 | - [Electron Vue Vite Boilerplate](https://github.com/antonreshetov/electron-vue-vite-boilerplate)
49 |
50 | Copyright (c) 2021-present, Anton Reshetov.
51 |
--------------------------------------------------------------------------------
/build/icons/256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antonreshetov/image-optimizer/4e02bf4ec2dc65b44bebbce2b0a304c16ad1acb3/build/icons/256x256.png
--------------------------------------------------------------------------------
/build/icons/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antonreshetov/image-optimizer/4e02bf4ec2dc65b44bebbce2b0a304c16ad1acb3/build/icons/icon.icns
--------------------------------------------------------------------------------
/build/icons/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antonreshetov/image-optimizer/4e02bf4ec2dc65b44bebbce2b0a304c16ad1acb3/build/icons/icon.ico
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['@commitlint/config-conventional']
3 | }
4 |
--------------------------------------------------------------------------------
/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antonreshetov/image-optimizer/4e02bf4ec2dc65b44bebbce2b0a304c16ad1acb3/demo.gif
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antonreshetov/image-optimizer/4e02bf4ec2dc65b44bebbce2b0a304c16ad1acb3/logo.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "image-optimizer",
3 | "version": "1.4.0",
4 | "description": "A tool for optimizing images and vector graphics",
5 | "main": "build/src/main/index.js",
6 | "scripts": {
7 | "dev": "tsc-watch -p tsconfig.electron.json --onSuccess 'npm run dev:server'",
8 | "dev:server": "node build/src/scripts/dev-server.js",
9 | "build:ts": "tsc -p tsconfig.electron.json",
10 | "build": "npm run build:ts && node build/src/scripts/build.js ",
11 | "lint": "eslint --ext .js,.ts,.vue . src",
12 | "lint:fix": "eslint --ext .js,.ts,.vue . --fix src",
13 | "release": "bumpp -c 'build: release v' -t",
14 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
15 | "prepare": "husky install"
16 | },
17 | "repository": "https://github.com/antonreshetov/image-optimizer",
18 | "author": {
19 | "name": "Anton Reshetov",
20 | "url": "https://github.com/antonreshetov"
21 | },
22 | "lint-staged": {
23 | "*.{js,ts,vue}": [
24 | "prettier --write",
25 | "eslint --fix"
26 | ]
27 | },
28 | "dependencies": {
29 | "axios": "^0.24.0",
30 | "canvas-confetti": "^1.4.0",
31 | "cwebp-bin": "6.1.2",
32 | "electron-store": "^8.0.1",
33 | "fs-extra": "^10.0.0",
34 | "gifsicle": "5.3.0",
35 | "junk": "^3.1.0",
36 | "mime-types": "^2.1.34",
37 | "mozjpeg": "^7.0.0",
38 | "pinia": "^2.0.9",
39 | "pngquant-bin": "^6.0.0",
40 | "queue": "^6.0.2",
41 | "svgo": "^2.8.0",
42 | "vue": "^3.2.26",
43 | "vue-router": "^4.0.12"
44 | },
45 | "devDependencies": {
46 | "@commitlint/cli": "^16.0.1",
47 | "@commitlint/config-conventional": "^16.0.0",
48 | "@types/canvas-confetti": "^1.4.2",
49 | "@types/estree": "^0.0.50",
50 | "@types/gifsicle": "^5.2.0",
51 | "@types/mime-types": "^2.1.1",
52 | "@types/mozjpeg": "^5.0.0",
53 | "@types/node": "^17.0.4",
54 | "@types/pngquant-bin": "^4.0.0",
55 | "@types/svgo": "^2.6.0",
56 | "@types/webpack": "^5.28.0",
57 | "@typescript-eslint/eslint-plugin": "^5.8.0",
58 | "@typescript-eslint/parser": "^5.8.0",
59 | "@vitejs/plugin-vue": "^1.10.2",
60 | "@vue/cli": "^4.5.15",
61 | "@vue/eslint-config-standard": "^6.1.0",
62 | "bumpp": "^7.1.1",
63 | "chalk": "^4.1.2",
64 | "chokidar": "^3.5.2",
65 | "electron": "^16.0.5",
66 | "electron-builder": "^22.14.5",
67 | "electron-devtools-installer": "^3.2.0",
68 | "eslint": "^8.5.0",
69 | "eslint-config-prettier": "^8.3.0",
70 | "eslint-plugin-import": "^2.25.3",
71 | "eslint-plugin-node": "^11.1.0",
72 | "eslint-plugin-prettier": "^4.0.0",
73 | "eslint-plugin-promise": "^6.0.0",
74 | "eslint-plugin-vue": "^8.2.0",
75 | "husky": "^7.0.0",
76 | "lint-staged": "^12.1.4",
77 | "prettier": "^2.5.1",
78 | "sass": "^1.45.1",
79 | "tsc-watch": "^4.6.0",
80 | "typescript": "^4.5.4",
81 | "unplugin-icons": "^0.13.0",
82 | "unplugin-vue-components": "^0.17.11",
83 | "vite": "^2.7.6",
84 | "vue-tsc": "^0.30.0"
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/config/electron-builder.ts:
--------------------------------------------------------------------------------
1 | import type { Configuration } from 'electron-builder'
2 | import path from 'path'
3 |
4 | export default {
5 | productName: 'Image Optimizer',
6 | appId: 'com.antonreshetov.image-optimizer',
7 | directories: {
8 | output: path.resolve(__dirname, '../../../dist')
9 | },
10 | nsis: {
11 | oneClick: false,
12 | perMachine: false,
13 | allowToChangeInstallationDirectory: true,
14 | shortcutName: 'Image Optimizer'
15 | },
16 | mac: {
17 | target: [
18 | { target: 'dmg', arch: 'arm64' },
19 | { target: 'dmg', arch: 'x64' }
20 | ]
21 | },
22 | win: {
23 | target: 'nsis'
24 | },
25 | linux: {
26 | target: ['snap']
27 | },
28 | extraMetadata: {
29 | main: 'src/main/index.js'
30 | },
31 | fileAssociations: [
32 | { name: 'JPG', ext: 'jpg' },
33 | { name: 'PNG', ext: 'png' },
34 | { name: 'GIF', ext: 'gif' },
35 | { name: 'SVG', ext: 'svg' }
36 | ],
37 | files: [
38 | '!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme}',
39 | '!**/node_modules/*/{test,__tests__,tests,powered-test,example,examples}',
40 | '!**/node_modules/*.d.ts',
41 | '!**/node_modules/.bin',
42 | 'package.json',
43 | {
44 | from: 'build/src',
45 | to: 'src',
46 | filter: ['main/**/*.js', 'renderer/**/*']
47 | }
48 | ]
49 | } as Configuration
50 |
--------------------------------------------------------------------------------
/src/config/vite.ts:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import vuePlugin from '@vitejs/plugin-vue'
3 | import { defineConfig } from 'vite'
4 | import Components from 'unplugin-vue-components/vite'
5 | import Icons from 'unplugin-icons/vite'
6 | import { FileSystemIconLoader } from 'unplugin-icons/loaders'
7 | import IconsResolver from 'unplugin-icons/resolver'
8 |
9 | const pathSrc = path.resolve(__dirname, '../../../src/renderer')
10 | const pathOut = path.resolve(__dirname, '../renderer')
11 |
12 | export default defineConfig({
13 | root: pathSrc,
14 | publicDir: 'public',
15 | server: {
16 | port: 3000,
17 | open: false
18 | },
19 | build: {
20 | outDir: pathOut,
21 | emptyOutDir: true,
22 | cssCodeSplit: false,
23 | rollupOptions: {
24 | output: {
25 | // Непонятно почему но main.js не создается,
26 | // но при этом решается задача по отключению кодсплиттинга
27 | manualChunks: () => 'main.js' //
28 | }
29 | }
30 | },
31 | plugins: [
32 | vuePlugin(),
33 | Components({
34 | dts: `${pathSrc}/types/components.d.ts`,
35 | dirs: [`${pathSrc}/components`],
36 | resolvers: [
37 | IconsResolver({
38 | prefix: '',
39 | customCollections: ['svg']
40 | })
41 | ]
42 | }),
43 | Icons({
44 | customCollections: {
45 | svg: FileSystemIconLoader(`${pathSrc}/assets/svg`)
46 | },
47 | iconCustomizer (collection, icons, props) {
48 | if (collection === 'svg') {
49 | props.width = '20px'
50 | props.height = '20px'
51 | }
52 | }
53 | })
54 | ],
55 | resolve: {
56 | alias: {
57 | '@': pathSrc
58 | }
59 | }
60 | })
61 |
--------------------------------------------------------------------------------
/src/main/image-compressor.ts:
--------------------------------------------------------------------------------
1 | import { isFile, isFolder, getFileSize } from './utils'
2 | import {
3 | ensureDirSync,
4 | readFile,
5 | writeFile,
6 | readdir,
7 | copyFileSync,
8 | unlinkSync
9 | } from 'fs-extra'
10 | import { execFile } from 'child_process'
11 | import mozjpeg from 'mozjpeg'
12 | import pngquant from 'pngquant-bin'
13 | import gifsicle from 'gifsicle'
14 | import cwebp from 'cwebp-bin'
15 | import svg from 'svgo'
16 | import junk from 'junk'
17 | import mime from 'mime-types'
18 | import queue from 'queue'
19 | import path from 'path'
20 | import util from 'util'
21 | import { store } from '../main/store'
22 | import type { BrowserWindow } from 'electron'
23 | import type { DroppedFile } from '../renderer/types'
24 | import type { FileOutput, FileSize } from './types'
25 |
26 | const readdirAsync = util.promisify(readdir)
27 |
28 | const MIN_FOLDER = 'minified'
29 | const MIN_SUFFIX = '.min'
30 | const MIME_TYPE_ENUM = {
31 | jpg: 'image/jpeg',
32 | png: 'image/png',
33 | svg: 'image/svg+xml',
34 | gif: 'image/gif',
35 | folder: ''
36 | }
37 |
38 | export class ImageOptimizer {
39 | #queue: queue
40 | #context
41 | files: DroppedFile[]
42 |
43 | constructor (files: DroppedFile[] = [], context: BrowserWindow) {
44 | this.#queue = queue({ results: [], concurrency: 10 })
45 | this.#context = context
46 |
47 | this.files = files
48 | }
49 |
50 | start () {
51 | const timeStart = new Date()
52 |
53 | this.#context.webContents.send('optimization-start')
54 |
55 | this.#optimize(this.files)
56 | this.#queue.on('end', () => {
57 | const timeEnd = new Date()
58 | const timeSpent = `${(timeEnd.valueOf() - timeStart.valueOf()) / 1000}s`
59 |
60 | this.#context.webContents.send('optimization-complete')
61 | this.#context.webContents.send('job-time', timeSpent)
62 | })
63 | }
64 |
65 | #optimize (files: DroppedFile[]) {
66 | files.forEach(async file => {
67 | if (!Object.values(MIME_TYPE_ENUM).includes(file.type)) {
68 | return
69 | }
70 |
71 | if (isFile(file.path)) {
72 | let { name, ext, dir } = path.parse(file.path)
73 | const isAddToSubfolder = store.app.get('addToSubfolder')
74 | const convertToWebp = store.app.get('convertToWebp')
75 | const availableExtToWebp = ['.png', '.jpg', '.jpeg']
76 |
77 | if (convertToWebp && availableExtToWebp.includes(ext)) {
78 | ext = '.webp'
79 | }
80 |
81 | const fileName = store.app.get('addMinSuffix')
82 | ? `${name}${MIN_SUFFIX}${ext}`
83 | : `${name}${ext}`
84 |
85 | const output = isAddToSubfolder
86 | ? `${dir}/${MIN_FOLDER}/${fileName}`
87 | : `${dir}/${fileName}`
88 |
89 | if (isAddToSubfolder) {
90 | ensureDirSync(`${dir}/${MIN_FOLDER}`)
91 | }
92 |
93 | this.#queue.push(() => this.#processFile(file, output))
94 | }
95 |
96 | if (isFolder(file.path)) {
97 | const folderPath = file.path
98 | const files = (await readdirAsync(file.path)) as string[]
99 | const _files: DroppedFile[] = []
100 |
101 | files.filter(junk.not).forEach(file => {
102 | if (isFolder(`${folderPath}/${file}`)) return
103 |
104 | _files.push({
105 | name: file,
106 | path: `${folderPath}/${file}`,
107 | type: mime.lookup(file) as string
108 | })
109 | })
110 |
111 | if (_files.length) {
112 | this.#optimize(_files)
113 | }
114 | }
115 | })
116 |
117 | this.#queue.start()
118 | }
119 |
120 | #processFile = (file: DroppedFile, output: string) => {
121 | const originalSize = getFileSize(file.path)
122 |
123 | return new Promise((resolve, reject) => {
124 | const toWebp = () => {
125 | execFile(cwebp, [file.path, '-o', output], (err: any) => {
126 | if (err) {
127 | console.log(err)
128 | reject(err)
129 | }
130 |
131 | const compressedSize = getFileSize(output)
132 | this.#sendToRenderer(file, originalSize, compressedSize)
133 | resolve()
134 | })
135 | }
136 |
137 | switch (file.type) {
138 | case MIME_TYPE_ENUM.jpg: {
139 | const { quality } = store.app.get('mozjpeg')
140 |
141 | let originalFile: string
142 | const isAddTempFile =
143 | !store.app.get('addToSubfolder') && !store.app.get('addMinSuffix')
144 | const convertToWebp = store.app.get('convertToWebp')
145 |
146 | if (isAddTempFile) {
147 | originalFile = output + '.tmp'
148 | copyFileSync(file.path, originalFile)
149 | } else {
150 | originalFile = file.path
151 | }
152 |
153 | if (convertToWebp) {
154 | toWebp()
155 | } else {
156 | execFile(
157 | mozjpeg,
158 | ['-quality', `${quality}`, '-outfile', output, originalFile],
159 | err => {
160 | if (err) {
161 | console.log(err)
162 | reject(err)
163 | }
164 |
165 | const compressedSize = getFileSize(output)
166 | this.#sendToRenderer(file, originalSize, compressedSize)
167 | resolve()
168 | }
169 | )
170 | }
171 | if (isAddTempFile) unlinkSync(originalFile)
172 | break
173 | }
174 |
175 | case MIME_TYPE_ENUM.png: {
176 | const { qualityMin, qualityMax } = store.app.get('pngquant')
177 | const convertToWebp = store.app.get('convertToWebp')
178 |
179 | if (convertToWebp) {
180 | toWebp()
181 | } else {
182 | execFile(
183 | pngquant,
184 | [
185 | '--quality',
186 | `${qualityMin}-${qualityMax}`,
187 | '-fo',
188 | output,
189 | file.path
190 | ],
191 | err => {
192 | if (err) {
193 | console.log(err)
194 | reject(err)
195 | }
196 |
197 | const compressedSize = getFileSize(output)
198 | this.#sendToRenderer(file, originalSize, compressedSize)
199 | resolve()
200 | }
201 | )
202 | }
203 | break
204 | }
205 |
206 | case MIME_TYPE_ENUM.gif: {
207 | execFile(gifsicle, ['-o', output, file.path], err => {
208 | if (err) {
209 | console.log(err)
210 | reject(err)
211 | }
212 |
213 | const compressedSize = getFileSize(output)
214 | this.#sendToRenderer(file, originalSize, compressedSize)
215 | resolve()
216 | })
217 | break
218 | }
219 |
220 | case MIME_TYPE_ENUM.svg: {
221 | readFile(file.path, (err, buffer) => {
222 | if (err) {
223 | console.log(err)
224 | reject(err)
225 | }
226 |
227 | const { data } = svg.optimize(buffer)
228 | writeFile(output, data, err => {
229 | if (err) console.log(err)
230 |
231 | const compressedSize = getFileSize(output)
232 | this.#sendToRenderer(file, originalSize, compressedSize)
233 | resolve()
234 | })
235 | })
236 | break
237 | }
238 | }
239 | })
240 | }
241 |
242 | #formatOutputData (
243 | file: DroppedFile,
244 | originalSize: FileSize,
245 | compressedSize: FileSize
246 | ): FileOutput {
247 | return {
248 | name: file.name,
249 | path: file.path,
250 | originalSize,
251 | compressedSize,
252 | compressionPercentage: Number(
253 | Math.abs(
254 | compressedSize.bytes * (100 / originalSize.bytes) - 100
255 | ).toFixed(2)
256 | )
257 | }
258 | }
259 |
260 | #sendToRenderer (
261 | file: DroppedFile,
262 | originalSize: FileSize,
263 | compressedSize: FileSize
264 | ) {
265 | this.#context.webContents.send(
266 | 'file-complete',
267 | this.#formatOutputData(file, originalSize, compressedSize)
268 | )
269 | }
270 | }
271 |
--------------------------------------------------------------------------------
/src/main/index.ts:
--------------------------------------------------------------------------------
1 | import { app, BrowserWindow, ipcMain, Menu, shell } from 'electron'
2 | import path from 'path'
3 | import { store } from './store'
4 | import { checkForUpdate } from './update-check'
5 | import { ImageOptimizer } from './image-compressor'
6 | import { createMenu } from './menu'
7 |
8 | const isDev = process.env.NODE_ENV === 'development'
9 | let mainWindow: BrowserWindow
10 |
11 | function createWindow () {
12 | const bounds = store.app.get('bounds')
13 | mainWindow = new BrowserWindow({
14 | width: 550,
15 | height: 370,
16 | ...bounds,
17 | titleBarStyle: 'hidden',
18 | resizable: false,
19 | backgroundColor: '#212123',
20 | webPreferences: {
21 | preload: path.resolve(__dirname, 'preload.js'),
22 | nodeIntegration: true,
23 | contextIsolation: true
24 | }
25 | })
26 |
27 | if (isDev) {
28 | const rendererPort = process.argv[2]
29 | mainWindow.loadURL(`http://localhost:${rendererPort}`)
30 | mainWindow.webContents.openDevTools({ mode: 'detach' })
31 | } else {
32 | mainWindow.loadFile(path.resolve(app.getAppPath(), 'src/renderer/index.html'))
33 | }
34 |
35 | mainWindow.on('close', () => {
36 | store.app.set('bounds', mainWindow.getBounds())
37 | })
38 |
39 | return { mainWindow }
40 | }
41 |
42 | function init () {
43 | createWindow()
44 | checkForUpdate(mainWindow)
45 | Menu.setApplicationMenu(Menu.buildFromTemplate(createMenu(mainWindow)))
46 | }
47 |
48 | app.whenReady().then(async () => {
49 | if (isDev) {
50 | const { default: installExtension, VUEJS3_DEVTOOLS } = await import(
51 | 'electron-devtools-installer'
52 | )
53 | installExtension(VUEJS3_DEVTOOLS)
54 | }
55 |
56 | init()
57 |
58 | app.on('activate', function () {
59 | if (BrowserWindow.getAllWindows().length === 0) {
60 | init()
61 | }
62 | checkForUpdate(mainWindow)
63 | })
64 | })
65 |
66 | app.on('window-all-closed', function () {
67 | if (process.platform !== 'darwin') app.quit()
68 | })
69 |
70 | ipcMain.on('drop', (_, files = []) => {
71 | const optimizer = new ImageOptimizer(files, mainWindow)
72 | optimizer.start()
73 | })
74 |
75 | ipcMain.on('open-url', (event, url) => {
76 | shell.openExternal(url)
77 | })
78 |
--------------------------------------------------------------------------------
/src/main/menu.ts:
--------------------------------------------------------------------------------
1 | import type { MenuItemConstructorOptions } from 'electron'
2 | import { app, dialog, shell, BrowserWindow } from 'electron'
3 | import { version, author } from '../../package.json'
4 | import os from 'os'
5 | import { getFilesOrDirs } from './utils'
6 | import { ImageOptimizer } from './image-compressor'
7 |
8 | const isMac = process.platform === 'darwin'
9 | const year = new Date().getFullYear()
10 |
11 | if (isMac) {
12 | app.setAboutPanelOptions({
13 | applicationName: 'Image Optimizer',
14 | applicationVersion: version,
15 | version,
16 | copyright: `${author.name}\n ©2021-${year}`
17 | })
18 | }
19 |
20 | const createSubmenu = (
21 | context: BrowserWindow
22 | ): MenuItemConstructorOptions[] => {
23 | if (isMac) {
24 | return [
25 | {
26 | label: 'About Image Optimizer',
27 | role: 'about'
28 | },
29 | {
30 | type: 'separator'
31 | },
32 | {
33 | label: 'Preferences',
34 | accelerator: 'CommandOrControl+,',
35 | click () {
36 | context.webContents.send('menu:preferences')
37 | }
38 | },
39 | {
40 | type: 'separator'
41 | },
42 | {
43 | label: 'Hide Image Optimizer',
44 | accelerator: 'Command+H'
45 | },
46 | {
47 | label: 'Hide Others',
48 | accelerator: 'Command+Shift+H'
49 | },
50 | {
51 | label: 'Show All'
52 | },
53 | {
54 | type: 'separator'
55 | },
56 | {
57 | label: 'Quit Image Optimizer',
58 | role: 'quit',
59 | accelerator: 'CommandOrControl+Q'
60 | }
61 | ]
62 | }
63 | return []
64 | }
65 |
66 | export const createMenu = (
67 | context: BrowserWindow
68 | ): MenuItemConstructorOptions[] => {
69 | const imageOptimizer = {
70 | label: 'Image Optimizer',
71 | submenu: createSubmenu(context)
72 | }
73 |
74 | const file = {
75 | label: 'File',
76 | submenu: [
77 | {
78 | label: 'Open Images',
79 | async click () {
80 | const { filePaths } = await dialog.showOpenDialog({
81 | properties: ['openFile', 'openDirectory', 'multiSelections']
82 | })
83 | console.log(filePaths)
84 | if (filePaths.length) {
85 | const files = getFilesOrDirs(filePaths)
86 | const optimizer = new ImageOptimizer(files, context)
87 | context.webContents.send('drop-from-dialog')
88 | optimizer.start()
89 | }
90 | },
91 | accelerator: 'CommandOrControl+O'
92 | }
93 | ]
94 | } as MenuItemConstructorOptions
95 |
96 | const help = {
97 | label: 'Help',
98 | role: 'help',
99 | submenu: [
100 | {
101 | label: 'View in GitHub',
102 | click () {
103 | shell.openExternal('https://github.com/antonreshetov/image-optimizer')
104 | }
105 | },
106 | {
107 | label: 'Report Issue',
108 | click () {
109 | shell.openExternal(
110 | 'https://github.com/antonreshetov/image-optimizer/issues/new'
111 | )
112 | }
113 | },
114 | {
115 | label: 'Donate',
116 | submenu: [
117 | {
118 | label: 'PayPal',
119 | click () {
120 | shell.openExternal('https://paypal.me/antonreshetov')
121 | }
122 | },
123 | {
124 | label: 'Patreon',
125 | click () {
126 | shell.openExternal('https://patreon.com/antonreshetov')
127 | }
128 | },
129 | {
130 | label: 'Ko-Fi',
131 | click () {
132 | shell.openExternal('https://ko-fi.com/antonreshetov')
133 | }
134 | }
135 | ]
136 | },
137 | {
138 | label: 'About',
139 | click () {
140 | dialog.showMessageBox(BrowserWindow.getFocusedWindow()!, {
141 | title: 'Image Optimizer',
142 | message: 'Image Optimizer',
143 | type: 'info',
144 | detail: `
145 | Version: ${version}
146 | Electron: ${process.versions.electron}
147 | Chrome: ${process.versions.chrome}
148 | Node.js: ${process.versions.node}
149 | V8: ${process.versions.v8}
150 | OS: ${os.type()} ${os.arch()} ${os.release()}
151 |
152 | ©2021-${year} Anton Reshetov
153 | `
154 | })
155 | }
156 | }
157 | ]
158 | } as MenuItemConstructorOptions
159 |
160 | return [imageOptimizer, file, help]
161 | }
162 |
--------------------------------------------------------------------------------
/src/main/preload.ts:
--------------------------------------------------------------------------------
1 | import type { EventCallback } from './types'
2 | import { contextBridge, ipcRenderer } from 'electron'
3 | import { store } from '../main/store'
4 |
5 | contextBridge.exposeInMainWorld('electron', {
6 | ipc: {
7 | on: (channel: string, cb: EventCallback) => ipcRenderer.on(channel, cb),
8 | send: (channel: string, data: any, cb?: EventCallback) => {
9 | ipcRenderer.send(channel, data)
10 | if (cb && typeof cb === 'function') {
11 | ipcRenderer.on(channel, cb)
12 | }
13 | },
14 | removeListener: (channel: string, cb: EventCallback) =>
15 | ipcRenderer.removeListener(channel, cb),
16 | removeListeners: (channel: string) =>
17 | ipcRenderer.removeAllListeners(channel)
18 | },
19 | store: {
20 | set: (key: any, value: any) => store.app.set(key, value),
21 | get: (key: any) => store.app.get(key),
22 | on: (key: any, cb: any) => store.app.onDidChange(key, cb)
23 | }
24 | })
25 |
--------------------------------------------------------------------------------
/src/main/store/index.ts:
--------------------------------------------------------------------------------
1 | import app from './module/app'
2 |
3 | export const store = {
4 | app
5 | }
6 |
--------------------------------------------------------------------------------
/src/main/store/module/app.ts:
--------------------------------------------------------------------------------
1 | import Store from 'electron-store'
2 | import type { StoreSchema } from '../../types'
3 |
4 | export default new Store({
5 | name: 'app',
6 | watch: true,
7 |
8 | schema: {
9 | bounds: {
10 | type: 'object',
11 | default: {}
12 | },
13 | addToSubfolder: {
14 | type: 'boolean',
15 | default: true
16 | },
17 | addMinSuffix: {
18 | type: 'boolean',
19 | default: false
20 | },
21 | clearResultList: {
22 | type: 'boolean',
23 | default: false
24 | },
25 | animationOnCompletion: {
26 | type: 'boolean',
27 | default: true
28 | },
29 | concurrency: {
30 | type: 'number',
31 | default: 10
32 | },
33 | mozjpeg: {
34 | type: 'object',
35 | properties: {
36 | quality: {
37 | type: 'number',
38 | default: 75
39 | }
40 | },
41 | default: {}
42 | },
43 | pngquant: {
44 | type: 'object',
45 | properties: {
46 | qualityMin: {
47 | type: 'number',
48 | default: 75
49 | },
50 | qualityMax: {
51 | type: 'number',
52 | default: 85
53 | }
54 | },
55 | default: {}
56 | },
57 | convertToWebp: {
58 | type: 'boolean',
59 | default: false
60 | }
61 | }
62 | })
63 |
--------------------------------------------------------------------------------
/src/main/types/cwebp-bin.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'cwebp-bin'
2 |
--------------------------------------------------------------------------------
/src/main/types/index.d.ts:
--------------------------------------------------------------------------------
1 | import type { IpcRendererEvent } from 'electron'
2 |
3 | declare interface EventCallback {
4 | (event?: IpcRendererEvent, ...args: any[]): void
5 | }
6 |
7 | declare global {
8 | interface Window {
9 | electron: {
10 | ipc: {
11 | on: (channel: string, cb: EventCallback) => void
12 | send: (channel: string, data: any, cb?: EventCallback) => void
13 | removeListener: (channel: string, cb: EventCallback) => void
14 | removeListeners: (channel: string) => void
15 | }
16 | store: {
17 | set: (key: any, value: any) => void
18 | get: (key: any) => any
19 | on: (key: any, cb: any) => void
20 | }
21 | }
22 | }
23 | }
24 |
25 | export interface StoreSchema {
26 | bounds: object
27 | addToSubfolder: boolean
28 | addMinSuffix: boolean
29 | clearResultList: boolean
30 | animationOnCompletion: boolean
31 | concurrency: number
32 | mozjpeg: {
33 | quality: number
34 | }
35 | pngquant: {
36 | qualityMin: number
37 | qualityMax: number
38 | }
39 | convertToWebp: boolean
40 | }
41 |
42 | export interface FileSize {
43 | bytes: number
44 | readable: string
45 | }
46 |
47 | export interface FileOutput {
48 | name: string
49 | path: string
50 | originalSize: FileSize
51 | compressedSize: FileSize
52 | compressionPercentage: number
53 | }
54 |
--------------------------------------------------------------------------------
/src/main/types/vite.shim.d.ts:
--------------------------------------------------------------------------------
1 | declare type Plugin$1 = any
2 |
--------------------------------------------------------------------------------
/src/main/update-check.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import type { BrowserWindow } from 'electron'
3 | import { version } from '../../package.json'
4 | const isDev = process.env.NODE_ENV === 'development'
5 |
6 | export async function checkForUpdate (context: BrowserWindow) {
7 | if (isDev) return
8 |
9 | const res = await axios.get(
10 | 'https://github.com/antonreshetov/image-optimizer/releases/latest'
11 | )
12 |
13 | if (res) {
14 | const latest = res.request.socket._httpMessage.path
15 | .split('/')
16 | .pop()
17 | .substring(1)
18 | if (latest !== version) {
19 | context.webContents.send('update-available')
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/main/utils/index.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs'
2 | import path from 'path'
3 | import mime from 'mime-types'
4 | import type { FileSize } from '../types'
5 | import type { DroppedFile } from '../../renderer/types'
6 |
7 | export const isFile = (path: string) => {
8 | const stat = fs.lstatSync(path)
9 | return stat.isFile()
10 | }
11 |
12 | export const isFolder = (path: string) => {
13 | const stat = fs.lstatSync(path)
14 | return stat.isDirectory()
15 | }
16 |
17 | export const getFileSize = (path: string): FileSize => {
18 | const stat = fs.lstatSync(path)
19 | return {
20 | bytes: stat.size,
21 | readable: formatBytes(stat.size)
22 | }
23 | }
24 |
25 | export const getFilesOrDirs = (paths: string[]): DroppedFile[] => {
26 | return paths.map(p => {
27 | const { name, ext } = path.parse(p)
28 | return {
29 | name: name + ext,
30 | path: p,
31 | type: isFolder(p) ? '' : mime.lookup(p) as string
32 | }
33 | })
34 | }
35 |
36 | export const formatBytes = (bytes: number, decimals = 2) => {
37 | if (bytes === 0) return '0 Bytes'
38 |
39 | const k = 1024
40 | const dm = decimals < 0 ? 0 : decimals
41 | const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
42 |
43 | const i = Math.floor(Math.log(bytes) / Math.log(k))
44 |
45 | return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
46 | }
47 |
--------------------------------------------------------------------------------
/src/renderer/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
9 |
13 |
14 |
15 |
55 |
56 |
95 |
--------------------------------------------------------------------------------
/src/renderer/assets/styles/base.css:
--------------------------------------------------------------------------------
1 |
2 | :root {
3 | --toolbar-height: 30px;
4 | --footer-height: 30px;
5 |
6 | --color-bg: hsl(240, 3%, 13%);
7 | --color-text: hsl(0, 0%, 67%);
8 | --color-gray-100: hsl(240, 3%, 16%);
9 | --color-gray-200: hsl(240, 3%, 20%);
10 | --color-gray-300: hsl(240, 3%, 30%);
11 | --color-gray-500: hsl(240, 3%, 50%);
12 | --color-gray-700: hsl(240, 3%, 70%);
13 | --color-error: hsl(0, 50%, 50%);
14 | --color-green: hsl(120, 61%, 50%);
15 | --color-table-row-even: var(--color-gray-100);
16 | }
17 |
18 | body {
19 | margin: 0;
20 | font-size: 12px;
21 | height: 100vh;
22 | background-color: var(--color-bg);
23 | color: var(--color-text);
24 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
25 | }
--------------------------------------------------------------------------------
/src/renderer/assets/svg/arrow-circle-up.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/renderer/assets/svg/cog.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/renderer/assets/svg/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/renderer/assets/svg/times.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/renderer/components/AppDragArea.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
14 |
Drag files or folder here
15 |
support only JPG, PNG, GIF and SVG
16 |
17 |
18 |
19 |
20 |
48 |
49 |
76 |
--------------------------------------------------------------------------------
/src/renderer/components/AppFileList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
29 |
30 |
31 |
32 |
33 |
34 | Name
35 | |
36 | Original Size |
37 | Optimized Size |
38 |
39 | Compression
40 | |
41 |
42 |
43 |
44 |
49 | {{ i.name }} |
50 | {{ i.originalSize.readable }} |
51 | {{ i.compressedSize.readable }} |
52 |
53 | {{ i.compressionPercentage }} %
54 | |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
110 |
111 |
151 |
--------------------------------------------------------------------------------
/src/renderer/components/AppFooter.vue:
--------------------------------------------------------------------------------
1 |
2 |
40 |
41 |
42 |
63 |
64 |
100 |
--------------------------------------------------------------------------------
/src/renderer/components/AppSettingRow.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ title }}
5 |
6 |
7 |
8 |
9 |
10 |
14 | {{ desc }}
15 |
16 |
17 |
18 |
19 |
20 |
28 |
29 |
52 |
--------------------------------------------------------------------------------
/src/renderer/components/ui/AppInput.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
12 |
13 |
14 |
15 |
41 |
42 |
66 |
--------------------------------------------------------------------------------
/src/renderer/components/ui/AppPreloader.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
13 |
14 |
54 |
--------------------------------------------------------------------------------
/src/renderer/components/ui/AppToggle.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
25 |
26 |
51 |
--------------------------------------------------------------------------------
/src/renderer/electron.ts:
--------------------------------------------------------------------------------
1 | const { ipc, store } = window.electron
2 |
3 | export { ipc, store }
4 |
--------------------------------------------------------------------------------
/src/renderer/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | declare module '*.vue' {
4 | import type { DefineComponent } from 'vue'
5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
6 | const component: DefineComponent<{}, {}, any>
7 | export default component
8 | }
9 |
--------------------------------------------------------------------------------
/src/renderer/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Image optimizer
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/renderer/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue'
2 | import { createPinia } from 'pinia'
3 | import router from './router'
4 | import App from './App.vue'
5 | import './assets/styles/base.css'
6 |
7 | createApp(App)
8 | .use(router)
9 | .use(createPinia())
10 | .mount('#app')
11 |
--------------------------------------------------------------------------------
/src/renderer/router.ts:
--------------------------------------------------------------------------------
1 | import { createWebHistory, createRouter } from 'vue-router'
2 |
3 | const history = createWebHistory()
4 | const router = createRouter({
5 | linkActiveClass: 'active',
6 | history,
7 | routes: [
8 | {
9 | path: '/',
10 | component: () => import('./views/Main.vue')
11 | },
12 | {
13 | path: '/settings',
14 | meta: { title: 'Test page' },
15 | component: () => import('./views/Settings.vue')
16 | }
17 |
18 | ]
19 | })
20 |
21 | export default router
22 |
--------------------------------------------------------------------------------
/src/renderer/store/index.ts:
--------------------------------------------------------------------------------
1 | import { defineStore } from 'pinia'
2 | import { store as electronStore } from '@/electron'
3 | import type { AppState } from '@/types'
4 |
5 | export const useStore = defineStore('app', {
6 | state: () =>
7 | ({
8 | files: [],
9 | totalFiles: {
10 | originalSize: 0,
11 | compressedSize: 0
12 | },
13 | showFileList: false,
14 | jobTime: '-',
15 | settings: {
16 | mozjpeg: {
17 | quality: electronStore.get('mozjpeg.quality')
18 | },
19 | pngquant: {
20 | qualityMax: electronStore.get('pngquant.qualityMax'),
21 | qualityMin: electronStore.get('pngquant.qualityMin')
22 | },
23 | convertToWebp: electronStore.get('convertToWebp'),
24 | addMinSuffix: electronStore.get('addMinSuffix'),
25 | addToSubfolder: electronStore.get('addToSubfolder'),
26 | clearResultList: electronStore.get('clearResultList'),
27 | animationOnCompletion: electronStore.get('animationOnCompletion')
28 | }
29 | } as AppState)
30 | })
31 |
--------------------------------------------------------------------------------
/src/renderer/types/index.d.ts:
--------------------------------------------------------------------------------
1 | import type { FileOutput, StoreSchema } from '../../main/types'
2 |
3 | export interface DroppedFile {
4 | name: string
5 | path: string
6 | type: string
7 | }
8 |
9 | export interface AppState {
10 | files: FileOutput[]
11 | totalFiles: {
12 | originalSize: number
13 | compressedSize: number
14 | }
15 | jobTime: string
16 | showFileList: boolean
17 | settings: Pick<
18 | StoreSchema,
19 | | 'mozjpeg'
20 | | 'pngquant'
21 | | 'addMinSuffix'
22 | | 'convertToWebp'
23 | | 'clearResultList'
24 | | 'addToSubfolder'
25 | | 'animationOnCompletion'
26 | >
27 | }
28 |
--------------------------------------------------------------------------------
/src/renderer/views/Main.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/renderer/views/Settings.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Settings
4 |
5 |
6 |
11 |
12 |
13 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
126 |
127 |
134 |
--------------------------------------------------------------------------------
/src/scripts/build-electron.ts:
--------------------------------------------------------------------------------
1 | import { build } from 'electron-builder'
2 | import config from '../config/electron-builder'
3 |
4 | export default function () {
5 | return build({ config })
6 | }
7 |
--------------------------------------------------------------------------------
/src/scripts/build-vue.ts:
--------------------------------------------------------------------------------
1 | import { build } from 'vite'
2 | import config from '../config/vite'
3 |
4 | export default function () {
5 | return build({
6 | base: './',
7 | ...config,
8 | mode: 'production'
9 | })
10 | }
11 |
--------------------------------------------------------------------------------
/src/scripts/build.ts:
--------------------------------------------------------------------------------
1 | import buildVue from './build-vue'
2 | import buildElectron from './build-electron'
3 | import chalk from 'chalk'
4 |
5 | process.env.NODE_ENV = 'production'
6 |
7 | async function build () {
8 | console.log()
9 | console.log(`${chalk.blueBright('Build started...')}`)
10 | console.log()
11 |
12 | await buildVue()
13 | await buildElectron()
14 |
15 | console.log()
16 | console.log(`${chalk.greenBright('Build success!')}`)
17 | console.log()
18 | }
19 |
20 | build()
21 |
--------------------------------------------------------------------------------
/src/scripts/dev-server.ts:
--------------------------------------------------------------------------------
1 | import { spawn } from 'child_process'
2 | import type { ChildProcess, SpawnOptionsWithoutStdio } from 'child_process'
3 | import path from 'path'
4 | import chalk from 'chalk'
5 | import chokidar from 'chokidar'
6 | import { createServer } from 'vite'
7 | import viteConfig from '../config/vite'
8 |
9 | process.env.NODE_ENV = 'development'
10 |
11 | let electronProcess: ChildProcess | null
12 | let rendererPort: number | undefined = 0
13 |
14 | async function startRenderer () {
15 | const server = await createServer({
16 | ...viteConfig,
17 | mode: 'development'
18 | })
19 |
20 | return await server.listen()
21 | }
22 |
23 | function startElectron () {
24 | if (electronProcess) return
25 |
26 | const args = ['.', rendererPort] as SpawnOptionsWithoutStdio
27 |
28 | electronProcess = spawn('electron', args)
29 |
30 | electronProcess?.stdout?.on('data', data => {
31 | console.log(chalk.blueBright('[Electron] ') + chalk.white(data.toString()))
32 | })
33 |
34 | electronProcess?.stderr?.on('data', data => {
35 | console.log(chalk.redBright('[Electron] ') + chalk.white(data.toString()))
36 | })
37 | }
38 |
39 | function restartElectron () {
40 | if (electronProcess) {
41 | electronProcess.kill()
42 | electronProcess = null
43 | }
44 |
45 | startElectron()
46 | }
47 |
48 | async function start () {
49 | console.log()
50 | console.log(`${chalk.blueBright('Starting Electron + Vite Dev Server...')}`)
51 | console.log()
52 |
53 | const devServer = await startRenderer()
54 | // console.log(devServer)
55 |
56 | rendererPort = devServer.config.server.port
57 |
58 | startElectron()
59 |
60 | chokidar
61 | .watch(path.resolve(__dirname, '../src/main'), {
62 | ignored: ['renderer']
63 | })
64 | .on('change', () => {
65 | restartElectron()
66 | })
67 | }
68 |
69 | start()
70 |
--------------------------------------------------------------------------------
/tsconfig.electron.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "module": "CommonJS",
6 | "moduleResolution": "Node",
7 | "strict": true,
8 | "jsx": "preserve",
9 | "sourceMap": true,
10 | "resolveJsonModule": true,
11 | "esModuleInterop": true,
12 | "lib": ["ESNext", "DOM"],
13 | "outDir": "./build",
14 | "paths": {
15 | "@/*": ["./src/*"]
16 | },
17 | },
18 | "include": [
19 | "src/config/*.ts",
20 | "src/scripts/*.ts",
21 | "src/main/**/*.ts",
22 | "src/main/types"
23 | ],
24 | "exclude":[
25 | "./node_modules",
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "target": "ESNext",
5 | "useDefineForClassFields": true,
6 | "module": "ESNext",
7 | "moduleResolution": "Node",
8 | "strict": true,
9 | "jsx": "preserve",
10 | "sourceMap": true,
11 | "resolveJsonModule": true,
12 | "esModuleInterop": true,
13 | "lib": ["ESNext", "DOM"],
14 | "outDir": "./build",
15 | "paths": {
16 | "@/*": ["./src/renderer/*"]
17 | },
18 | "types": ["unplugin-icons/types/vue"]
19 | },
20 | "include": [
21 | "src/renderer/**/*.ts",
22 | "src/renderer/**/*.d.ts",
23 | "src/renderer/**/*.tsx",
24 | "src/renderer/**/*.vue",
25 | "src/main/types"
26 | ],
27 | "exclude":[
28 | "./node_modules"
29 | ]
30 | }
--------------------------------------------------------------------------------