<|start_filename|>package.json<|end_filename|>
{
  "name": "crushee-desktop",
  "version": "2.4.0",
  "description": "Crushee",
  "main": "src/electron.js",
  "repository": "github:xanderfrangos/crushee",
  "dependencies": {
    "@xanderfrangos/imagemin-gifsicle": "^7.0.0",
    "appdata-path": "^1.0.0",
    "css-loader": "^5.0.1",
    "del": "^6.0.0",
    "electron-is-dev": "^1.2.0",
    "imagemin": "^7.0.1",
    "imagemin-advpng": "^6.0.0",
    "imagemin-jpeg-recompress": "^7.0.0",
    "imagemin-jpegtran": "^7.0.0",
    "imagemin-mozjpeg": "^9.0.0",
    "imagemin-pngquant": "^9.0.1",
    "imagemin-svgo": "^8.0.0",
    "react": "^17.0.1",
    "react-dom": "^17.0.1",
    "react-window": "^1.8.6",
    "sharp": "^0.28.2",
    "slash": "^3.0.0",
    "universal-analytics": "^0.4.23",
    "uuid": "^8.3.2"
  },
  "devDependencies": {
    "@babel/core": "^7.12.10",
    "@babel/plugin-proposal-class-properties": "^7.12.1",
    "@babel/preset-react": "^7.12.10",
    "concurrently": "5.3.0",
    "cross-env": "7.0.3",
    "electron": "11.1.1",
    "electron-builder": "22.9.1",
    "parcel-bundler": "^1.12.5",
    "sass": "^1.32.5",
    "wait-on": "5.2.1"
  },
  "scripts": {
    "parcel": "parcel src/html/*.html --port 3001 --out-dir build",
    "parcel-build": "parcel build src/html/*.html --out-dir build --public-url ./ --no-minify",
    "dev": "wait-on http://localhost:3001/index.html && electron . ",
    "electron-build": "electron-builder --x64",
    "postinstall": "electron-builder install-app-deps",
    "appx": "npm run parcel-build && electron-builder -w appx -c.extraMetadata.name=crushee-appx ",
    "appx-signed": "npm run parcel-build && electron-builder -w appx -c.win.certificateFile=cert.pfx -c.extraMetadata.name=crushee-appx",
    "mac": "npm run parcel-build && electron-builder --x64 ",
    "mac-unsigned": "npm run parcel-build && electron-builder --x64 -c.mac.identity=null",
    "build": "npm run parcel-build && npm run electron-build",
    "start": "concurrently \"npm run parcel\" \"npm run dev\""
  },
  "author": "<NAME>",
  "license": "MIT",
  "build": {
    "productName": "Crushee",
    "appId": "com.xanderfrangos.crushee",
    "asar": false,
    "directories": {
      "buildResources": "resources"
    },
    "mac": {
      "icon": "src/assets/icon-shadow.icns",
      "category": "",
      "darkModeSupport": true,
      "artifactName": "${productName}-v${version}-${os}.${ext}",
      "hardenedRuntime": false
    },
    "win": {
      "icon": "src/assets/icon-shadow.ico",
      "artifactName": "${productName}-v${version}-${os}-setup.${ext}"
    },
    "appx": {
      "publisherDisplayName": "<NAME>",
      "applicationId": "Crushee",
      "displayName": "Crushee",
      "identityName": "38002AlexanderFrangos.Crushee",
      "publisher": "CN=B8E9A58B-32A7-4C6C-A474-D4BE2A3CEAD8",
      "showNameOnTiles": true
    },
    "files": [
      "src/electron.js",
      "src/app.preload.js",
      "src/assets/*",
      "src/optimizer/server.js",
      "src/optimizer/file-thread.js",
      "src/**/*",
      "build/**/*"
    ]
  },
  "engines": {
    "node": ">=12.0.0"
  },
  "os": [
    "win32",
    "darwin"
  ]
}


<|start_filename|>src/settings.js<|end_filename|>
import React from "react";
import ReactDOM from "react-dom";
import SettingsWindow from "./Components/SettingsWindow";

window.GlobalSettings = {
    Quality: {
        app: {}
    }
}

ReactDOM.render(<SettingsWindow theme={window.settings.theme} />, document.getElementById("settings"));

<|start_filename|>src/css/nunito/stylesheet.css<|end_filename|>
/*! Generated by Font Squirrel (https://www.fontsquirrel.com) on December 28, 2019 */



@font-face {
    font-family: 'Nunito';
    src: url('nunito-black-webfont.woff2') format('woff2');
    font-weight: 900;
    font-style: normal;

}




@font-face {
    font-family: 'Nunito';
    src: url('nunito-bold-webfont.woff2') format('woff2');
    font-weight: 700;
    font-style: normal;

}




@font-face {
    font-family: 'Nunito';
    src: url('nunito-extrabold-webfont.woff2') format('woff2');
    font-weight: 800;
    font-style: normal;

}




@font-face {
    font-family: 'Nunito';
    src: url('nunito-extralight-webfont.woff2') format('woff2');
    font-weight: 200;
    font-style: normal;

}




@font-face {
    font-family: 'Nunito';
    src: url('nunito-light-webfont.woff2') format('woff2');
    font-weight: 300;
    font-style: normal;

}




@font-face {
    font-family: 'Nunito';
    src: url('nunito-medium-webfont.woff2') format('woff2');
    font-weight: 500;
    font-style: normal;

}




@font-face {
    font-family: 'Nunito';
    src: url('nunito-regular-webfont.woff2') format('woff2');
    font-weight: 400;
    font-style: normal;

}




