├── .github ├── optimus-logo--960x540.png ├── optimus_screenshot_editor--1200x742.png ├── optimus_screenshot_options--1200x742.png ├── optimus_screenshot_options--1200x936.png └── workflows │ ├── build.yml │ └── codeql-analysis.yml ├── .gitignore ├── .jshint ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── appicon--512.png ├── appicon.png ├── backend ├── config │ └── config.go ├── image │ ├── file.go │ └── filemanager.go ├── jpeg │ └── jpeg.go ├── localstore │ └── localstore.go ├── png │ └── png.go ├── stat │ └── stat.go └── webp │ └── webp.go ├── dmg-spec.json ├── frontend ├── .gitignore ├── .prettierrc ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── package.json.md5 ├── postcss.config.js ├── src │ ├── App.vue │ ├── assets │ │ ├── css │ │ │ ├── animations.css │ │ │ ├── buttons.css │ │ │ ├── input.css │ │ │ ├── main.css │ │ │ └── tooltip.css │ │ └── images │ │ │ └── optimus-logo--640x360.png │ ├── components │ │ ├── About.vue │ │ ├── BtnClose.vue │ │ ├── Dropdown.vue │ │ ├── Editor.vue │ │ ├── Notification.vue │ │ ├── Settings.vue │ │ ├── Sidebar.vue │ │ └── Stats.vue │ ├── lib │ │ ├── event-bus.js │ │ ├── file.js │ │ └── time.js │ ├── main.js │ └── store.js ├── tailwind.config.js └── vue.config.js ├── go.mod ├── go.sum ├── main.go ├── optimus.AppImage.desktop ├── optimus.json ├── project.json └── wails.json /.github/optimus-logo--960x540.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Splode/optimus/b58bf0d5adc4b07043c6bb4da12f119be08e1d42/.github/optimus-logo--960x540.png -------------------------------------------------------------------------------- /.github/optimus_screenshot_editor--1200x742.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Splode/optimus/b58bf0d5adc4b07043c6bb4da12f119be08e1d42/.github/optimus_screenshot_editor--1200x742.png -------------------------------------------------------------------------------- /.github/optimus_screenshot_options--1200x742.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Splode/optimus/b58bf0d5adc4b07043c6bb4da12f119be08e1d42/.github/optimus_screenshot_options--1200x742.png -------------------------------------------------------------------------------- /.github/optimus_screenshot_options--1200x936.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Splode/optimus/b58bf0d5adc4b07043c6bb4da12f119be08e1d42/.github/optimus_screenshot_options--1200x936.png -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - "v*" 5 | jobs: 6 | package: 7 | strategy: 8 | matrix: 9 | go-version: [1.16] 10 | os: [ubuntu-latest, macos-latest, windows-latest] 11 | runs-on: ${{ matrix.os }} 12 | steps: 13 | - name: Install Go 14 | uses: actions/setup-go@v2 15 | with: 16 | go-version: ${{ matrix.go-version }} 17 | - name: Checkout code 18 | uses: actions/checkout@v2 19 | - name: Set Version 20 | run: echo "VERSION=$(git rev-parse --short HEAD)" >> $GITHUB_ENV 21 | - name: Get Wails dependencies 22 | run: sudo apt update && sudo apt install -y libgtk-3-dev libwebkit2gtk-4.0-dev 23 | if: matrix.os == 'ubuntu-latest' 24 | - name: Get Wails 25 | run: go get -u github.com/wailsapp/wails/cmd/wails 26 | - name: Build package macOS 27 | run: | 28 | export PATH=${PATH}:`go env GOPATH`/bin 29 | echo "building on ${{ matrix.os }}" 30 | mkdir -p ~/.wails 31 | cp wails.json ~/.wails/ 32 | export LOG_LEVEL=debug 33 | export GODEBUG=1 34 | wails build -p 35 | echo "converting .app into a .dmg" 36 | npm install -g appdmg 37 | appdmg dmg-spec.json optimus.dmg 38 | zip optimus.zip optimus.dmg 39 | if: matrix.os == 'macos-latest' 40 | - name: Build package linux 41 | run: | 42 | export PATH=$PATH:$(go env GOPATH)/bin 43 | echo "building on ${{ matrix.os }}" 44 | echo ${{ env.GITHUB_REF }} 45 | echo ${{ env.GITHUB_HEAD_REF }} 46 | mkdir -p ~/.wails 47 | cp wails.json ~/.wails/ 48 | export LOG_LEVEL=debug 49 | export GODEBUG=1 50 | wails build 51 | tar -czvf optimus.tar.gz ./build/optimus 52 | # wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage 53 | # chmod +x linuxdeploy*.AppImage 54 | # ls ./ 55 | # ./linuxdeploy*.AppImage --appdir AppDir --executable ./build/optimus --desktop-file=optimus.AppImage.desktop --icon-file=appicon--512.png --output appimage 56 | if: matrix.os == 'ubuntu-latest' 57 | - name: Build package windows 58 | run: | 59 | $GP = (go env GOPATH) 60 | $env:path = "$env:path;$GP\bin" 61 | echo "building on ${{ matrix.os }}" 62 | New-Item -ItemType directory -Path "$HOME\.wails" -Force 63 | Copy-Item -Path "$PWD\wails.json" -Destination "$HOME\.wails\wails.json" 64 | choco install mingw 65 | wails build -p 66 | Compress-Archive -Path .\build\optimus* -DestinationPath .\optimus.zip 67 | if: matrix.os == 'windows-latest' 68 | - name: upload artifact macOS 69 | uses: actions/upload-artifact@v1 70 | with: 71 | name: optimus-macOS 72 | path: optimus.zip 73 | if: matrix.os == 'macos-latest' 74 | - name: upload artifact linux 75 | uses: actions/upload-artifact@v2-preview 76 | with: 77 | name: optimus-linux 78 | path: optimus.tar.gz 79 | if: matrix.os == 'ubuntu-latest' 80 | # - name: upload artifact linux appimage 81 | # uses: actions/upload-artifact@v2-preview 82 | # with: 83 | # name: optimus-linux-appimage 84 | # path: Optimus-${{ env.VERSION }}-x86_64.AppImage 85 | # if: matrix.os == 'ubuntu-latest' 86 | - name: upload artifact windows 87 | uses: actions/upload-artifact@v1 88 | with: 89 | name: optimus-windows 90 | path: optimus.zip 91 | if: matrix.os == 'windows-latest' 92 | 93 | release: 94 | runs-on: ubuntu-latest 95 | needs: package 96 | steps: 97 | - name: Create Release 98 | id: create_release 99 | uses: actions/create-release@v1 100 | env: 101 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 102 | with: 103 | tag_name: ${{ github.ref }} 104 | release_name: ${{ github.ref }} 105 | draft: true 106 | prerelease: true 107 | - name: Download macOS package 108 | uses: actions/download-artifact@v1 109 | with: 110 | name: optimus-macOS 111 | - name: Upload macOS package to release 112 | id: upload-macOS-release-asset 113 | uses: actions/upload-release-asset@v1 114 | env: 115 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 116 | with: 117 | upload_url: ${{ steps.create_release.outputs.upload_url }} 118 | asset_path: ./optimus-macOS/optimus.zip 119 | asset_name: optimus_${{ github.ref }}_macOS.zip 120 | asset_content_type: application/octet-stream 121 | - name: Download linux package 122 | uses: actions/download-artifact@v1 123 | with: 124 | name: optimus-linux 125 | - name: Upload Linux package to release 126 | id: upload-linux-release-asset 127 | uses: actions/upload-release-asset@v1 128 | env: 129 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 130 | with: 131 | upload_url: ${{ steps.create_release.outputs.upload_url }} 132 | asset_path: ./optimus-linux/optimus.tar.gz 133 | asset_name: optimus_${{ github.ref }}_linux_x86_64.tar.gz 134 | asset_content_type: application/octet-stream 135 | - name: Download windows package 136 | uses: actions/download-artifact@v1 137 | with: 138 | name: optimus-windows 139 | - name: Upload Windows package to release 140 | id: upload-windows-release-asset 141 | uses: actions/upload-release-asset@v1 142 | env: 143 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 144 | with: 145 | upload_url: ${{ steps.create_release.outputs.upload_url }} 146 | asset_path: ./optimus-windows/optimus.zip 147 | asset_name: optimus_${{ github.ref }}_windows_x86_64.zip 148 | asset_content_type: application/octet-stream 149 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [main] 9 | schedule: 10 | - cron: '0 10 * * 0' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | # Override automatic language detection by changing the below list 21 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 22 | language: ['go', 'javascript'] 23 | # Learn more... 24 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v2 29 | with: 30 | # We must fetch at least the immediate parents so that if this is 31 | # a pull request then we can checkout the head. 32 | fetch-depth: 2 33 | 34 | # If this run was triggered by a pull request event, then checkout 35 | # the head of the pull request instead of the merge commit. 36 | - run: git checkout HEAD^2 37 | if: ${{ github.event_name == 'pull_request' }} 38 | 39 | # Initializes the CodeQL tools for scanning. 40 | - name: Initialize CodeQL 41 | uses: github/codeql-action/init@v1 42 | with: 43 | languages: ${{ matrix.language }} 44 | 45 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 46 | # If this step fails, then you should remove it and run the build manually (see below) 47 | - name: Autobuild 48 | uses: github/codeql-action/autobuild@v1 49 | 50 | # ℹ️ Command-line programs to run using the OS shell. 51 | # 📚 https://git.io/JvXDl 52 | 53 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 54 | # and modify them (or add more) to build your code if your project 55 | # uses a compiled language 56 | 57 | #- run: | 58 | # make bootstrap 59 | # make release 60 | 61 | - name: Perform CodeQL Analysis 62 | uses: github/codeql-action/analyze@v1 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | build/**/* 3 | .idea/ 4 | .vscode/ -------------------------------------------------------------------------------- /.jshint: -------------------------------------------------------------------------------- 1 | { 2 | "esversion": 6 3 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at flyweight@pm.me. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Christopher Murphy 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 | ![Optimus logo](./.github/optimus-logo--960x540.png) 2 | 3 | # Optimus 4 | 5 | > Image compression, optimization and conversion desktop app. 6 | 7 | ## Overview 8 | 9 | Optimus is a desktop image optimization application. It supports conversion and compression between WebP, JPEG, and PNG image formats. 10 | 11 | ## Features 12 | 13 | - Convert to and from JPEG, PNG, and WebP formats. 14 | - Compress JPEG, PNG (lossy), and WebP (lossy and lossless) formats. 15 | - Resize images to various sizes in a single batch operation. 16 | - View simple stats on session and all-time use. 17 | 18 | ![Screenshot of Optimus primary image editor view](./.github/optimus_screenshot_editor--1200x742.png) 19 | 20 | ![Screenshot of Optimus options view](./.github/optimus_screenshot_options--1200x936.png) 21 | 22 | ## Installation 23 | 24 | ### Downloads 25 | 26 | Download the latest version from the [releases page](https://github.com/Splode/optimus/releases). 27 | 28 | Optimus is available for Windows, macOS, and Linux. 29 | 30 | ### Scoop 31 | 32 | ```bash 33 | scoop install https://raw.githubusercontent.com/Splode/optimus/main/optimus.json 34 | ``` 35 | 36 | ## Development 37 | 38 | Optimus is built using [Wails](https://wails.app/) and uses JavaScript on the frontend and Go on the backend. 39 | 40 | Take the following steps to develop locally: 41 | 42 | 1. Clone the repo 43 | 2. Install Wails 44 | 3. Install go and npm dependencies 45 | 46 | ## License 47 | 48 | MIT © 2020 Christopher Murphy 49 | -------------------------------------------------------------------------------- /appicon--512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Splode/optimus/b58bf0d5adc4b07043c6bb4da12f119be08e1d42/appicon--512.png -------------------------------------------------------------------------------- /appicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Splode/optimus/b58bf0d5adc4b07043c6bb4da12f119be08e1d42/appicon.png -------------------------------------------------------------------------------- /backend/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/wailsapp/wails" 7 | "optimus/backend/jpeg" 8 | "optimus/backend/localstore" 9 | "optimus/backend/png" 10 | "optimus/backend/webp" 11 | "os" 12 | "path" 13 | "path/filepath" 14 | "strconv" 15 | ) 16 | 17 | const filename = "conf.json" 18 | 19 | // App represents application persistent configuration values. 20 | type App struct { 21 | OutDir string `json:"outDir"` 22 | Target string `json:"target"` 23 | Prefix string `json:"prefix"` 24 | Suffix string `json:"suffix"` 25 | Sizes []*size `json:"sizes"` 26 | JpegOpt *jpeg.Options `json:"jpegOpt"` 27 | PngOpt *png.Options `json:"pngOpt"` 28 | WebpOpt *webp.Options `json:"webpOpt"` 29 | } 30 | 31 | // Config represents the application settings. 32 | type Config struct { 33 | App *App 34 | Runtime *wails.Runtime 35 | Logger *wails.CustomLogger 36 | localStore *localstore.LocalStore 37 | } 38 | 39 | // WailsInit performs setup when Wails is ready. 40 | func (c *Config) WailsInit(runtime *wails.Runtime) error { 41 | c.Runtime = runtime 42 | c.Logger = c.Runtime.Log.New("Config") 43 | c.Logger.Info("Config initialized...") 44 | return nil 45 | } 46 | 47 | // NewConfig returns a new instance of Config. 48 | func NewConfig() *Config { 49 | c := &Config{} 50 | c.localStore = localstore.NewLocalStore() 51 | 52 | a, err := c.localStore.Load(filename) 53 | if err != nil { 54 | c.App, _ = defaults() 55 | } 56 | if err = json.Unmarshal(a, &c.App); err != nil { 57 | fmt.Printf("error") 58 | } 59 | return c 60 | } 61 | 62 | // GetAppConfig returns the application configuration. 63 | func (c *Config) GetAppConfig() map[string]interface{} { 64 | return map[string]interface{}{ 65 | "outDir": c.App.OutDir, 66 | "target": c.App.Target, 67 | "prefix": c.App.Prefix, 68 | "suffix": c.App.Suffix, 69 | "sizes": c.App.Sizes, 70 | "jpegOpt": c.App.JpegOpt, 71 | "pngOpt": c.App.PngOpt, 72 | "webpOpt": c.App.WebpOpt, 73 | } 74 | } 75 | 76 | // OpenOutputDir opens the output directory using the native system browser. 77 | func (c *Config) OpenOutputDir() error { 78 | if err := c.Runtime.Browser.OpenURL(c.App.OutDir); err != nil { 79 | return err 80 | } 81 | return nil 82 | } 83 | 84 | // RestoreDefaults sets the app configuration to defaults. 85 | func (c *Config) RestoreDefaults() (err error) { 86 | var a *App 87 | a, err = defaults() 88 | if err != nil { 89 | return err 90 | } 91 | c.App = a 92 | if err = c.store(); err != nil { 93 | return err 94 | } 95 | return nil 96 | } 97 | 98 | // SetConfig sets and stores the given configuration. 99 | func (c *Config) SetConfig(cfg string) error { 100 | a := &App{} 101 | if err := json.Unmarshal([]byte(cfg), &a); err != nil { 102 | c.Logger.Errorf("failed to unmarshal config: %v", err) 103 | return err 104 | } 105 | c.App = a 106 | if err := c.store(); err != nil { 107 | c.Logger.Errorf("failed to store config: %v", err) 108 | return err 109 | } 110 | return nil 111 | } 112 | 113 | // SetOutDir opens a directory select dialog and sets the output directory to 114 | // the chosen directory. 115 | func (c *Config) SetOutDir() string { 116 | dir := c.Runtime.Dialog.SelectDirectory() 117 | if dir != "" { 118 | c.App.OutDir = dir 119 | c.Logger.Infof("set output directory: %s", dir) 120 | if err := c.store(); err != nil { 121 | c.Logger.Errorf("failed to store config: %v", err) 122 | } 123 | } 124 | return c.App.OutDir 125 | } 126 | 127 | // defaults returns the application configuration defaults. 128 | func defaults() (*App, error) { 129 | a := &App{ 130 | Target: "webp", 131 | JpegOpt: &jpeg.Options{Quality: 80}, 132 | PngOpt: &png.Options{Quality: 80}, 133 | WebpOpt: &webp.Options{Lossless: false, Quality: 80}, 134 | } 135 | ud, err := os.UserHomeDir() 136 | if err != nil { 137 | fmt.Printf("failed to get user directory: %v", err) 138 | return nil, err 139 | } 140 | 141 | od := path.Join(ud, "Optimus") 142 | cp := filepath.Clean(od) 143 | 144 | if _, err = os.Stat(od); os.IsNotExist(err) { 145 | if err = os.Mkdir(od, 0777); err != nil { 146 | od = "./" 147 | fmt.Printf("failed to create default output directory: %v", err) 148 | return nil, err 149 | } 150 | } 151 | a.OutDir = cp 152 | return a, nil 153 | } 154 | 155 | // store stores the configuration state to the file system. 156 | func (c *Config) store() error { 157 | js, err := json.Marshal(c.GetAppConfig()) 158 | if err != nil { 159 | c.Logger.Errorf("failed to marshal config: %v", err) 160 | return err 161 | } 162 | if err = c.localStore.Store(js, filename); err != nil { 163 | c.Logger.Errorf("failed to store config: %v", err) 164 | return err 165 | } 166 | return nil 167 | } 168 | 169 | // rect represents an image width and height size. 170 | type rect struct { 171 | Height int `json:"height,omitempty"` 172 | Width int `json:"width,omitempty"` 173 | } 174 | 175 | // String returns a string representation of the rect. 176 | // For example, "1280x720" 177 | func (r *rect) String() string { 178 | w := strconv.Itoa(r.Width) 179 | h := strconv.Itoa(r.Height) 180 | return fmt.Sprintf("%sx%s", w, h) 181 | } 182 | 183 | // size represents an image resizing. Strategy represents an image resizing 184 | // strategy, such as cropping. 185 | type size struct { 186 | rect 187 | Strategy int `json:"strategy"` 188 | } 189 | -------------------------------------------------------------------------------- /backend/image/file.go: -------------------------------------------------------------------------------- 1 | package image 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "image" 8 | "io/ioutil" 9 | "optimus/backend/config" 10 | "optimus/backend/jpeg" 11 | "optimus/backend/png" 12 | "optimus/backend/webp" 13 | "os" 14 | "path" 15 | "path/filepath" 16 | 17 | "github.com/disintegration/imaging" 18 | "github.com/muesli/smartcrop" 19 | "github.com/muesli/smartcrop/nfnt" 20 | "github.com/wailsapp/wails" 21 | ) 22 | 23 | const ( 24 | fill = iota 25 | fit 26 | smart 27 | ) 28 | 29 | var mimes = map[string]string{ 30 | "image/.jpg": "jpg", 31 | "image/jpg": "jpg", 32 | "image/jpeg": "jpg", 33 | "image/png": "png", 34 | "image/webp": "webp", 35 | } 36 | 37 | // File represents an image file. 38 | type File struct { 39 | Data []byte `json:"data"` 40 | Ext string `json:"ext"` 41 | ID string `json:"id"` 42 | MimeType string `json:"type"` 43 | Name string `json:"name"` 44 | Size int64 `json:"size"` 45 | ConvertedFile string 46 | IsConverted bool 47 | Image image.Image 48 | Runtime *wails.Runtime 49 | } 50 | 51 | // Decode decodes the file's data based on its mime type. 52 | func (f *File) Decode() error { 53 | mime, err := getFileType(f.MimeType) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | switch mime { 59 | case "jpg": 60 | f.Image, err = jpeg.DecodeJPEG(bytes.NewReader(f.Data)) 61 | case "png": 62 | f.Image, err = png.DecodePNG(bytes.NewReader(f.Data)) 63 | case "webp": 64 | f.Image, err = webp.DecodeWebp(bytes.NewReader(f.Data)) 65 | } 66 | if err != nil { 67 | return err 68 | } 69 | return nil 70 | } 71 | 72 | // GetConvertedSize returns the size of the converted file. 73 | func (f *File) GetConvertedSize() (int64, error) { 74 | if f.ConvertedFile == "" { 75 | return 0, errors.New("file has no converted file") 76 | } 77 | s, err := os.Stat(f.ConvertedFile) 78 | if err != nil { 79 | return 0, err 80 | } 81 | return s.Size(), nil 82 | } 83 | 84 | // GetSavings returns the delta between original and converted file size. 85 | func (f *File) GetSavings() (int64, error) { 86 | c, err := f.GetConvertedSize() 87 | if err != nil { 88 | return 0, err 89 | } 90 | return f.Size - c, nil 91 | } 92 | 93 | // Write saves a file to disk based on the encoding target. 94 | func (f *File) Write(c *config.Config) error { 95 | // TODO resizing should probably be in its own method 96 | if c.App.Sizes != nil { 97 | for _, r := range c.App.Sizes { 98 | if r.Height <= 0 || r.Width <= 0 { 99 | f.Runtime.Events.Emit("notify", map[string]interface{}{ 100 | "msg": fmt.Sprintf("Invalid image size: %s", r.String()), 101 | "type": "warn", 102 | }) 103 | continue 104 | } 105 | var i image.Image 106 | var s string 107 | switch r.Strategy { 108 | case fill: 109 | i = imaging.Fill(f.Image, r.Width, r.Height, imaging.Center, imaging.Lanczos) 110 | s = r.String() 111 | case fit: 112 | i = imaging.Fit(f.Image, r.Width, r.Height, imaging.Lanczos) 113 | s = fmt.Sprintf("%dx%d", i.Bounds().Max.X, i.Bounds().Max.Y) 114 | case smart: 115 | analyzer := smartcrop.NewAnalyzer(nfnt.NewDefaultResizer()) 116 | crop, err := analyzer.FindBestCrop(f.Image, r.Width, r.Height) 117 | if err != nil { 118 | return err 119 | } 120 | croppedImg := f.Image.(SubImager).SubImage(crop) 121 | i = imaging.Resize(croppedImg, r.Width, r.Height, imaging.Lanczos) 122 | s = fmt.Sprintf("%dx%d", i.Bounds().Max.X, i.Bounds().Max.Y) 123 | } 124 | buf, err := encToBuf(i, c.App) 125 | dest := path.Join(c.App.OutDir, c.App.Prefix+f.Name+"--"+s+c.App.Suffix+"."+c.App.Target) 126 | if err != nil { 127 | return err 128 | } 129 | if err = ioutil.WriteFile(dest, buf.Bytes(), 0666); err != nil { 130 | return err 131 | } 132 | } 133 | } 134 | buf, err := encToBuf(f.Image, c.App) 135 | dest := path.Join(c.App.OutDir, c.App.Prefix+f.Name+c.App.Suffix+"."+c.App.Target) 136 | if err != nil { 137 | return err 138 | } 139 | if err = ioutil.WriteFile(dest, buf.Bytes(), 0666); err != nil { 140 | return err 141 | } 142 | f.ConvertedFile = filepath.Clean(dest) 143 | f.IsConverted = true 144 | return nil 145 | } 146 | 147 | // encToBuf encodes an image to a buffer using the configured target. 148 | func encToBuf(i image.Image, a *config.App) (*bytes.Buffer, error) { 149 | var b bytes.Buffer 150 | var err error 151 | switch a.Target { 152 | case "jpg": 153 | b, err = jpeg.EncodeJPEG(i, a.JpegOpt) 154 | case "png": 155 | b, err = png.EncodePNG(i, a.PngOpt) 156 | case "webp": 157 | b, err = webp.EncodeWebp(i, a.WebpOpt) 158 | } 159 | if err != nil { 160 | return nil, err 161 | } 162 | return &b, nil 163 | } 164 | 165 | // getFileType returns the file's type based on the given mime type. 166 | func getFileType(t string) (string, error) { 167 | m, prs := mimes[t] 168 | if !prs { 169 | _ = errors.New("unsupported file type:" + t) 170 | } 171 | return m, nil 172 | } 173 | 174 | // SubImager handles creating a subimage from an image rect. 175 | type SubImager interface { 176 | SubImage(r image.Rectangle) image.Image 177 | } 178 | -------------------------------------------------------------------------------- /backend/image/filemanager.go: -------------------------------------------------------------------------------- 1 | package image 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/wailsapp/wails" 7 | "optimus/backend/config" 8 | "optimus/backend/stat" 9 | "runtime/debug" 10 | "strings" 11 | "sync" 12 | "time" 13 | ) 14 | 15 | // FileManager handles collections of Files for conversion. 16 | type FileManager struct { 17 | Files []*File 18 | 19 | Runtime *wails.Runtime 20 | Logger *wails.CustomLogger 21 | 22 | config *config.Config 23 | stats *stat.Stat 24 | } 25 | 26 | // NewFileManager creates a new FileManager. 27 | func NewFileManager(c *config.Config, s *stat.Stat) *FileManager { 28 | return &FileManager{ 29 | config: c, 30 | stats: s, 31 | } 32 | } 33 | 34 | // WailsInit performs setup when Wails is ready. 35 | func (fm *FileManager) WailsInit(runtime *wails.Runtime) error { 36 | fm.Runtime = runtime 37 | fm.Logger = fm.Runtime.Log.New("FileManager") 38 | fm.Logger.Info("FileManager initialized...") 39 | return nil 40 | } 41 | 42 | // HandleFile processes a file from the client. 43 | func (fm *FileManager) HandleFile(fileJson string) (err error) { 44 | file := &File{Runtime: fm.Runtime} 45 | if err = json.Unmarshal([]byte(fileJson), &file); err != nil { 46 | return err 47 | } 48 | 49 | if err = file.Decode(); err != nil { 50 | return err 51 | } 52 | fm.Files = append(fm.Files, file) 53 | fm.Logger.Infof("added file to file manager: %s", file.Name) 54 | 55 | return nil 56 | } 57 | 58 | // Clear removes the files in the FileManager. 59 | func (fm *FileManager) Clear() { 60 | fm.Files = nil 61 | debug.FreeOSMemory() 62 | } 63 | 64 | // Convert runs the conversion on all files in the FileManager. 65 | func (fm *FileManager) Convert() (errs []error) { 66 | var wg sync.WaitGroup 67 | wg.Add(fm.countUnconverted()) 68 | 69 | c := 0 70 | var b int64 71 | t := time.Now().UnixNano() 72 | for _, file := range fm.Files { 73 | file := file 74 | if !file.IsConverted { 75 | go func(wg *sync.WaitGroup) { 76 | err := file.Write(fm.config) 77 | if err != nil { 78 | fm.Logger.Errorf("failed to convert file: %s, %v", file.ID, err) 79 | fm.Runtime.Events.Emit("notify", map[string]interface{}{ 80 | "msg": fmt.Sprintf("Failed to convert file: %s, %s", file.Name, err.Error()), 81 | "type": "warn", 82 | }) 83 | errs = append(errs, fmt.Errorf("failed to convert file: %s", file.Name)) 84 | } else { 85 | fm.Logger.Info(fmt.Sprintf("converted file: %s", file.Name)) 86 | s, err := file.GetConvertedSize() 87 | if err != nil { 88 | fm.Logger.Errorf("failed to read converted file size: %v", err) 89 | } 90 | fm.Runtime.Events.Emit("conversion:complete", map[string]interface{}{ 91 | "id": file.ID, 92 | // TODO: standardize this path conversion 93 | "path": strings.Replace(file.ConvertedFile, "\\", "/", -1), 94 | "size": s, 95 | }) 96 | c++ 97 | s, err = file.GetSavings() 98 | if err != nil { 99 | fm.Logger.Errorf("failed to get file conversion savings: %v", err) 100 | } 101 | b += s 102 | } 103 | wg.Done() 104 | }(&wg) 105 | } 106 | } 107 | 108 | wg.Wait() 109 | nt := (time.Now().UnixNano() - t) / 1000000 110 | fm.stats.SetImageCount(c) 111 | fm.stats.SetByteCount(b) 112 | fm.stats.SetTimeCount(nt) 113 | fm.Runtime.Events.Emit("conversion:stat", map[string]interface{}{ 114 | "count": c, 115 | "resizes": c * len(fm.config.App.Sizes), 116 | "savings": b, 117 | "time": nt, 118 | }) 119 | fm.Clear() 120 | return errs 121 | } 122 | 123 | // OpenFile opens the file at the given filepath using the file's native file 124 | // application. 125 | func (fm *FileManager) OpenFile(p string) error { 126 | if err := fm.Runtime.Browser.OpenFile(p); err != nil { 127 | fm.Logger.Errorf("failed to open file %s: %v", p, err) 128 | return err 129 | } 130 | return nil 131 | } 132 | 133 | // countUnconverted returns the number of files in the FileManager that haven't 134 | // been converted. 135 | func (fm *FileManager) countUnconverted() int { 136 | c := 0 137 | for _, file := range fm.Files { 138 | if !file.IsConverted { 139 | c++ 140 | } 141 | } 142 | return c 143 | } 144 | -------------------------------------------------------------------------------- /backend/jpeg/jpeg.go: -------------------------------------------------------------------------------- 1 | package jpeg 2 | 3 | import ( 4 | "bytes" 5 | "image" 6 | "image/jpeg" 7 | "io" 8 | ) 9 | 10 | // Options represent JPEG encoding options. 11 | type Options struct { 12 | Quality int `json:"quality"` 13 | } 14 | 15 | // DecodeJPEG decodes a JPEG file and return an image. 16 | func DecodeJPEG(r io.Reader) (image.Image, error) { 17 | i, err := jpeg.Decode(r) 18 | if err != nil { 19 | return nil, err 20 | } 21 | return i, nil 22 | } 23 | 24 | // EncodeJPEG encodes an image into JPEG and returns a buffer. 25 | func EncodeJPEG(i image.Image, o *Options) (buf bytes.Buffer, err error) { 26 | err = jpeg.Encode(&buf, i, &jpeg.Options{Quality: o.Quality}) 27 | return buf, err 28 | } 29 | -------------------------------------------------------------------------------- /backend/localstore/localstore.go: -------------------------------------------------------------------------------- 1 | package localstore 2 | 3 | import ( 4 | "github.com/vrischmann/userdir" 5 | "io/ioutil" 6 | "os" 7 | "path" 8 | ) 9 | 10 | // LocalStore provides reading and writing application data to the user's 11 | // configuration directory. 12 | type LocalStore struct { 13 | ConfDir string 14 | } 15 | 16 | // NewLocalStore returns a localStore instance. 17 | func NewLocalStore() *LocalStore { 18 | return &LocalStore{ConfDir: path.Join(userdir.GetConfigHome(), "Optimus")} 19 | } 20 | 21 | // Load reads the given file in the user's configuration directory and returns 22 | // its contents. 23 | func (l *LocalStore) Load(filename string) ([]byte, error) { 24 | p := path.Join(l.ConfDir, filename) 25 | d, err := ioutil.ReadFile(p) 26 | if err != nil { 27 | return nil, err 28 | } 29 | return d, err 30 | } 31 | 32 | // Store writes data to the user's configuration directory at the given 33 | // filename. 34 | func (l *LocalStore) Store(data []byte, filename string) error { 35 | p := path.Join(l.ConfDir, filename) 36 | if err := ensureDirExists(l.ConfDir); err != nil { 37 | return err 38 | } 39 | if err := ioutil.WriteFile(p, data, 0777); err != nil { 40 | return err 41 | } 42 | return nil 43 | } 44 | 45 | // ensureDirExists checks for the existence of the directory at the given path, 46 | // which is created if it does not exist. 47 | func ensureDirExists(path string) error { 48 | _, err := os.Stat(path) 49 | if os.IsNotExist(err) { 50 | if err = os.Mkdir(path, 0777); err != nil { 51 | return err 52 | } 53 | } 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /backend/png/png.go: -------------------------------------------------------------------------------- 1 | package png 2 | 3 | import ( 4 | "bytes" 5 | "github.com/foobaz/lossypng/lossypng" 6 | "image" 7 | "image/png" 8 | "io" 9 | ) 10 | 11 | const qMax = 20 12 | 13 | // Options represent PNG encoding options. 14 | type Options struct { 15 | Quality int `json:"quality"` 16 | } 17 | 18 | // DecodePNG decodes a PNG file and return an image. 19 | func DecodePNG(r io.Reader) (image.Image, error) { 20 | i, err := png.Decode(r) 21 | if err != nil { 22 | return nil, err 23 | } 24 | return i, nil 25 | } 26 | 27 | // EncodePNG encodes an image into PNG and returns a buffer. 28 | func EncodePNG(i image.Image, o *Options) (buf bytes.Buffer, err error) { 29 | c := lossypng.Compress(i, 2, qualityFactor(o.Quality)) 30 | err = png.Encode(&buf, c) 31 | return buf, err 32 | } 33 | 34 | // qualityFactor normalizes the PNG quality factor from a max of 20, where 0 is 35 | // no conversion. 36 | func qualityFactor(q int) int { 37 | f := q / 100 38 | return qMax - (f * qMax) 39 | } 40 | -------------------------------------------------------------------------------- /backend/stat/stat.go: -------------------------------------------------------------------------------- 1 | package stat 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/wailsapp/wails" 6 | "optimus/backend/localstore" 7 | ) 8 | 9 | const filename = "stats.json" 10 | 11 | // Stat represents application statistics. 12 | type Stat struct { 13 | ByteCount int64 `json:"byteCount"` 14 | ImageCount int `json:"imageCount"` 15 | TimeCount int64 `json:"timeCount"` 16 | 17 | Runtime *wails.Runtime 18 | Logger *wails.CustomLogger 19 | 20 | localStore *localstore.LocalStore 21 | } 22 | 23 | // NewStat returns a new Stat instance. 24 | func NewStat() *Stat { 25 | s := &Stat{ 26 | localStore: localstore.NewLocalStore(), 27 | } 28 | 29 | d, _ := s.localStore.Load(filename) 30 | _ = json.Unmarshal(d, &s) 31 | return s 32 | } 33 | 34 | // WailsInit performs setup when Wails is ready. 35 | func (s *Stat) WailsInit(runtime *wails.Runtime) error { 36 | s.Runtime = runtime 37 | s.Logger = s.Runtime.Log.New("Stat") 38 | s.Logger.Info("Stat initialized...") 39 | return nil 40 | } 41 | 42 | // GetStats returns the application stats. 43 | func (s *Stat) GetStats() map[string]interface{} { 44 | return map[string]interface{}{ 45 | "byteCount": s.ByteCount, 46 | "imageCount": s.ImageCount, 47 | "timeCount": s.TimeCount, 48 | } 49 | } 50 | 51 | // SetByteCount adds and persists the given byte count to the app stats. 52 | func (s *Stat) SetByteCount(b int64) { 53 | if b <= 0 { 54 | return 55 | } 56 | s.ByteCount += b 57 | if err := s.store(); err != nil { 58 | s.Logger.Errorf("failed to store stats: %v", err) 59 | } 60 | } 61 | 62 | // SetImageCount adds and persists the given image count to the app stats. 63 | func (s *Stat) SetImageCount(i int) { 64 | if i <= 0 { 65 | return 66 | } 67 | s.ImageCount += i 68 | if err := s.store(); err != nil { 69 | s.Logger.Errorf("failed to store stats: %v", err) 70 | } 71 | } 72 | 73 | // SetTimeCount adds and persists the given time count to the app stats. 74 | func (s *Stat) SetTimeCount(t int64) { 75 | if t < 0 { 76 | return 77 | } 78 | s.TimeCount += t 79 | if err := s.store(); err != nil { 80 | s.Logger.Errorf("failed to store stats: %v", err) 81 | } 82 | } 83 | 84 | // store stores the app stats to the file system. 85 | func (s *Stat) store() error { 86 | js, err := json.Marshal(s.GetStats()) 87 | if err != nil { 88 | return err 89 | } 90 | if err = s.localStore.Store(js, filename); err != nil { 91 | return err 92 | } 93 | return nil 94 | } 95 | -------------------------------------------------------------------------------- /backend/webp/webp.go: -------------------------------------------------------------------------------- 1 | package webp 2 | 3 | import ( 4 | "bytes" 5 | "github.com/chai2010/webp" 6 | "image" 7 | "io" 8 | ) 9 | 10 | // Options represent WebP encoding options. 11 | type Options struct { 12 | Lossless bool `json:"lossless"` 13 | Quality int `json:"quality"` 14 | } 15 | 16 | // DecodeWebp a webp file and return an image. 17 | func DecodeWebp(r io.Reader) (image.Image, error) { 18 | i, err := webp.Decode(r) 19 | if err != nil { 20 | return nil, err 21 | } 22 | return i, nil 23 | } 24 | 25 | // EncodeWebp encodes an image into webp and returns a buffer. 26 | func EncodeWebp(i image.Image, o *Options) (buf bytes.Buffer, err error) { 27 | if err = webp.Encode(&buf, i, &webp.Options{Lossless: o.Lossless, Quality: float32(o.Quality)}); err != nil { 28 | return buf, err 29 | } 30 | return buf, nil 31 | } 32 | -------------------------------------------------------------------------------- /dmg-spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Optimus installer", 3 | "background-color": "#326DE6", 4 | "icon-size": 80, 5 | "contents": [ 6 | { 7 | "x": 192, 8 | "y": 344, 9 | "type": "file", 10 | "path": "./build/optimus.app" 11 | }, 12 | { 13 | "x": 448, 14 | "y": 344, 15 | "type": "link", 16 | "path": "/Applications" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw* 22 | -------------------------------------------------------------------------------- /frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # vue basic 2 | 3 | ## Project setup 4 | 5 | ``` 6 | npm install 7 | ``` 8 | 9 | ### Compiles and hot-reloads for development 10 | 11 | ``` 12 | npm run serve 13 | ``` 14 | 15 | ### Compiles and minifies for production 16 | 17 | ``` 18 | npm run build 19 | ``` 20 | 21 | ### Run your tests 22 | 23 | ``` 24 | npm run test 25 | ``` 26 | 27 | ### Lints and fixes files 28 | 29 | ``` 30 | npm run lint 31 | ``` 32 | 33 | ### Customize configuration 34 | 35 | See [Configuration Reference](https://cli.vuejs.org/config/). 36 | -------------------------------------------------------------------------------- /frontend/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [['@vue/app', { useBuiltIns: 'entry' }]] 3 | } 4 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Optimus", 3 | "author": { 4 | "name": "Christopher Murphy", 5 | "email": "flyweight@protonmail.com" 6 | }, 7 | "private": true, 8 | "version": "0.5.0-beta", 9 | "scripts": { 10 | "serve": "vue-cli-service serve", 11 | "build": "vue-cli-service build", 12 | "lint": "vue-cli-service lint" 13 | }, 14 | "dependencies": { 15 | "@wailsapp/runtime": "^1.1.1", 16 | "autoprefixer": "^9.8.6", 17 | "core-js": "^3.6.4", 18 | "regenerator-runtime": "^0.13.7", 19 | "tailwindcss": "^1.9.1", 20 | "v-tooltip": "^2.0.3", 21 | "vue": "^2.6.12", 22 | "vue-slider-component": "^3.2.6", 23 | "vuex": "^3.5.1" 24 | }, 25 | "devDependencies": { 26 | "@vue/cli-plugin-babel": "^4.5.7", 27 | "@vue/cli-plugin-eslint": "^4.5.7", 28 | "@vue/cli-service": "^4.5.7", 29 | "babel-eslint": "^10.1.0", 30 | "eslint": "^7.11.0", 31 | "eslint-plugin-vue": "^7.0.1", 32 | "eventsource-polyfill": "^0.9.6", 33 | "vue-template-compiler": "^2.6.12", 34 | "webpack-hot-middleware": "^2.25.0" 35 | }, 36 | "eslintConfig": { 37 | "root": true, 38 | "env": { 39 | "node": true 40 | }, 41 | "extends": [ 42 | "plugin:vue/essential", 43 | "eslint:recommended" 44 | ], 45 | "rules": {}, 46 | "parserOptions": { 47 | "parser": "babel-eslint" 48 | } 49 | }, 50 | "browserslist": [ 51 | "> 1%", 52 | "last 2 versions", 53 | "not ie <= 8" 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /frontend/package.json.md5: -------------------------------------------------------------------------------- 1 | 5426204eb3ec3fb253ed2169cd3fd066 -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('tailwindcss')('tailwind.config.js'), 4 | require('autoprefixer')() 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 58 | -------------------------------------------------------------------------------- /frontend/src/assets/css/animations.css: -------------------------------------------------------------------------------- 1 | .fade-enter-active, .fade-leave-active { 2 | transition: opacity 600ms cubic-bezier(.07, .95, 0, 1); 3 | } 4 | 5 | .fade-enter, .fade-leave-to { 6 | opacity: 0; 7 | } 8 | 9 | .fade-fast-enter-active, .fade-fast-leave-active { 10 | transition: opacity 200ms ease-in-out; 11 | } 12 | 13 | .fade-fast-enter, .fade-fast-leave-to { 14 | opacity: 0; 15 | } 16 | 17 | .fade-list-enter-active, .fade-list-leave-active { 18 | transition: all 1.2s ease-in-out; 19 | } 20 | 21 | .fade-list-enter, .fade-list-leave-to { 22 | opacity: 0; 23 | transform: translateX(3rem); 24 | } 25 | 26 | .fade-list-move { 27 | transition: all 1s ease-in-out; 28 | } 29 | 30 | .ta { 31 | transition: all .3s cubic-bezier(.07, .95, 0, 1); 32 | } 33 | 34 | .ta-slow { 35 | transition: all 1s cubic-bezier(.07, .95, 0, 1); 36 | } 37 | 38 | .ta-color { 39 | transition: color .3s cubic-bezier(.07, .95, 0, 1); 40 | } 41 | 42 | .ta-color-slow { 43 | transition: color 1s cubic-bezier(.07, .95, 0, 1); 44 | } 45 | 46 | .anime-txt-success { 47 | animation: txt-success 600ms ease-in-out forwards; 48 | } 49 | 50 | @keyframes txt-success { 51 | 0% { 52 | color: #b3b3b3; 53 | } 54 | 50% { 55 | color: #27ffa7; 56 | } 57 | 100% { 58 | color: #b3b3b3; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /frontend/src/assets/css/buttons.css: -------------------------------------------------------------------------------- 1 | .btn { 2 | @apply border-2 flex font-medium items-center justify-center px-8 py-2 rounded-full; 3 | min-width: 136px; 4 | min-height: 40px; 5 | } 6 | 7 | .btn--disabled { 8 | @apply bg-gray-700 border-gray-700 cursor-default text-gray-400; 9 | } -------------------------------------------------------------------------------- /frontend/src/assets/css/input.css: -------------------------------------------------------------------------------- 1 | /* vue-select */ 2 | 3 | .dropdown-toggle { 4 | background-color: #18181f; 5 | background-image: none; 6 | border-radius: 0.375rem; 7 | color: #b3b3b3; 8 | font-weight: 400; 9 | margin: 0; 10 | transition: color .3s cubic-bezier(.07, .95, 0, 1); 11 | } 12 | 13 | .dropdown-toggle:hover { 14 | background-color: #18181f; 15 | color: #27ffa7; 16 | } 17 | 18 | .dropdown-menu { 19 | background-color: #18181f; 20 | border: 0; 21 | border-top: 1px solid #3a3a42; 22 | border-top-left-radius: 0; 23 | border-top-right-radius: 0; 24 | } 25 | 26 | .dropdown-menu > li > a { 27 | color: #b3b3b3; 28 | font-weight: 500; 29 | transition: all .3s cubic-bezier(.07, .95, 0, 1); 30 | } 31 | 32 | .dropdown-menu > li > a:hover { 33 | background-color: #27ffa7; 34 | color: #18181f; 35 | } 36 | 37 | /* vue slider*/ 38 | 39 | .vue-slider-rail { 40 | background-color: #18181f; 41 | } 42 | 43 | .vue-slider:hover .vue-slider-rail { 44 | background-color: #18181f; 45 | } 46 | 47 | .vue-slider:hover .vue-slider-dot-handle { 48 | border-color: transparent; 49 | opacity: 1; 50 | } 51 | 52 | .vue-slider:hover .vue-slider-dot-handle:hover { 53 | border-color: transparent; 54 | } 55 | 56 | .vue-slider-dot-handle { 57 | background-color: #cbccd2; 58 | border-color: transparent; 59 | opacity: 0; 60 | transition: all .6s cubic-bezier(.07, .95, 0, 1); 61 | } 62 | 63 | .slider-blue .vue-slider-dot-handle:hover, 64 | .slider-pink .vue-slider-dot-handle:hover, 65 | .slider-yellow .vue-slider-dot-handle:hover { 66 | border-color: transparent; 67 | } 68 | 69 | 70 | .vue-slider-dot-handle:hover, 71 | .vue-slider-process, 72 | .vue-slider:hover .vue-slider-process, 73 | .vue-slider-dot-handle { 74 | background-color: #27ffa7; 75 | } 76 | 77 | .vue-slider-dot-tooltip-inner { 78 | background-color: #18181f; 79 | border-color: #18181f; 80 | color: #f4f5f9; 81 | } 82 | -------------------------------------------------------------------------------- /frontend/src/assets/css/main.css: -------------------------------------------------------------------------------- 1 | @import url('animations.css'); 2 | @import url('buttons.css'); 3 | @import url('input.css'); 4 | @import url('tooltip.css'); 5 | 6 | @tailwind base; 7 | @tailwind components; 8 | @tailwind utilities; 9 | 10 | 11 | #app { 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | color: #b3b3b3; 15 | } 16 | 17 | * { 18 | user-select: none; 19 | } 20 | 21 | html { 22 | background-color: #18181f; 23 | background-size: 20px 20px; 24 | overflow: hidden; 25 | height: 100%; 26 | } 27 | 28 | body { 29 | scrollbar-base-color: #18181f; 30 | scrollbar-face-color: #212128; 31 | scrollbar-3dlight-color: #18181f; 32 | scrollbar-highlight-color: #18181f; 33 | scrollbar-track-color: #18181f; 34 | scrollbar-arrow-color: #18181f; 35 | scrollbar-shadow-color: #212128; 36 | height: 100%; 37 | } 38 | -------------------------------------------------------------------------------- /frontend/src/assets/css/tooltip.css: -------------------------------------------------------------------------------- 1 | .tooltip { 2 | background-color: #18181f; 3 | /*border: 2px solid #3a3a42;*/ 4 | border-radius: 6px; 5 | color: #f4f5f9; 6 | display: block; 7 | font-size: 14px; 8 | max-width: 280px; 9 | padding: 0.5rem 1rem; 10 | z-index: 100; 11 | } -------------------------------------------------------------------------------- /frontend/src/assets/images/optimus-logo--640x360.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Splode/optimus/b58bf0d5adc4b07043c6bb4da12f119be08e1d42/frontend/src/assets/images/optimus-logo--640x360.png -------------------------------------------------------------------------------- /frontend/src/components/About.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 66 | 67 | 79 | -------------------------------------------------------------------------------- /frontend/src/components/BtnClose.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 40 | 41 | 58 | -------------------------------------------------------------------------------- /frontend/src/components/Dropdown.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 94 | 95 | 196 | -------------------------------------------------------------------------------- /frontend/src/components/Editor.vue: -------------------------------------------------------------------------------- 1 | 267 | 268 | 706 | 707 | 747 | -------------------------------------------------------------------------------- /frontend/src/components/Notification.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /frontend/src/components/Settings.vue: -------------------------------------------------------------------------------- 1 | 284 | 285 | 513 | 514 | 534 | -------------------------------------------------------------------------------- /frontend/src/components/Sidebar.vue: -------------------------------------------------------------------------------- 1 | 80 | 81 | 99 | 100 | 153 | -------------------------------------------------------------------------------- /frontend/src/components/Stats.vue: -------------------------------------------------------------------------------- 1 | 79 | 80 | 104 | -------------------------------------------------------------------------------- /frontend/src/lib/event-bus.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | export const EventBus = new Vue() 4 | -------------------------------------------------------------------------------- /frontend/src/lib/file.js: -------------------------------------------------------------------------------- 1 | /** 2 | * fExt returns the extension of a given file. 3 | * @param {string} filename - The filename. 4 | * @returns {string} 5 | */ 6 | export function fExt(filename) { 7 | return filename.split('.').pop() 8 | } 9 | 10 | /** 11 | * fName returns the name of a given file without its extension. 12 | * @param {string} filename - The filename. 13 | * @returns {string} 14 | */ 15 | export function fName(filename) { 16 | filename = filename.replace(/\\/g, '/') 17 | return filename.substring( 18 | filename.lastIndexOf('/') + 1, 19 | filename.lastIndexOf('.') 20 | ) 21 | } 22 | 23 | /** 24 | * fSize returns a pretty string from a number of bytes. 25 | * For example, 1024 converts to "1 MB" 26 | * @param {number} bytes - File size in bytes. 27 | * @returns {string} 28 | */ 29 | export function fSize(bytes) { 30 | if (bytes === 0) { 31 | return '0.00 B' 32 | } 33 | const e = Math.floor(Math.log(bytes) / Math.log(1024)) 34 | return (bytes / Math.pow(1024, e)).toFixed(2) + ' ' + ' KMGTP'.charAt(e) + 'B' 35 | } 36 | -------------------------------------------------------------------------------- /frontend/src/lib/time.js: -------------------------------------------------------------------------------- 1 | /** 2 | * prettyTime returns a human-friendly time representation from milliseconds. 3 | * @param {number} ms 4 | * @returns {(string)[]} 5 | */ 6 | export function prettyTime(ms) { 7 | const seconds = (ms / 1000).toFixed(1) 8 | const minutes = (ms / (1000 * 60)).toFixed(1) 9 | const hours = (ms / (1000 * 60 * 60)).toFixed(1) 10 | const days = (ms / (1000 * 60 * 60 * 24)).toFixed(1) 11 | 12 | if (ms < 1000) { 13 | return [ms, 'Milliseconds'] 14 | } else if (seconds < 60) { 15 | return [seconds, 'Seconds'] 16 | } else if (minutes < 60) { 17 | return [minutes, 'Minutes'] 18 | } else if (hours < 24) { 19 | return [hours, 'Hours'] 20 | } else { 21 | return [days, 'Days'] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/main.js: -------------------------------------------------------------------------------- 1 | import 'core-js/stable' 2 | import 'regenerator-runtime/runtime' 3 | import Vue from 'vue' 4 | import App from './App.vue' 5 | import store from './store' 6 | import * as Wails from '@wailsapp/runtime' 7 | import VTooltip from 'v-tooltip' 8 | 9 | Vue.use(VTooltip, { defaultDelay: 600, defaultOffset: 16 }) 10 | 11 | Vue.config.productionTip = false 12 | Vue.config.devtools = true 13 | 14 | Wails.Init(() => { 15 | new Vue({ 16 | render: h => h(App), 17 | store 18 | }).$mount('#app') 19 | }) 20 | -------------------------------------------------------------------------------- /frontend/src/store.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import { fSize } from './lib/file' 4 | import { prettyTime } from './lib/time' 5 | 6 | Vue.use(Vuex) 7 | 8 | const store = new Vuex.Store({ 9 | state: { 10 | config: { 11 | outDir: '', 12 | target: '', 13 | prefix: '', 14 | suffix: '', 15 | sizes: [], 16 | jpegOpt: { quality: 0 }, 17 | pngOpt: { quality: 0 }, 18 | webpOpt: { lossless: false, quality: 0 } 19 | }, 20 | stats: { 21 | byteCount: 0, 22 | imageCount: 0, 23 | timeCount: 0 24 | }, 25 | session: { 26 | count: 0, 27 | savings: 0, 28 | time: 0 29 | } 30 | }, 31 | getters: { 32 | config(state) { 33 | return state.config 34 | }, 35 | 36 | session(state) { 37 | return { 38 | count: state.session.count, 39 | hasSavings: state.session.savings > 0, 40 | savings: fSize(state.session.savings), 41 | time: prettyTime(state.session.time) 42 | } 43 | }, 44 | 45 | stats(state) { 46 | return { 47 | byteCount: fSize(state.stats.byteCount), 48 | imageCount: state.stats.imageCount, 49 | timeCount: prettyTime(state.stats.timeCount) 50 | } 51 | } 52 | }, 53 | actions: { 54 | addSize(context) { 55 | context.commit('addSize') 56 | }, 57 | removeSize(context, index) { 58 | context.commit('removeSize', index) 59 | }, 60 | setSizeStrategy(context, payload) { 61 | context.commit('setSizeStrategy', payload) 62 | }, 63 | 64 | getConfig(context) { 65 | window.backend.Config.GetAppConfig() 66 | .then(cfg => { 67 | context.commit('setConfig', cfg) 68 | }) 69 | .catch(err => { 70 | console.error(err) 71 | }) 72 | }, 73 | 74 | setConfig(context, c) { 75 | window.backend.Config.SetConfig(JSON.stringify(c)) 76 | .then(() => { 77 | context.dispatch('getConfig') 78 | }) 79 | .catch(err => { 80 | console.error(err) 81 | }) 82 | }, 83 | 84 | setConfigProp(context, payload) { 85 | context.commit('setConfigProp', payload) 86 | }, 87 | 88 | setSessionProp(context, payload) { 89 | context.commit('setSessionProp', payload) 90 | }, 91 | 92 | toggleWebpLossless(context) { 93 | context.commit('toggleWebpLossless') 94 | }, 95 | 96 | getStats(context) { 97 | window.backend.Stat.GetStats() 98 | .then(s => { 99 | context.commit('setStats', s) 100 | }) 101 | .catch(err => { 102 | console.error(err) 103 | }) 104 | }, 105 | setStats(context, s) { 106 | context.commit('setStats', s) 107 | } 108 | }, 109 | mutations: { 110 | addSize(state) { 111 | const s = { height: null, width: null, strategy: 0 } 112 | if (!state.config.sizes) { 113 | state.config.sizes = [s] 114 | } else { 115 | state.config.sizes.push(s) 116 | } 117 | }, 118 | removeSize(state, index) { 119 | state.config.sizes.splice(index, 1) 120 | }, 121 | setSizeStrategy(state, payload) { 122 | state.config.sizes[payload.index].strategy = payload.value 123 | }, 124 | 125 | setConfig(state, c) { 126 | state.config = c 127 | }, 128 | 129 | setConfigProp(state, payload) { 130 | state.config[payload.key] = payload.value 131 | }, 132 | 133 | setSessionProp(state, payload) { 134 | state.session[payload.key] += payload.value 135 | }, 136 | 137 | setStats(state, s) { 138 | state.stats = s 139 | }, 140 | 141 | toggleWebpLossless(state) { 142 | state.config.webpOpt.lossless = !state.config.webpOpt.lossless 143 | } 144 | } 145 | }) 146 | 147 | export default store 148 | -------------------------------------------------------------------------------- /frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: ['./src/**/*.vue'], 3 | theme: { 4 | colors: { 5 | blue: { 6 | default: '#27d1ff' 7 | }, 8 | gray: { 9 | 100: '#f4f5f9', 10 | 200: '#b3b3b3', 11 | 300: '#808080', 12 | 400: '#666666', 13 | 700: '#3a3a42', 14 | 800: '#212128', 15 | 900: '#18181f' 16 | }, 17 | green: { 18 | default: '#27ffa7' 19 | }, 20 | orange: { 21 | default: '#ff9b45' 22 | }, 23 | pink: { 24 | default: '#ff45bd' 25 | }, 26 | purple: { 27 | 400: '#d690ff', 28 | default: '#ba45ff' 29 | }, 30 | red: { 31 | default: '#f84d53' 32 | }, 33 | yellow: { 34 | default: '#ffe027' 35 | } 36 | } 37 | }, 38 | variants: {}, 39 | plugins: [] 40 | } 41 | -------------------------------------------------------------------------------- /frontend/vue.config.js: -------------------------------------------------------------------------------- 1 | let cssConfig = {} 2 | 3 | if (process.env.NODE_ENV === 'production') { 4 | cssConfig = { 5 | extract: { 6 | filename: '[name].css', 7 | chunkFilename: '[name].css' 8 | } 9 | } 10 | } 11 | 12 | module.exports = { 13 | chainWebpack: config => { 14 | let limit = 9999999999999999 15 | config.module 16 | .rule('images') 17 | .test(/\.(png|gif|jpg)(\?.*)?$/i) 18 | .use('url-loader') 19 | .loader('url-loader') 20 | .tap(options => Object.assign(options, { limit: limit })) 21 | config.module 22 | .rule('fonts') 23 | .test(/\.(woff2?|eot|ttf|otf|svg)(\?.*)?$/i) 24 | .use('url-loader') 25 | .loader('url-loader') 26 | .options({ 27 | limit: limit 28 | }) 29 | }, 30 | css: cssConfig, 31 | configureWebpack: { 32 | output: { 33 | filename: '[name].js' 34 | }, 35 | optimization: { 36 | splitChunks: false 37 | } 38 | }, 39 | devServer: { 40 | disableHostCheck: true 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module optimus 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/Masterminds/semver v1.5.0 // indirect 7 | github.com/chai2010/webp v1.1.0 8 | github.com/disintegration/imaging v1.6.2 9 | github.com/fatih/color v1.9.0 // indirect 10 | github.com/foobaz/lossypng v0.0.0-20200814224715-48fa8819852a 11 | github.com/leaanthony/slicer v1.4.1 // indirect 12 | github.com/mattn/go-colorable v0.1.7 // indirect 13 | github.com/muesli/smartcrop v0.3.0 14 | github.com/pkg/errors v0.9.1 // indirect 15 | github.com/vrischmann/userdir v0.0.0-20151206171402-20f291cebd68 16 | github.com/wailsapp/wails v1.16.5 17 | golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 // indirect 18 | golang.org/x/net v0.0.0-20200930145003-4acb6c075d10 // indirect 19 | golang.org/x/sys v0.0.0-20200929083018-4d22bbb62b3c // indirect 20 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= 2 | github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= 3 | github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= 4 | github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc= 5 | github.com/abadojack/whatlanggo v1.0.1 h1:19N6YogDnf71CTHm3Mp2qhYfkRdyvbgwWdd2EPxJRG4= 6 | github.com/abadojack/whatlanggo v1.0.1/go.mod h1:66WiQbSbJBIlOZMsvbKe5m6pzQovxCH9B/K8tQB2uoc= 7 | github.com/chai2010/webp v1.1.0 h1:4Ei0/BRroMF9FaXDG2e4OxwFcuW2vcXd+A6tyqTJUQQ= 8 | github.com/chai2010/webp v1.1.0/go.mod h1:LP12PG5IFmLGHUU26tBiCBKnghxx3toZFwDjOYvd3Ow= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= 13 | github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= 14 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 15 | github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= 16 | github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= 17 | github.com/foobaz/lossypng v0.0.0-20200814224715-48fa8819852a h1:0TYY/syyvt/+y5PWAkybgG2o6zHY+UrI3fuixaSeRoI= 18 | github.com/foobaz/lossypng v0.0.0-20200814224715-48fa8819852a/go.mod h1:wRxTcIExb9GZAgOr1wrQuOZBkyoZNQi7znUmeyKTciA= 19 | github.com/go-playground/colors v1.2.0 h1:0EdjTXKrr2g1L/LQTYtIqabeHpZuGZz1U4osS1T8+5M= 20 | github.com/go-playground/colors v1.2.0/go.mod h1:miw1R2JIE19cclPxsXqNdzLZsk4DP4iF+m88bRc7kfM= 21 | github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= 22 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 23 | github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A= 24 | github.com/jackmordaunt/icns v1.0.0 h1:RYSxplerf/l/DUd09AHtITwckkv/mqjVv4DjYdPmAMQ= 25 | github.com/jackmordaunt/icns v1.0.0/go.mod h1:7TTQVEuGzVVfOPPlLNHJIkzA6CoV7aH1Dv9dW351oOo= 26 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 27 | github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o= 28 | github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak= 29 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 30 | github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= 31 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 32 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 33 | github.com/leaanthony/slicer v1.4.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY= 34 | github.com/leaanthony/slicer v1.4.1 h1:X/SmRIDhkUAolP79mSTO0jTcVX1k504PJBqvV6TwP0w= 35 | github.com/leaanthony/slicer v1.4.1/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY= 36 | github.com/leaanthony/spinner v0.5.3 h1:IMTvgdQCec5QA4qRy0wil4XsRP+QcG1OwLWVK/LPZ5Y= 37 | github.com/leaanthony/spinner v0.5.3/go.mod h1:oHlrvWicr++CVV7ALWYi+qHk/XNA91D9IJ48IqmpVUo= 38 | github.com/leaanthony/synx v0.1.0 h1:R0lmg2w6VMb8XcotOwAe5DLyzwjLrskNkwU7LLWsyL8= 39 | github.com/leaanthony/synx v0.1.0/go.mod h1:Iz7eybeeG8bdq640iR+CwYb8p+9EOsgMWghkSRyZcqs= 40 | github.com/leaanthony/wincursor v0.1.0 h1:Dsyp68QcF5cCs65AMBmxoYNEm0n8K7mMchG6a8fYxf8= 41 | github.com/leaanthony/wincursor v0.1.0/go.mod h1:7TVwwrzSH/2Y9gLOGH+VhA+bZhoWXBRgbGNTMk+yimE= 42 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 43 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 44 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 45 | github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw= 46 | github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 47 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 48 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 49 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 50 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 51 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 52 | github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= 53 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 54 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 55 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 56 | github.com/muesli/smartcrop v0.3.0 h1:JTlSkmxWg/oQ1TcLDoypuirdE8Y/jzNirQeLkxpA6Oc= 57 | github.com/muesli/smartcrop v0.3.0/go.mod h1:i2fCI/UorTfgEpPPLWiFBv4pye+YAG78RwcQLUkocpI= 58 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= 59 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= 60 | github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98= 61 | github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= 62 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 63 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 64 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 65 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 66 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 67 | github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k= 68 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 69 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 70 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 71 | github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 72 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 73 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 74 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 75 | github.com/syossan27/tebata v0.0.0-20180602121909-b283fe4bc5ba h1:2DHfQOxcpWdGf5q5IzCUFPNvRX9Icf+09RvQK2VnJq0= 76 | github.com/syossan27/tebata v0.0.0-20180602121909-b283fe4bc5ba/go.mod h1:iLnlXG2Pakcii2CU0cbY07DRCSvpWNa7nFxtevhOChk= 77 | github.com/vrischmann/userdir v0.0.0-20151206171402-20f291cebd68 h1:Ah2/69Z24rwD6OByyOdpJDmttftz0FTF8Q4QZ/SF1E4= 78 | github.com/vrischmann/userdir v0.0.0-20151206171402-20f291cebd68/go.mod h1:EqKqAeKddSL9XSGnfXd/7iLncccKhR16HBKVva7ENw8= 79 | github.com/wailsapp/wails v1.16.5 h1:6kGXCeiTwQsm/vkKqtr/StzH2BRXV/uBZe6afUSuWbg= 80 | github.com/wailsapp/wails v1.16.5/go.mod h1:aADbAvTzZrKGd4Td7d1l4Dp5Hx7lLJEvVH7guIHoDf8= 81 | golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 82 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 83 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 84 | golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 85 | golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 86 | golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 h1:QelT11PB4FXiDEXucrfNckHoFxwt8USGY1ajP1ZF5lM= 87 | golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 88 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 89 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 90 | golang.org/x/net v0.0.0-20200930145003-4acb6c075d10 h1:YfxMZzv3PjGonQYNUaeU2+DhAdqOxerQ30JFB6WgAXo= 91 | golang.org/x/net v0.0.0-20200930145003-4acb6c075d10/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 92 | golang.org/x/sys v0.0.0-20180606202747-9527bec2660b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 93 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 94 | golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 95 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 96 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 97 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 98 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 99 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 100 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 101 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 102 | golang.org/x/sys v0.0.0-20200929083018-4d22bbb62b3c h1:/h0vtH0PyU0xAoZJVcRw1k0Ng+U0JAy3QDiFmppIlIE= 103 | golang.org/x/sys v0.0.0-20200929083018-4d22bbb62b3c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 104 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 105 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 106 | gopkg.in/AlecAivazis/survey.v1 v1.8.4/go.mod h1:iBNOmqKz/NUbZx3bA+4hAGLRC7fSK7tgtVDT4tB22XA= 107 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 108 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 109 | gopkg.in/yaml.v3 v3.0.0-20190709130402-674ba3eaed22/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 110 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= 111 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 112 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "embed" 5 | "optimus/backend/config" 6 | "optimus/backend/image" 7 | "optimus/backend/stat" 8 | 9 | "github.com/wailsapp/wails" 10 | ) 11 | 12 | //go:embed frontend/dist/app.js 13 | var js string 14 | 15 | //go:embed frontend/dist/app.css 16 | var css string 17 | 18 | func main() { 19 | app := wails.CreateApp(&wails.AppConfig{ 20 | Width: 1200, 21 | Height: 742, 22 | Title: "Optimus", 23 | JS: js, 24 | CSS: css, 25 | Colour: "#18181f", 26 | Resizable: true, 27 | }) 28 | 29 | cfg := config.NewConfig() 30 | st := stat.NewStat() 31 | fm := image.NewFileManager(cfg, st) 32 | 33 | app.Bind(cfg) 34 | app.Bind(st) 35 | app.Bind(fm) 36 | _ = app.Run() 37 | } 38 | -------------------------------------------------------------------------------- /optimus.AppImage.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Optimus 3 | Exec=optimus 4 | Icon=appicon--512 5 | Type=Application 6 | Categories=GTK;GNOME;Utility;Development; -------------------------------------------------------------------------------- /optimus.json: -------------------------------------------------------------------------------- 1 | { 2 | "bin": "optimus.exe", 3 | "description": "Optimus is an image conversion and optimization application. It supports conversion and compression between WebP, JPEG, and PNG image formats.", 4 | "hash": "9e90cf09e659f7ecf5144d05d00fbb7315739dabd2dea14bb6294c5d26b3e761", 5 | "homepage": "https://github.com/splode/optimus", 6 | "license": "MIT", 7 | "shortcuts": [["optimus.exe", "Optimus"]], 8 | "url": "https://github.com/Splode/optimus/releases/download/v0.5.0-beta/optimus_v0.5.0-beta_windows_x86_64.zip", 9 | "version": "0.5.0-beta" 10 | } 11 | -------------------------------------------------------------------------------- /project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Optimus", 3 | "description": "Image conversion and compression. Supports WebP, JPEG, and PNG.", 4 | "author": { 5 | "name": "Christopher Murphy", 6 | "email": "flyweight@protonmail.com" 7 | }, 8 | "version": "0.5.0-beta", 9 | "binaryname": "optimus", 10 | "frontend": { 11 | "dir": "frontend", 12 | "install": "npm install", 13 | "build": "npm run build", 14 | "bridge": "src", 15 | "serve": "npm run serve" 16 | }, 17 | "WailsVersion": "1.7.1", 18 | "CrossCompile": false, 19 | "Platform": "", 20 | "Architecture": "", 21 | "LdFlags": "" 22 | } 23 | -------------------------------------------------------------------------------- /wails.json: -------------------------------------------------------------------------------- 1 | { 2 | "email": "flyweight@pm.me", 3 | "name": "Christopher Murphy" 4 | } --------------------------------------------------------------------------------