@font-face {
    font-family: 'Nunito';
    src: url('nunito-semibold-webfont.woff2') format('woff2');
    font-weight: 600;
    font-style: normal;

}

<|start_filename|>src/Utilities.js<|end_filename|>
export function getFormattedSize(size) {
    let absSize = Math.abs(size);
    let outSize = size;
    if (absSize < 1000) {
        // bytes
        outSize = size + " bytes"
    } else if (absSize < 1000 * 1000) {
        // KB
        outSize = (size / 1000).toFixed(1) + "KB"
    } else if (absSize < 1000 * 1000 * 1000) {
        // MB
        outSize = (size / (1000 * 1000)).toFixed(1) + "MB"
    } else if (absSize < 1000 * 1000 * 1000 * 1000) {
        // MB
        outSize = (size / (1000 * 1000 * 1000)).toFixed(1) + "GB"
    }
    return outSize
}

export function getFormattedPercent(start, end) {
    if (start == 0 || end == 0)
        return "No change"
    if (start == end) {
        return "No change"
    }
    else if (start < end) {
        return ((100 + ((end / start) * 100)).toFixed(0) + "% larger")
    } else {
        return (Math.floor(100 - ((end / start) * 100)) + "% smaller")
    }

}

<|start_filename|>src/electron.js<|end_filename|>
const { app, BrowserWindow, ipcMain, systemPreferences, Menu, MenuItem, Notification, nativeTheme } = require('electron')
const path = require("path")
const fs = require("fs")
const { fork } = require("child_process")
const isDev = require("electron-is-dev");
const os = require("os")
const ua = require('universal-analytics');
const { v4: uuid } = require('uuid');
let mainWindow
let splashWindow
let settingsWindow
const debug = console
let analytics = false
let analyticsQueue = false
let analyticsInterval = false

// App version
const crusheeVersion = require('../package.json').version

// Handle multiple instances
const appLocked = app.requestSingleInstanceLock()

if (!appLocked) {
  app.quit()
} else {
  app.on('second-instance', (event, commandLine, workingDirectory) => {
    console.log(commandLine, workingDirectory)
    if (mainWindow) {
      if (mainWindow.isMinimized()) mainWindow.restore()
      mainWindow.focus()
      tryOpenFiles(files)
    }
  })
}

// Handle opening files
app.on('will-finish-launching', () => {
  app.on('open-file', (event, path) => {
    event.preventDefault()
    tryOpenFiles(path)
  });
});

function tryOpenFiles(files) {
  if(files) {
    if(typeof files == "object") {
      for(let file in files) {
        fs.lstat(file, (err, stats) => {
          if(!err) {
            mainWindow.webContents.send("open-file", file)
          }
        })
      }
    } else if(typeof files == "string") {
      fs.lstat(files, (err, stats) => {
        if(!err) {
          mainWindow.webContents.send("open-file", files)
        }
      })
    }
  }
}

const settingsPath = path.join(app.getPath("userData"), `\\settings${(isDev ? "-dev" : "")}.json`)
let settings = {
  theme: 'system',
  threads: 'auto',
  updates: true,
  analytics: true,
  autoThreads: (os.cpus().length > 3 ? Math.floor(os.cpus().length / 3) + 1 : 1),
  uuid: uuid()
}

function readSettings() {
  try {
    if (fs.existsSync(settingsPath)) {
      settings = Object.assign(settings, JSON.parse(fs.readFileSync(settingsPath)))
    } else {
      fs.writeFileSync(settingsPath, JSON.stringify({}))
    }
  } catch (e) {
    debug.error("Couldn't load settings", e)
  }
  processSettings()
}

function writeSettings(newSettings = {}, processAfter = true) {
  settings = Object.assign(settings, newSettings)

  // Save new settings
  try {
    fs.writeFile(settingsPath, JSON.stringify(settings), (e) => { if (e) debug.error(e) })
  } catch (e) {
    debug.error("Couldn't save settings.", settingsPath, e)
  }
  if (processAfter) processSettings();
}

ipcMain.on('send-settings', (event, settings) => {
  writeSettings(settings)
})

ipcMain.on('request-settings', () => {
  debug.log("Settings requested")
  processSettings()
})

function processSettings() {
  if (settings.theme) {
    nativeTheme.themeSource = settings.theme
    if(mainWindow) mainWindow.setVibrancy(getVibrancy())
  }
  if(settings.analytics) {
    if(!analytics) {
      console.log("\x1b[34mAnalytics:\x1b[0m starting with UUID " + settings.uuid)
      analytics = ua('UA-137776048-2', settings.uuid)
      analytics.set("ds", "app")
      analytics.pageview(app.name + "/" + "v" + app.getVersion() + "/" + os.platform()).send()
      analytics.event({
        ec: "Session Information",
        ea: "Version",
        el: "v" + app.getVersion()
      }).event({
        ec: "Session Information",
        ea: "App Name",
        el: app.name
      }).event({
        ec: "Session Information",
        ea: "Platform",
        el: os.platform()
      }).event({
        ec: "Session Information",
        ea: "OS Version",
        el: os.release()
      }).event({
        ec: "Session Information",
        ea: "CPU Cores",
        el: os.cpus().length
      }).event({
        ec: "Session Information",
        ea: "CPU Model",
        el: os.cpus()[0].model
      }).send()

      analyticsInterval = setInterval(() => {
        if(analytics && analyticsQueue) {
          console.log("\x1b[34mAnalytics:\x1b[0m Sending analytics")
          analyticsQueue.send()
          analyticsQueue = false
        }
      }, 30 * 1000)
    }
  } else {
    analytics = false
    if(analyticsInterval) {
      clearInterval(analyticsInterval)
    }
  }
  sendToAllWindows('settings-updated', settings)
}

function sendToAllWindows(eventName, data) {
  if (mainWindow) {
    mainWindow.webContents.send(eventName, data)
  }
  if (settingsWindow) {
    settingsWindow.webContents.send(eventName, data)
  }
}

function getVibrancy() {
  if(os.platform() === "darwin") {
    const release = os.release().split('.')[0]
    if(release >= 18) {
      return 'fullscreen-ui'
    }
  }
  return (nativeTheme.shouldUseDarkColors ? "dark" : "light")
}

function createSplash() {
  splashWindow = new BrowserWindow({
    width: 320,
    height: 320,
    icon: __dirname + '/assets/icon-shadow.ico',
    title: 'Crushee',
    show: false,
    frame: false,
    transparent: true,
    backgroundColor: "#00000000",
    webPreferences: {
      navigateOnDragDrop: false,
      contextIsolation: false
    },
    titleBarStyle: "default"
  })
  splashWindow.setIgnoreMouseEvents(true)
  splashWindow.loadURL(
    isDev
      ? "http://localhost:3001/splash.html"
      : `file://${path.join(__dirname, "../build/splash.html")}`
  );
  splashWindow.webContents.on('did-finish-load', function () {
    if (splashWindow) splashWindow.show();
  });
}

function createSettingsWindow() {
  if (settingsWindow != null) {
    settingsWindow.focus()
    return false;
  }
  settingsWindow = new BrowserWindow({
    width: 400,
    height: 640,
    icon: __dirname + '/assets/icon-shadow.ico',
    title: 'Crushee Settings',
    show: false,
    frame: (os.platform() === "darwin" ? true : false),
    resizable: false,
    backgroundColor: '#00FFFFFF',
    titleBarStyle: 'hidden',
    vibrancy: getVibrancy(),
    webPreferences: {
      navigateOnDragDrop: false,
      webSecurity: false,
      scrollBounce: true,
      enableRemoteModule: true,
      preload: path.resolve(__dirname, 'settings.preload.js'),
      contextIsolation: false
    }
  })
  settingsWindow.loadURL(
    isDev
      ? "http://localhost:3001/settings.html"
      : `file://${path.join(__dirname, "../build/settings.html")}`
  );

  settingsWindow.webContents.on('did-finish-load', () => {
    settingsWindow.show()
    //settingsWindow.webContents.openDevTools()
  })

  settingsWindow.on('closed', function () {
    settingsWindow = null
  })

}

function createWindow() {

  // Create the browser window.
  mainWindow = new BrowserWindow({
    width: 1070,
    height: 720,
    minWidth: 700,
    minHeight: 600,
    icon: __dirname + '/assets/icon-shadow.ico',
    title: 'Crushee',
    show: false,
    frame: (os.platform() === "darwin" ? true : false),
    backgroundColor: '#00FFFFFF',
    titleBarStyle: 'hidden',
    vibrancy: getVibrancy(),
    webPreferences: {
      navigateOnDragDrop: false,
      webSecurity: false,
      scrollBounce: true,
      //experimentalFeatures: true,
      enableRemoteModule: true,
      preload: path.resolve(__dirname, 'app.preload.js'),
      contextIsolation: false
    }
  })
  mainWindow.loadURL(
    isDev
      ? "http://localhost:3001/index.html"
      : `file://${path.join(__dirname, "../build/index.html")}`
  );

  mainWindow.webContents.on('did-finish-load', function () {
    processSettings()

    let blurEnabled = true
    try {
      if (os.platform() === "win32") {
        // Disabling blur on Windows for the foreseeable future. Too buggy. 
        blurEnabled = false
        /*
        const SWCA = require('windows-swca')
        const release = os.release().split('.')[2]
        if (release < 18363) {
          blurEnabled = false
        } else if (release < 19041) {
          SWCA.SetWindowCompositionAttribute(mainWindow.getNativeWindowHandle(), SWCA.ACCENT_STATE.ACCENT_ENABLE_BLURBEHIND, 0xDDDDDDBB)
        } else {
          SWCA.SetWindowCompositionAttribute(mainWindow.getNativeWindowHandle(), SWCA.ACCENT_STATE.ACCENT_ENABLE_BLURBEHIND, 0xDDDDDDBB)

          // Maybe one day this will work without lagging when transparency is on in Windows...
          //SWCA.SetWindowCompositionAttribute(mainWindow.getNativeWindowHandle(), SWCA.ACCENT_STATE.ACCENT_ENABLE_ACRYLICBLURBEHIND, 0xDDDDDD22)
        }
        */
      }
    } catch (e) {
      console.log("Could not enable blur.", e)
      blurEnabled = false
    }


    setTimeout(() => {
      if (splashWindow) {
        splashWindow.close();
        splashWindow = null;
      }
      mainWindow.webContents.send('blurEnabled', blurEnabled)
      mainWindow.show();
    }, 500)

    mainWindow.webContents.send('version', `v${crusheeVersion}`)

  });

  // and load the index.html of the app.


  mainWindow.setMenuBarVisibility(false)
  mainWindow.setMenuBarVisibility(true)
  const menu = Menu.buildFromTemplate(menuTemplate);
  Menu.setApplicationMenu(menu);

  // Open the DevTools.
  // mainWindow.webContents.openDevTools()

  // Emitted when the window is closed.
  mainWindow.on('closed', function () {
    mainWindow = null
    app.quit()
  })
}


// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', tryStart)

// Quit when all windows are closed.
app.on('window-all-closed', function () {
  // On macOS it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
  if (process.platform !== 'darwin') {
    app.quit()
  }
  app.quit()
})

app.on('activate', function () {
  // On macOS it's common to re-create a window in the app when the
  // dock icon is clicked and there are no other windows open.
  if (mainWindow === null) {
    try {
      tryStart()
    } catch (e) {
      // Not sure what to do. It didn't work.
      app.quit()
    }
  }
})







// Start up app
function tryStart() {
  createSplash()
  readSettings()
  createWindow()
  tryingConnection = false
}

const menus = {


  File: [
    {
      label: 'Add file(s)',
      accelerator: 'Shift+CmdOrCtrl+A',
      click: () => {
        mainWindow.webContents.send('shortcut', {
          shortcut: "add-files"
        })
      }
    }, {
      label: 'Add folder(s)',
      click: () => {
        mainWindow.webContents.send('shortcut', {
          shortcut: "add-folders"
        })
      }
    }, {
      type: 'separator'
    }, {
      label: 'Save all files',
      accelerator: 'CmdOrCtrl+S',
      click: () => {
        console.log('Save All Clicked');
        mainWindow.webContents.send('shortcut', {
          shortcut: "save-all"
        })
      }
    }, {
      label: 'Save to folder',
      accelerator: 'CmdOrCtrl+F',
      click: () => {
        console.log('Save To Folder Clicked');
        mainWindow.webContents.send('shortcut', {
          shortcut: "save-to-folder"
        })
      }
    }, {
      type: 'separator'
    }, {
      label: 'Preferences',
      click: () => {
        createSettingsWindow()
      }
    }, {
      type: 'separator'
    }, {
      label: 'Quit',
      accelerator: 'CmdOrCtrl+Q',
      click: () => {
        app.quit();
      }
    }
  ],


  Edit: [
    {
      label: 'Recrush all files',
      accelerator: 'CmdOrCtrl+R',
      click: () => {
        mainWindow.webContents.send('shortcut', {
          shortcut: "recrush"
        })
      }
    },
    {
      label: 'Clear all files',
      accelerator: 'CmdOrCtrl+D',
      click: () => {
        mainWindow.webContents.send('shortcut', {
          shortcut: "clear-all"
        })
      }
    }, {
      type: 'separator'
    }, {
      label: 'Remove unoptimized files (<0%)',
      accelerator: 'CmdOrCtrl+L',
      click: () => {
        console.log('Remove Larger Clicked');
        mainWindow.webContents.send('shortcut', {
          shortcut: "remove-large-files"
        })
      }
    }
  ],


  Help: [
    {
      label: 'Developer tools',
      accelerator: 'CmdOrCtrl+I',
      click: () => {
        console.log('Inspector Clicked');
        mainWindow.webContents.openDevTools()
        mainWindow.webContents.send('shortcut', {
          shortcut: "inspector"
        })
      }
    },
    {
      type: 'separator'
    },
    {
      label: `About Crushee v${crusheeVersion}`,
      click: () => {
        console.log('Version Clicked');
        require("electron").shell.openExternal("https://crushee.app/?app")
      }
    }
  ],

  RightClickFile: [
    {
      label: `Crush file`,
      click: () => {
        mainWindow.webContents.send('shortcut', {
          shortcut: "right-click-crush"
        })
      }
    }, {
      label: `Compare changes`,
      click: () => {
        mainWindow.webContents.send('shortcut', {
          shortcut: "right-click-compare"
        })
      }
    }, {
      label: `Show original`,
      click: () => {
        mainWindow.webContents.send('shortcut', {
          shortcut: "right-click-show-original"
        })
      }
    }, {
      type: 'separator'
    }, {
      label: `Save`,
      click: () => {
        mainWindow.webContents.send('shortcut', {
          shortcut: "right-click-save"
        })
      }
    },
    {
      label: `Save as...`,
      click: () => {
        mainWindow.webContents.send('shortcut', {
          shortcut: "right-click-save-as"
        })
      }
    },
    {
      type: 'separator'
    },
    {
      label: `Remove`,
      click: () => {
        mainWindow.webContents.send('shortcut', {
          shortcut: "right-click-remove"
        })
      }
    }
  ]

}


const menuTemplate = [
  {
    label: 'File',
    submenu: menus.File
  },
  {
    label: 'Edit',
    submenu: menus.Edit
  },
  {
    label: 'Help',
    submenu: menus.Help
  }
];


ipcMain.on('popupMenu', (event, args) => {

  const popupMenu = menus[args.menu]

  if (args.disable) {
    switch (args.menu) {
      case "RightClickFile":
        popupMenu[1].enabled = false
        popupMenu[4].enabled = false
        popupMenu[5].enabled = false
        break;
      case "File":
        popupMenu[3].enabled = false
        popupMenu[4].enabled = false
        break;
      case "Edit":
        break;
    }
  } else {
    switch (args.menu) {
      case "RightClickFile":
        popupMenu[1].enabled = true
        popupMenu[4].enabled = true
        popupMenu[5].enabled = true
        break;
      case "File":
        popupMenu[3].enabled = true
        popupMenu[4].enabled = true
        break;
      case "Edit":
        break;
    }
  }

  const menu2 = Menu.buildFromTemplate(popupMenu);
  menu2.popup({
    x: (args.x ? Math.round(args.x) : null),
    y: (args.y ? Math.round(args.y) : null)
  })
})


ipcMain.on('crushEvent', (event, settings) => {
  if(analytics) {
    if(!analyticsQueue) {
      analyticsQueue = analytics
    }
    for(let category in settings) {
      for(let key in settings[category]) {
        analyticsQueue.event({
          ec: "Crush Settings",
          ea: category,
          el: key,
          ev: settings[category][key]
        })
      }
    }
  }
})
ipcMain.on('saveEvent', (event, data) => {
  if(analytics) {
    if(!analyticsQueue) {
      analyticsQueue = analytics
    }
    for(let category in data) {
      for(let key in data[category]) {
        analyticsQueue.event({
          ec: "Save Results",
          ea: category,
          el: key,
          ev: data[category][key]
        })
      }
    }
  }
})
ipcMain.on('event', (event, data) => {
  if(analytics) {
    if(!analyticsQueue) {
      analyticsQueue = analytics
    }
    analyticsQueue.event(data)
  }
})

ipcMain.on('open-url', (event, url) => {
  require("electron").shell.openExternal(url)
})

// Fix http:// + file:// mixing
const { protocol } = require("electron");

app.whenReady().then(() => {
  protocol.registerFileProtocol('file', (request, callback) => {
    const pathname = decodeURI(request.url.replace('file:///', ''));
    const parts = pathname.split('?')
    callback(parts[0]);
  });
});

// Kill everything if this process fails
app.on("error", () => { app.quit() })

<|start_filename|>src/app.main.js<|end_filename|>
import React from "react";
import ReactDOM from "react-dom";
import App from "./components/App";

const files = []
window.files = files
window.stats = {}

window.eventState = {
    crushing: 0,
    saving: 0,
    analyzing: 0
}

window.changeEventState = (event, state, sendUpdate = true) => {
    window.eventState[event] = state
    if(sendUpdate) window.sendUpdate()
}

ReactDOM.render(<App />, document.getElementById("crushee"));

document.body.addEventListener("dragover", function (event) {
    // prevent default to allow drop

    event.dataTransfer.dropEffect = 'copy';
    event.preventDefault();
}, false);

document.body.addEventListener("drop", (event) => {
    event.preventDefault();
    let files = []
    for (let file of event.dataTransfer.files) {
        files.push(file.path)
    }
    window.addFiles(files)
}, false);


window.changeQualityLevel(2)
setTimeout(() => {
    if(window.appInfo.isAppX === false && window.GlobalSettings.App.updates) {
        fetch("https://api.github.com/repos/xanderfrangos/crushee/releases").then((response) => {
            response.json().then((json) => {
                if(json[0].tag_name != window.appInfo.version) {
                    window.appInfo.newVersion = json[0].tag_name
                    window.sendUpdate()
                }
            })
        });
    }
}, 1000)

window.thisWindow.on('resize', () => {
    window.sendUpdate()
})

<|start_filename|>src/optimizer/file-thread.js<|end_filename|>
const sharp = require("sharp")

const slash = require('slash');

const imagemin = require('imagemin');
const imageminGIFSicle = require('@xanderfrangos/imagemin-gifsicle');
const imageminJPEGRecompress = require('imagemin-jpeg-recompress');
const imageminMozJPEG = require('imagemin-mozjpeg');
const imageminSVGO = require('imagemin-svgo');
const imageminAdvpng = require('imagemin-advpng');
const imageminPngquant = require('imagemin-pngquant');

const fs = require("fs")
const path = require("path")

const consoleLog = console.log
//console.log = () => {}

// Default image settings
let imgSettings = {
    resize: {
        width: "",
        height: "",
        crop: false,
        mode: "exact"
    },
    jpg: {
        quality: 95,
        subsampling: 1,
        useOriginal: false
    },
    png: {
        qualityMin: 50,
        qualityMax: 99
    },
    webp: {
        quality: 80,
        alphaQuality: 100,
        make: false,
        only: false,
        lossless: false,
        nearLossless: false,
        smartSubsample: false,
        reductionEffort: 4
    },
    gif: {
        colors: 128,
        quality: 95
    },
    heif: {
        quality: 95,
        compression: "hevc",
        lossless: false,
        speed: 5,
        chromaSubsampling: "4:2:0"
    },
    avif: {
        quality: 95,
        lossless: false,
        speed: 5,
        chromaSubsampling: "4:2:0"
    }
}



parseBool = (value) => {
    const str = String(value)
    return (str.toLowerCase() === "true" || str.toLowerCase() === "yes" || str.toLowerCase() === "1" || value === 1 || value === true ? true : false)
}


/*
//
//
//   IMAGE PROCESSING
//
//
*/

var debug = false;
//
//   Manipulate images
//
async function processImage(file, outFolder, options = {}, quality = 100) {

    // Merge and santize options
    let settings = Object.assign(imgSettings, options)
    settings.resize.width = (parseInt(settings.resize.width) > 5400 ? 5400 : settings.resize.width)
    settings.resize.height = (parseInt(settings.resize.height) > 5400 ? 5400 : settings.resize.height)

    let ext = path.extname(file).toLowerCase()

    try {
        let image = sharp(file, {
            density: 300
        })

        let metadata = await image.metadata()

        // Fix AVIF being detected as HEIF
        if(metadata.format === "heif" && ext === ".avif") {
            metadata.format = "avif"
        }

        sendGenericMessage("Detected format:" + metadata.format)
        sendGenericMessage("Requested output:" + (settings.app.convert === "none" || !settings.app.convert ? metadata.format : settings.app.convert))

        if (parseBool(settings.jpg.make) === false && parseBool(settings.webp.make) === false) {
            switch (metadata.format) {
                case "jpeg":
                    ext = ".jpg"
                    break
                default:
                    ext = "." + metadata.format
                    break;
            }
        }
        
        // Fix orientation from EXIF data, if available
        image.rotate()

        let resized = false
        if (settings.resize.width || settings.resize.height) {
            resized = true

            let width = (settings.resize.width && parseInt(settings.resize.width) > 0 ? parseInt(settings.resize.width) : null)
            let height = (settings.resize.height && parseInt(settings.resize.height) > 0 ? parseInt(settings.resize.height) : null)

            // Check if percentages
            if(settings.resize.width && parseInt(settings.resize.width) != 0 && settings.resize.width.substr(-1) === "%") {
                width = Math.ceil(metadata.width * (parseInt(settings.resize.width) / 100))
            }
            if(settings.resize.height && parseInt(settings.resize.height) != 0 && settings.resize.height.substr(-1) === "%") {
                height = Math.ceil(metadata.height * (parseInt(settings.resize.height) / 100))
            }

            if(settings.resize.mode === "min") {
                let shouldResize = false
                if(width && metadata.width < width) {
                    shouldResize = true
                }

                if(height && metadata.height < height) {
                    shouldResize = true
                }

                if(shouldResize)
                image.resize(
                    width,
                    height,
                    { fit: "outside" }
                )
            } else if(settings.resize.mode === "max") {
                let shouldResize = false
                if(width && metadata.width > width) {
                    shouldResize = true
                }

                if(height && metadata.height > height) {
                    shouldResize = true
                }

                if(shouldResize)
                image.resize(
                    width,
                    height,
                    { fit: "inside" }
                )
            } else {
                image.resize(
                    width,
                    height,
                    { fit: (width && height ? "cover" : "inside") }
                )
            }

            

        if(resized) {
            const { info } = await image.toBuffer({ resolveWithObject: true });
            consoleLog(`Original size: ${metadata.width}x${metadata.height}`)
            consoleLog(`Output size: ${info.width}x${info.height}`)
        }

        }

        if (settings.app.convert === "jpg" || settings.app.convert === "png" || settings.app.convert === "webp" || settings.app.convert === "heif" || settings.app.convert === "avif") {
            ext = "." + settings.app.convert
        }

        if (ext === ".jpg" || ext === ".jpeg") {
            ext = ".jpg" // Force .jpg because it's objectively correct
            image.flatten({
                background: { r: 255, g: 255, b: 255 }
            })

            image.jpeg({
                quality: 100,
                chromaSubsampling: '4:4:4'
            })

        } else if (ext === ".png") {
            image.png()
        } else if (ext === ".gif" || ext === ".svg") {
            return file
        } else if (ext === ".webp") {
            let webp = {
                quality: parseInt(settings.webp.quality),
                alphaQuality: parseInt(settings.webp.quality),
                reductionEffort: 3
            }

            // Lossless or near-lossless
            if(parseInt(settings.webp.quality) === 100) {
                webp.lossless = true
            } else if(parseInt(settings.webp.quality) > 90) {
                webp.nearLossless = true
            }

            // High quality subsampling
            if(parseInt(settings.webp.quality) > 80) {
                webp.smartSubsample = true
            }

            // Lower quality alpha
            if(parseInt(settings.webp.quality) < 95) {
                webp.alphaQuality = parseInt(webp.quality * 0.8)
            }

            image.webp(webp)
        } else if (ext === ".avif") {
            let avif = {
                quality: parseInt(settings.avif.quality),
                speed: 5
            }
            if(settings.avif.subsampling) {
                switch(parseInt(settings.avif.subsampling)) {
                    case 1: avif.chromaSubsampling = '4:4:4'; break;
                    case 2: avif.chromaSubsampling = '4:2:2'; break;
                    case 3: avif.chromaSubsampling = '4:2:0'; break;
                }
            }
            if(avif.quality === 100) {
                avif.lossless = true
            }
            image.avif(avif)
        } else {
            return false
        }
        
        const outPath = outFolder + "manipulated" + ext
        let promise = image.toFile(outPath)
            .then(() => {
                return outPath
            }).catch((e) => {
                return false
            })
        return promise
    } catch (e) {
        consoleLog(e)
    }

}

//
//    Compress images after manipulation
//
async function compressFile(file, outFolder, options = {}, jpgEngineName = "jpegRecompress") {
    const settings = Object.assign(imgSettings, options)

    const inExt = path.extname(file).toLowerCase()

    // Abort unsupported file types
    // ...mostly webp
    if (!(inExt == ".png" || inExt == ".jpg" || inExt == ".jpeg" || inExt == ".svg" || inExt == ".gif"))
        return file

    let jpgPlugin = imageminMozJPEG({ quality: settings.jpg.quality })

    if (jpgEngineName == "mozjpeg") {

        let sample = ["1x1"]
        if (settings.jpg.subsampling == 2) {
            sample = ["2x2"]
        } else if (settings.jpg.subsampling == 3) {
            sample = ["3x3"]
        }

        sendGenericMessage("Subsampling at " + sample)

        jpgPlugin = imageminMozJPEG({
            quality: 31 + parseInt(settings.jpg.quality / 1.5),
            sample,
            progressive: true,
            tune: "hvs-psnr"
        })

    } else {
        const quality = parseInt(settings.jpg.quality)
        const min = parseInt(quality * Math.pow(0.95, (100 - quality) * 1.5))
        const method = (parseInt(settings.jpg.subsampling) <= 1 ? "ssim" : "smallfry")
        if (parseInt(settings.jpg.subsampling) <= 1) {
            jpgPlugin = imageminJPEGRecompress({
                quality: "high",
                method,
                min,
                subsample: "disable"
            })
        } else {
            jpgPlugin = imageminJPEGRecompress({
                quality: "high",
                method,
                min
            })
        }
    }

    if (settings.png.qualityMin > settings.png.qualityMax)
        settings.png.qualityMax = settings.png.qualityMin;

    return new Promise((resolve, reject) => {
        imagemin([slash(file)], {
            destination: slash(outFolder),
            plugins: [
                jpgPlugin,
                imageminGIFSicle({
                    optimizationLevel: 3,
                    colors: settings.gif.colors,
                    lossy: parseInt(settings.gif.quality - 100) * -2,
                }),
                imageminSVGO(),
                imageminPngquant({
                    quality: [parseInt(settings.png.qualityMin) * 0.01, parseInt(settings.png.qualityMax) * 0.01],
                    strip: true,
                    speed: 3
                }),
                imageminAdvpng({
                    number: 4,
                    iterations: 2
                })
            ]
        }).then((a) => {
            if (a.length === 0) {
                resolve(false)
            } else {
                resolve(a[0].destinationPath)
            }
        }).catch((e) => {
            // Compression didn't go so well
            consoleLog(e)
            resolve(false)
        })
    })
}


//
//   Builds thumbnail previews
//
async function makePreview(file, outFolder) {
    const outPath = path.join(outFolder, "preview.jpg")
    try {
        let image = sharp(file)
        const metadata = await image.metadata()
        let stats = fs.statSync(file)
        image.rotate() // Fix orientation from EXIF data, if available
        image.resize(200, 200, { fit: "cover" })
        image.flatten({
            background: { r: 255, g: 255, b: 255 }
        })
        image.jpeg({
            quality: 75,
            chromaSubsampling: '4:2:0',
            trellisQuantisation: true
        })
        let promise = image.toFile(outPath)
            .then(() => {
                // Preview made!
                return {
                    Format: metadata.format,
                    FileSize: stats["size"],
                    X: metadata.width,
                    Y: metadata.height
                }
            }).catch((e) => {
                // Preview failed to write
                console.log(e)
                return false
            })
        return promise
    } catch (e) {
        // Preview super failed
        return false
    }
}

//
//   Process queued image
//
async function job(uuid, fn, f, o, options = {}, mode = "compress") {

    let original = f
    let uuidDir = o + uuid + "/"

    if (mode === "preview") {
        sendGenericMessage("Making previews...")
        // Make thumbnails
        fs.mkdirSync(slash(uuidDir + "preview/"), { recursive: true })
        try {
            return await makePreview(f, uuidDir + "preview/")
        } catch (e) {
            sendGenericMessage("ERROR: Creating preview failed. " + e)
            process.send({
                type: 'failed',
                uuid,
                threadNum
            })
            return false;
        }
    } else {
        const quality = parseInt(options.jpg.quality)
        // Process with sharp
        sendGenericMessage("Processing...")
        let resized = await processImage(f, uuidDir, options)
        if (!resized) {
            consoleLog("Failed processing! Returning original file :(")
            resized = f;
        }

        // Determine if the original file is OK to use as-is. Must have been a JPEG and hasn't been resized.
        let canUseOriginalImage = (path.extname(resized) == path.extname(f) && path.extname(resized) == ".jpg" && !(options.resize.width || options.resize.height) ? true : false)

        // Use original file if wanted/able.
        if (canUseOriginalImage) {
            //sendGenericMessage("Original JPEG image can be used.")
        } else {

        }

        const results = {}

        if (!(options.resize.width || options.resize.height)) {
            results["original"] = original
        }

        // Use MozJPEG to adjust overall quality
        // We'll use this on JPEGs that have been processed by sharp
        let mozJPEG
        if (path.extname(resized) == ".jpg" && quality < 95) {
            sendGenericMessage("MozJPEG compressing...")
            mozJPEG = compressFile(resized, path.join(uuidDir, "moz"), options, "mozjpeg").then(result => {
                if (result) {
                    results["mozJPEG"] = result
                }
            })
        }

        let mozOriginal
        if (path.extname(resized) == ".jpg" && canUseOriginalImage && quality < 95) {
            sendGenericMessage("MozJPEG compressing original...")
            mozOriginal = compressFile(f, path.join(uuidDir, "mozO"), options, "mozjpeg").then(result => {
                if (result) {
                    results["mozOriginal"] = result
                }
            })
        }

        // Compress processed file
        sendGenericMessage("Compressing...")

        // Use original if possible, else resized
        let compressed = compressFile(resized, path.join(uuidDir, "compressed"), options).then(result => {
            if (result) {
                results["compressed"] = result
            }
        })

        // Compress original, if possible
        let compressedOriginal
        if (canUseOriginalImage) {
            compressedOriginal = compressFile(f, path.join(uuidDir, "compressedOriginal"), options).then(result => {
                if (result) {
                    results["compressedOriginal"] = result
                }
            })
        }


        let mozCompressed
        if (await mozJPEG && quality < 95 && !(canUseOriginalImage && options.jpg.useOriginal === true)) {
            // Compress processed file
            sendGenericMessage("Compressing mozJPEG...")

            // Use original if possible, else resized
            let optionsCopy = Object.assign(options, {})
            optionsCopy.jpg.subsampling = 3
            mozCompressed = compressFile(await mozJPEG, path.join(uuidDir, "mozCompressed"), options).then(result => {
                if (result) {
                    results["mozCompressed"] = result
                }
            })
        }

        mozJPEG = await mozJPEG
        compressed = await compressed
        compressedOriginal = await compressedOriginal
        mozCompressed = await mozCompressed

        const smallest = {
            name: "original",
            size: (canUseOriginalImage ? fs.statSync(f).size : -1)
        }
        consoleLog("===== RESULTS =====")
        for (let result in results) {
            let size = fs.statSync(results[result]).size
            consoleLog(`\x1b[36m${result}:\x1b[0m ${size / 1000}kb`)
            if (result != "original" && size > 0 && (size < smallest.size || smallest.size == -1)) {
                smallest.name = result
                smallest.size = size
            }
        }
        consoleLog("===================")
        consoleLog(`\x1b[36mSmallest size is\x1b[0m ${smallest.name} \x1b[36mat\x1b[0m ${smallest.size / 1000} kb\x1b[36m!\x1b[0m`)
        consoleLog("===================")

        // Collect sizes to send back
        let finalSize = smallest.size

        sendGenericMessage("Preparing final file...")
        // Copy finished file to final location
        if(!fs.existsSync(uuidDir + "crushed/")) {
            fs.mkdirSync(uuidDir + "crushed/")
        }
        
        let finalFile = uuidDir + "crushed/" + path.basename(fn, path.extname(fn)) + "_" + Date.now() + path.extname(resized)

        try {
            if(fs.existsSync(finalFile)) {
                fs.unlinkSync(finalFile)
            }
        } catch(e) {
            consoleLog(`Couldn't unlink ${finalFile}`)
        }
        

        fs.copyFileSync(results[smallest.name], finalFile)



        sendGenericMessage("Writing timestamp...")
        // Write timestamp for cleanup
        fs.writeFileSync(uuidDir + "ts", Date.now())

        sendGenericMessage("Clearing temp files...")

        // Build and send results
        const metadata = await sharp(finalFile).metadata()
        const result = {
            Crushed: path.basename(finalFile),
            BaseName: path.basename(fn, path.extname(fn)) + path.extname(resized),
            Extension: path.extname(path.basename(finalFile)),
            FileSize: finalSize,
            X: metadata.width,
            Y: metadata.height
        }

        sendGenericMessage("Done!")
        // Congrats, job well done!
        return result
    }





}




/*
//
//
//   QUEUE/JOB MANAGEMENT
//
//
*/

let processQueue = []
let processBusy = false
let threadNum = -1
let shouldDie = false
process.on('message', (data) => {
    if (data.type == "job") {
        processQueue.push(data)
        sendGenericMessage(`Recieved job ${data.uuid} | Current queue: ${processQueue.length}`)
        sendQueueUpdate()
        checkCanDoJob()
    } else if (data.type == 'setThreadNum') {
        threadNum = data.result
        sendGenericMessage("Thread ready!")
    } else if (data.type == 'quit') {
        sendGenericMessage("Shutting down...")
        process.exit(0)
    } else if(data.type == 'softKill') {
        shouldDie = true
    }
})

// Send queue updates after events and every so often
setInterval(sendQueueUpdate, 1000)
function sendQueueUpdate() {
    process.send({
        type: 'queueLength',
        result: processQueue.length,
        threadNum: threadNum
    })
}

function sendGenericMessage(message) {
    process.send({
        type: 'generic',
        message: message,
        threadNum: threadNum
    })
}

// Try to run a job, if able
setInterval(checkCanDoJob, 100)
async function checkCanDoJob() {
    if (processBusy == false) {

        if(shouldDie) {
            process.exit(0)
            return false;
        }

        if (processQueue.length === 0) {
            // No jobs, request another

            process.send({
                type: 'jobRequest',
                threadNum: threadNum
            })

        } else {
            processBusy = true
            let data = processQueue[0]
            job(...data.payload).then((result) => {

                // Send response that the job was finished
                process.send({
                    type: 'finished',
                    result: result,
                    uuid: data.uuid,
                    threadNum: threadNum
                })

                // Remove old job and make not busy
                processQueue.splice(0, 1)
                processBusy = false

                sendGenericMessage(`Finished job ${data.uuid} | Current queue: ${processQueue.length}`)

                // Finished job, start another
                checkCanDoJob()

            }).catch((e) => {


                // Something went wrong :(
                // Tell someone
                sendGenericMessage(e)
                process.send(false)
                process.send({
                    type: 'finished',
                    result: false,
                    uuid: processQueue[0].uuid,
                    threadNum: threadNum
                })

                // Remove old, errored-out job
                processQueue.splice(0, 1)
                processBusy = false

                // Try next job
                checkCanDoJob()
            })
        }

    }
}


setInterval(sendStillAliveMessage, 1000)

function sendStillAliveMessage() {
    process.send({
        type: 'alive',
        message: "Thread active",
        threadNum: threadNum
    })
}





var deleteFolderRecursive = function (path) {
    if (fs.existsSync(path)) {
        fs.readdirSync(path).forEach(function (file, index) {
            var curPath = path + "/" + file;
            if (fs.lstatSync(curPath).isDirectory()) { // recurse
                deleteFolderRecursive(curPath);
            } else { // delete file
                fs.unlinkSync(curPath);
            }
        });
        fs.rmdirSync(path);
    }
};

<|start_filename|>src/settings.preload.js<|end_filename|>
const { ipcRenderer: ipc, remote } = require('electron');
let browser = remote.getCurrentWindow()

function sendSettings(newSettings) {
    ipc.send('send-settings', newSettings)
}

function requestSettings() {
    ipc.send('request-settings')
}

function openURL(url) {
    ipc.send('open-url', url)
}

ipc.on('settings-updated', (event, settings) => {
    window.settings = settings
    window.dispatchEvent(new CustomEvent('settingsUpdated', {
        detail: settings
    }))
})

// Request startup data
browser.webContents.once('dom-ready', () => {
    requestSettings()
})

window.sendSettings = sendSettings
window.requestSettings = requestSettings
window.openURL = openURL
window.lastUpdate = Date.now()
window.settings = {}
window.thisWindow = browser

window.version = 'v' + remote.app.getVersion()
window.isAppX = (remote.app.name == "crushee-appx" ? true : false)