Create a Desktop App With JavaScript & Electron
Примечание
1 ошибка
Если вылазят такие ошибки, то это означает, что у нас используется неправильный импорт
Неправильный пример:
preload.js
const contextBridge = require("electron");
Правильный пример: нужно закидывать модули через { }
preload.js
const { contextBridge, ipcRenderer, remote, shell } = require("electron");
2 ошибка
В
renderer.js
может автоматически произвестись импорт функции, которую мы подключили черезpreload
. Нужно следить, чтобы в рендерере не было импортов модулей из электрона и ноды
Генерация проекта Electron-приложения
Через npm-пакет electron-app создадим шаблон electron-приложения
Дальше установим плагин для уведомлений на ОС и сам изменитель разрешений изобаржений
И командой npx electronmon .
мы будем запускать нормальное обновление приложения при каждом изменении кода (оно будет перезагружаться, если будем менять JS-код и обновлять страницу браузера, если будем корректировать фронт)
npm init electron-app@latest my-electron-img-resizer
npm i toastify-js resize-img --save-dev
npx electronmon .
Пишем начальную работу окон (main process)
Самая начальная часть - основная логика нашего приложения, которая представляет из себя main
процесс. Он воплощает собой бэкэнд нашего сайта, который создаёт сами окна приложения, реализует определённый функционал по обработке, созданию и измению файлов на нашем компьютере.
- Импорты. Тут можно импортировать как сами модули электрона, так и модули ноды.
- Отслеживание сред. Тут мы проверяем, в какой среде разработки мы находимся и на какой операционной системе. В зависимости от результатов, приложение будет себя вести по разному.
- Через инстанс
BrowserWindow
создаётся новое окно приложения, которое представляет из себя окно браузера - Далее было создано окно “о приложении” через ещё один инстанс окна браузера
- Далее идёт блок кода, который срабатывает при запуске приложения
- Тут мы запускаем рендер нашего основного окна
- Далее мы присваиваем ему новое меню
- Новое меню создаётся через формирование своего собственного шаблона этой панели
- Далее мы создадим условие, которое не даст запустить повторно новый инстанс приложения
- И последним блоком условия мы закрываем само приложение, если закрыты все его окна (актуально на маке)
// 1
// основные библиотеки электрона
const { app, BrowserWindow, Menu } = require("electron");
// библиотеки ноды
// модуль работы с путями на компьютере
const path = require("path");
// 2
// проверяем, находимся ли мы в режиме разработчика
const isDev = process.env.NODE_ENV !== "production";
// проверяет, является ли система маком
const isMac = process.platform !== "darwin";
// 8
// Шаблон нашего собственного меню приложения (находится в самом верху)
const menu = [
// так же мы можем вставить название меню по условию ...(условие ? [одно меню] : [пустой либо другое меню])
// если мы пользуемся маком, то выведем меню с названием нашего приложения
...(isMac
? [
{
label: app.name,
submenu: [
{ label: "About", click: () => createAboutWindow() },
],
},
]
: []),
// мы можем создать наше кастомное меню
{
// название группы меню
label: "CunsomFile",
// подменю
submenu: [
{
// название подменю
label: "Quit",
// функция этого подменю
click: () => app.quit(),
// быстрые сочетания клавиш для срабатывания функции
accelerator: "Ctrl+W",
},
],
},
// а можем добавить встроенные функции меню
{
role: "fileMenu",
},
// если сидим не на маке, то создадим пункт Help
...(!isMac
? [
{
label: "Help",
submenu: [
{ label: "About", click: () => createAboutWindow() },
],
},
]
: []),
];
if (require("electron-squirrel-startup")) {
app.quit();
}
// 3
// функция для создания нового основного окна браузера
const createMainWindow = () => {
// создаём окно
const mainWindow = new BrowserWindow({
// если мы находимся в режиме разработчика, то делаем окно шире (для девтулза)
width: isDev ? 1100 : 600,
height: 600,
webPreferences: {
// подключаем прелоад - пишем актуальный путь до него от этого файла
preload: path.join(__dirname, "../preload/preload.js"),
},
});
// загружаем браузерную страницу - основное окно браузера
mainWindow.loadFile(path.join(__dirname, "../renderer/index.html"));
// если мы находимся в режиме разработки, то открываем инструменты разработчика
if (isDev) {
mainWindow.webContents.openDevTools();
}
};
// 4
// функция для создания окна о приложении
const createAboutWindow = () => {
// создаём окно
const aboutWindow = new BrowserWindow({
width: 300,
height: 300,
autoHideMenuBar: true,
webPreferences: {
// подключаем прелоад - пишем актуальный путь до него от этого файла
preload: path.join(__dirname, "../preload/preload.js"),
},
});
// загружаем браузерную страницу - основное окно браузера
aboutWindow.loadFile(path.join(__dirname, "../renderer/about.html"));
};
// 5
// когда приложение будет готово, запустим рендер окна
app.whenReady().then(() => {
// 6
// запускаем функцию создания окна
createMainWindow();
// 7
// Подставляем в приложение наше собственное меню
const mainMenu = new Menu.buildFromTemplate(menu);
Menu.setApplicationMenu(mainMenu);
// 9
// если приложение активно, то не даём запустить новый инстанс приложения
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
// 10
// если мы сидим на маке, то отключаем инстанс приложения при закрытии окна
app.on("window-all-closed", () => {
if (isMac) {
app.quit();
}
});
Как итог наше приложение запускается в режиме разработчика шире и с девтулзом
Мы можем кликнуть по окну “О приложении”
И это окно вызовется
И так же закрыть приложение через наше кастомное меню
Пишем фронт (renderer процесс)
Непосредственно renderer
процесс представляет из себя самый обычный JS файл, который мы можем подцепить к нашей странице и начать в нём писать фронт-энд логику
- Мы получаем элементы с нашей страницы
- Мы создаём функцию загрузки изображения
- Мы вызываем ивенты
// 1 Получаем формы
const form = document.querySelector("#img-form"),
img = document.querySelector("#img"),
outputPath = document.querySelector("#output-path"),
filename = document.querySelector("#filename"),
heightInput = document.querySelector("#height"),
widthInput = document.querySelector("#width");
// 2 Функции
const loadImage = (e) => {
// получаем файл из инпута, который находится в файлах ивента
const file = e.target.files[0];
// проверяем, является ли импортнутый файл изображением
if (!isFileImage(file)) {
// не продолжаем выполнять код, так как приложение не получило изображение
return;
}
// Получаем оригинальные размеры
// создаём инстанс изображеия по нашему объекту
const image = new Image();
image.src = URL.createObjectURL(file);
// и далее нужно из полученного изображения выцепить размеры
// тут используем ~обязательно~ анонимную функцию
image.onload = function () {
widthInput.value = this.width;
heightInput.value = this.height;
};
// отображаем интерфейс изменения размеров изображения
form.style.display = "block";
// выведем название нашего изображения
filename.innerText = file.name;
};
// Функция, которая проверяет, является ли файл изображением
const isFileImage = (file) => {
// типы, которые мы поддерживаем
const acceptedImageTypes = ["image/png", "image/gif", "image/jpeg"];
// возвращает true, если содержимое переменной file.type = одному из значений массива с нужными типами
return file && acceptedImageTypes.includes(file["type"]);
};
// 3 Ивенты
img.addEventListener("change", loadImage);
В итоге, сейчас мы можем закинуть фотографию в приложение и получить его имя и размеры высоты и ширины
Пишем мост между браузерным АПИ и ОС (preload процесс)
preload
позволяет нам пользоваться функциями ноды в renderer
процессе. А конкретно, нам нужно произвести все нужные импорты, которые нужно было бы осуществить в renderer
процесс
Первым делом нужно подключить preload
файл в наш main.js
main.js
// функция для создания нового основного окна браузера
const createMainWindow = () => {
// создаём окно
const mainWindow = new BrowserWindow({
// если мы находимся в режиме разработчика, то делаем окно шире (для девтулза)
width: isDev ? 1100 : 600,
height: 600,
webPreferences: {
// подключаем прелоад - пишем актуальный путь до него от этого файла
preload: path.join(__dirname, "../preload/preload.js"),
},
});
/// ... код
Дальше в самом preload
нужно произвести сами ипорты нужных модулей и экспортировать их через contextBridge.exposeInMainWorld()
, в который первым аргументом передадим имя объекта, который хранит функции, а вторым аргументом сам объект с функциями, которые нужно перенести.
preload.js
const os = require("os");
const path = require("path");
const { contextBridge } = require("electron");
// тут будем пользоваться функцией получения домашнего пути компьютера
contextBridge.exposeInMainWorld("os", {
homedir: () => os.homedir(),
});
// тут дублируем работу функции path.join(передаём аргументы в порядке обычной работы этой функции)
contextBridge.exposeInMainWorld("path", {
join: (...args) => path.join(...args),
});
И уже тут мы можем непосредственно пользоваться теми функциями, которые мы передали из preload
renderer.js
const loadImage = (e) => {
const file = e.target.files[0];
if (!isFileImage(file)) {
return;
}
const image = new Image();
image.src = URL.createObjectURL(file);
image.onload = function () {
widthInput.value = this.width;
heightInput.value = this.height;
};
form.style.display = "block";
filename.innerText = file.name;
//! Тут уже будет использоваться функция из preload
// получаем конечный путь до сгенерированного файла
outputPath.innerText = path.join(os.homedir(), "imageresizer");
};
toastify-js
Модуль toastify-js
позволяет нам выводить уведомления пользователю об ошибке или успешном выполнении операции
Все сторонние модули в renderer
процесс нужно подключать через preload
. Поэтому для начала передадим сюда функцию создания уведомления.
const os = require("os");
const path = require("path");
const Toastify = require("toastify-js"); // сам модуль сообщения
const { contextBridge } = require("electron");
contextBridge.exposeInMainWorld("os", {
homedir: () => os.homedir(),
});
contextBridge.exposeInMainWorld("path", {
join: (...args) => path.join(...args),
});
//! тут непосредственно передаём функцию вывода сообщения из модуля Toastify
contextBridge.exposeInMainWorld("Toastify", {
toast: (options) => Toastify(options).showToast(),
});
Далее нам нужно реализовать сам вызов данной функции в целях оповещения пользователя
renderer.js
const loadImage = (e) => {
const file = e.target.files[0];
if (!isFileImage(file)) {
//! Если загрузилось не изображение, то мы выведем сообщение об ошибке результате
alertError("Please, select the image");
return;
}
const image = new Image();
image.src = URL.createObjectURL(file);
image.onload = function () {
widthInput.value = this.width;
heightInput.value = this.height;
};
form.style.display = "block";
filename.innerText = file.name;
outputPath.innerText = path.join(os.homedir(), "imageresizer");
//! Если изображение удачно загрузилось, то мы можем вывести сообщение о положительном результате
alertSuccess("Image has been success loaded!");
};
//! Функция вывода сообщения об ошибке
const alertError = (message) => {
Toastify.toast({
text: message,
duration: 5000,
close: false,
style: {
background: "red",
color: "white",
textAlign: "center",
},
});
};
//! Функция вывода сообщения об успешной загрузке
const alertSuccess = (message) => {
Toastify.toast({
text: message,
duration: 5000,
close: false,
style: {
background: "green",
color: "white",
textAlign: "center",
},
});
};
При попытке загрузить любой файл помимо фото, у нас выходит ошибка
Когда загружаем фотографию, выходит положительное уведомление
IPC Renderer (Send) + IPC Main (Recieve)
Модуль IPC внутри электрона обеспечивает создание канала общения и передачи уведомлений между main
и renderer
процессами
Для начала, нам нужно передать в renderer
процесс саму функцию ipcRenderer
, чтобы ей воспользоваться
preload.js
// импортируем ipcRenderer
const { contextBridge, ipcRenderer, remote, shell } = require("electron");
// передаём в рендерер-процесс функцию ipcRenderer
contextBridge.exposeInMainWorld("ipcRenderer", {
send: (channel, data) => ipcRenderer.send(channel, data),
on: (channel, func) =>
ipcRenderer.on(channel, (event, ...args) => func(...args)),
});
Сейчас нам нужно реализовать Form Submit Handler (форму отправки), которая будет осуществлять отправку изображения на бэк. В ней мы отправляем на бэк данные о пути до изображения, ширине изображения и высоте используя функцию ipcRenderer()
(отправка изображения) по каналу "image:resize"
renderer.js
// Функция отправки изображения в мэин процесс
const sendImage = (e) => {
e.preventDefault();
// введённая высота и ширина в поля
const width = widthInput.value;
const height = heightInput.value;
// получаем путь до расположения изображения
const imgPath = img.files[0].path;
// если было закинуто не изображение, то выведем ошибку
if (!img.files[0]) {
alertError("Please, load an image");
return;
}
// если попытаемся указать ширину или высоту изображения меньше 1px, то получим ошибку
if (width == "" || height == "") {
alertError("Image is not corrected");
return;
}
// ! отправляем через ipcRenderer опции изображения
ipcRenderer.send("image:resize", {
imgPath,
width,
height,
});
};
// осуществляем отправку изображения на бэк через кнопку формы
form.addEventListener("submit", sendImage);
И в main
процессе принимаем по каналу "image:resize"
полученное изображение из нашего renderer
процесса
main.js
const { app, BrowserWindow, Menu, ipcMain } = require("electron");
// получаем изображение с рендерер-процесса
ipcMain.on("image:resize", (e, options) => {
console.log(options);
});
В итоге, мы видим в консоли опции изображения, которые мы передали в бэк
Resize Image
Изменять размер изображения мы можем с помощью модуля Node.JS. Последовательность такая:
- Подключаем модуль
resize-img
- Получаем уведомление из
renderer
процесса через модульipcMain
и в нём же триггерим функциюresizeImage()
- Функция
resizeImage()
генерирует изображение по заданным опциям, создаёт папку для изображений и туда сохраняет готовое изображение
main.js
// основные библиотеки электрона
// Menu - позволяет создать кастомное меню
// ipcMain - позволяет открыть канал отправки сообщений в рендерер-процесс
// shell - позволяет работать с проводником
const { app, BrowserWindow, Menu, ipcMain, shell } = require("electron");
// библиотеки ноды
// модуль работы с путями на компьютере
const path = require("path");
// работает с ОС
const os = require("os");
// позволяет создавать файлы, папки и работать с файловой системой
const fs = require("fs");
// Это сам модуль для изменения разрешения изображения
const resizeImg = require("resize-img");
// получаем изображение с рендерер-процесса
ipcMain.on("image:resize", (e, options) => {
// укажем путь, куда должно быть сгенерировано фото
options.dest = path.join(os.homedir(), "imageresizer");
// производим изменение размера
resizeImage(options);
});
// функция изменения размера изображения
const resizeImage = async ({ imgPath, height, width, dest }) => {
try {
// если всё нормально то реализуем изменение изображения
const newPath = await resizeImg(fs.readFileSync(imgPath), {
// добавляем ширину и высоту, переводя их в числовые значения через оператор "+"
width: +width,
height: +height,
});
// создаст имя файла
const filename = path.basename(imgPath);
// создаём папку назначения, если она не существует
if (!fs.existsSync(dest)) {
fs.mkdirSync(dest);
}
// и уже создаём файл в папке, которую создали выше
fs.writeFileSync(path.join(dest, filename), newPath);
// и отправляем уведомление обратно в редерер-процесс о том, что изображение создано
mainWindow.webContents.send("image:done");
// открываем папку с изображением
shell.openPath(dest);
} catch (error) {
// и при ошибке будет выводиться ошибка
console.log(error);
}
};
Catch Message In Renderer
Тут мы отправляем из функции resizeImage
уведомление (просто создав канал image:done
) в renderer
процесс
main.js
// функция изменения размера изображения
const resizeImage = async ({ imgPath, height, width, dest }) => {
try {
/// ... код
// и отправляем уведомление обратно в редерер-процесс о том, что изображение создано
mainWindow.webContents.send("image:done");
/// ... код
} catch (error) {
// и при ошибке будет выводиться ошибка
console.log(error);
}
};
В renderer
процессе создаём ipcRenderer
, который сработает при получении сигнала от канала image:done
из main
процесса
renderer.js
// Функция отправки изображения в мэин процесс
const sendImage = (e) => {
/// ... код
};
// Ловим событие image:done с готовым изображением
ipcRenderer.on("image:done", () => {
alertSuccess(`Image is rendered to ${widthInput.value} x ${heightInput.value}!`);
});
Make mainWindow Global
Это очень важная часть, так как функция изменения изображения требует глобального доступа к основному окну (а иначе мы не сможем обратиться к нему для отправки уведомления)
main.js
// ! Делаем главное окно глобальным, чтобы воспользоваться им в функции изменения размера изображения
let mainWindow;
const createMainWindow = () => {
mainWindow = new BrowserWindow({
width: isDev ? 1100 : 600,
height: 600,
webPreferences: {
contextIsolation: true,
nodeIntegration: true,
enableRemoteModule: true,
// подключаем прелоад - пишем актуальный путь до него от этого файла
preload: path.join(__dirname, "../preload/preload.js"),
},
});
mainWindow.loadFile(path.join(__dirname, "../renderer/index.html"));
if (isDev) {
mainWindow.webContents.openDevTools();
}
};
app.whenReady().then(() => {
createMainWindow();
const mainMenu = new Menu.buildFromTemplate(menu);
Menu.setApplicationMenu(mainMenu);
// ! уничтожает mainWindow при закрытии, так как самостоятельно он не уничтожится
mainWindow.on("closed", () => {
mainWindow = null;
});
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
Test Production Mode
Тут можно поменять режим разработки (изначально он development
)
main.js
// тут мы можем поменять режим разработки
process.env.NODE_ENV = "production";
// проверяем, находимся ли мы в режиме разработчика
const isDev = process.env.NODE_ENV !== "production";
// проверяет, является ли система маком
const isMac = process.platform !== "darwin";
Упаковка приложения
Эта команда собирает приложение и его установщик
npm run make
Результат!
Мы закидываем изображение, меняем размер, нажимаем resize
И автоматически открывается папка со сгенерированным изображением
Код и структура папок
main.js
// основные библиотеки электрона
// Menu - позволяет создать кастомное меню
// ipcMain - позволяет открыть канал отправки сообщений в рендерер-процесс
// shell - позволяет работать с проводником
const { app, BrowserWindow, Menu, ipcMain, shell } = require("electron");
// библиотеки ноды
// модуль работы с путями на компьютере
const path = require("path");
// работает с ОС
const os = require("os");
// позволяет создавать файлы, папки и работать с файловой системой
const fs = require("fs");
// Это сам модуль для изменения разрешения изображения
const resizeImg = require("resize-img");
// тут мы можем поменять режим разработки
process.env.NODE_ENV = "production";
// проверяем, находимся ли мы в режиме разработчика
const isDev = process.env.NODE_ENV !== "production";
// проверяет, является ли система маком
const isMac = process.platform !== "darwin";
// Шаблон нашего собственного меню приложения (находится в самом верху)
const menu = [
// так же мы можем вставить название меню по условию ...(условие ? [одно меню] : [пустой либо другое меню])
// если мы пользуемся маком, то выведем меню с названием нашего приложения
...(isMac
? [
{
label: app.name,
submenu: [
{ label: "About", click: () => createAboutWindow() },
],
},
]
: []),
// мы можем создать наше кастомное меню
{
// название группы меню
label: "CunsomFile",
// подменю
submenu: [
{
// название подменю
label: "Quit",
// функция этого подменю
click: () => app.quit(),
// быстрые сочетания клавиш для срабатывания функции
accelerator: "Ctrl+W",
},
],
},
// а можем добавить встроенные функции меню
{
role: "fileMenu",
},
// если сидим не на маке, то создадим пункт Help
...(!isMac
? [
{
label: "Help",
submenu: [
{ label: "About", click: () => createAboutWindow() },
],
},
]
: []),
];
if (require("electron-squirrel-startup")) {
app.quit();
}
// ! Делаем главное окно глобальным, чтобы воспользоваться им в функции изменения размера изображения
let mainWindow;
// функция для создания нового основного окна браузера
const createMainWindow = () => {
// создаём окно
mainWindow = new BrowserWindow({
// если мы находимся в режиме разработчика, то делаем окно шире (для девтулза)
width: isDev ? 1100 : 600,
height: 600,
webPreferences: {
contextIsolation: true,
nodeIntegration: true,
enableRemoteModule: true,
// подключаем прелоад - пишем актуальный путь до него от этого файла
preload: path.join(__dirname, "../preload/preload.js"),
},
});
// загружаем браузерную страницу - основное окно браузера
mainWindow.loadFile(path.join(__dirname, "../renderer/index.html"));
// если мы находимся в режиме разработки, то открываем инструменты разработчика
if (isDev) {
mainWindow.webContents.openDevTools();
}
};
// функция для создания окна о приложении
const createAboutWindow = () => {
// создаём окно
const aboutWindow = new BrowserWindow({
width: 300,
height: 300,
autoHideMenuBar: true,
});
// загружаем браузерную страницу - основное окно браузера
aboutWindow.loadFile(path.join(__dirname, "../renderer/about.html"));
};
// когда приложение будет готово, запустим рендер окна
app.whenReady().then(() => {
// запускаем функцию создания окна
createMainWindow();
// Подставляем в приложение наше собственное меню
const mainMenu = new Menu.buildFromTemplate(menu);
Menu.setApplicationMenu(mainMenu);
// уничтожает mainWindow при закрытии, так как самостоятельно он не уничтожится
mainWindow.on("closed", () => {
mainWindow = null;
});
// если приложение активно, то не даём запустить новый инстанс приложения
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
// получаем изображение с рендерер-процесса
ipcMain.on("image:resize", (e, options) => {
// укажем путь, куда должно быть сгенерировано фото
options.dest = path.join(os.homedir(), "imageresizer");
// производим изменение размера
resizeImage(options);
});
// функция изменения размера изображения
const resizeImage = async ({ imgPath, height, width, dest }) => {
try {
// если всё нормально то реализуем изменение изображения
const newPath = await resizeImg(fs.readFileSync(imgPath), {
// добавляем ширину и высоту, переводя их в числовые значения через оператор "+"
width: +width,
height: +height,
});
// создаст имя файла
const filename = path.basename(imgPath);
// создаём папку назначения, если она не существует
if (!fs.existsSync(dest)) {
fs.mkdirSync(dest);
}
// и уже создаём файл в папке, которую создали выше
fs.writeFileSync(path.join(dest, filename), newPath);
// и отправляем уведомление обратно в редерер-процесс о том, что изображение создано
mainWindow.webContents.send("image:done");
// открываем папку с изображением
shell.openPath(dest);
} catch (error) {
// и при ошибке будет выводиться ошибка
console.log(error);
}
};
// если мы сидим на маке, то отключаем инстанс приложения при закрытии окна
app.on("window-all-closed", () => {
if (isMac) {
app.quit();
}
});
preload.js
const os = require("os");
const path = require("path");
const Toastify = require("toastify-js");
const { contextBridge, ipcRenderer, remote, shell } = require("electron");
// тут будем пользоваться функцией получения домашнего пути компьютера
contextBridge.exposeInMainWorld("os", {
homedir: () => os.homedir(),
});
// тут дублируем работу функции path.join(передаём аргументы в порядке обычной работы этой функции)
contextBridge.exposeInMainWorld("path", {
join: (...args) => path.join(...args),
});
// тут непосредственно передаём функцию вывода сообщения из модуля Toastify
contextBridge.exposeInMainWorld("Toastify", {
toast: (options) => Toastify(options).showToast(),
});
// передаём в рендерер-процесс функцию ipcRenderer
contextBridge.exposeInMainWorld("ipcRenderer", {
send: (channel, data) => ipcRenderer.send(channel, data),
on: (channel, func) =>
ipcRenderer.on(channel, (event, ...args) => func(...args)),
});
renderer.js
// 1 Получаем формы
const form = document.querySelector("#img-form"),
img = document.querySelector("#img"),
outputPath = document.querySelector("#output-path"),
filename = document.querySelector("#filename"),
heightInput = document.querySelector("#height"),
widthInput = document.querySelector("#width");
// 2 Функции
const loadImage = (e) => {
// получаем файл из инпута, который находится в файлах ивента
const file = e.target.files[0];
// проверяем, является ли импортнутый файл изображением
if (!isFileImage(file)) {
//! Если загрузилось не изображение, то мы выведем сообщение об ошибке результате
alertError("Please, select the image");
// не продолжаем выполнять код, так как приложение не получило изображение
return;
}
// Получаем оригинальные размеры
// создаём инстанс изображеия по нашему объекту
const image = new Image();
image.src = URL.createObjectURL(file);
// и далее нужно из полученного изображения выцепить размеры
// тут используем ~обязательно~ анонимную функцию
image.onload = function () {
widthInput.value = this.width;
heightInput.value = this.height;
};
// отображаем интерфейс изменения размеров изображения
form.style.display = "block";
// выведем название нашего изображения
filename.innerText = file.name;
// получаем конечный путь до сгенерированного файла
outputPath.innerText = path.join(os.homedir(), "imageresizer");
//! Если изображение удачно загрузилось, то мы можем вывести сообщение о положительном результате
alertSuccess("Image has been success created");
};
// Функция отправки изображения в мэин процесс
const sendImage = (e) => {
e.preventDefault();
// введённая высота и ширина в поля
const width = widthInput.value;
const height = heightInput.value;
// получаем путь до расположения изображения
const imgPath = img.files[0].path;
// если было закинуто не изображение, то выведем ошибку
if (!img.files[0]) {
alertError("Please, load an image");
return;
}
// если попытаемся указать ширину или высоту изображения меньше 1px, то получим ошибку
if (width == "" || height == "") {
alertError("Image is not corrected");
return;
}
// отправляем через ipcRenderer
ipcRenderer.send("image:resize", {
imgPath,
width,
height,
});
};
// Ловим событие image:done с готовым изображением
ipcRenderer.on("image:done", () => {
alertSuccess(`Image is rendered to ${widthInput.value} x ${heightInput.value}!`);
});
// Функция, которая проверяет, является ли файл изображением
const isFileImage = (file) => {
// типы, которые мы поддерживаем
const acceptedImageTypes = ["image/png", "image/gif", "image/jpeg"];
// возвращает true, если содержимое переменной file.type = одному из значений массива с нужными типами
return file && acceptedImageTypes.includes(file["type"]);
};
//! Функция вывода сообщения об ошибке
const alertError = (message) => {
Toastify.toast({
text: message,
duration: 5000,
close: false,
style: {
background: "red",
color: "white",
textAlign: "center",
},
});
};
//! Функция вывода сообщения об успешной загрузке
const alertSuccess = (message) => {
Toastify.toast({
text: message,
duration: 5000,
close: false,
style: {
background: "green",
color: "white",
textAlign: "center",
},
});
};
// 3 Ивенты
// осуществляем загрузку изображения
img.addEventListener("change", loadImage);
// осуществляем отправку изображения на бэк
form.addEventListener("submit", sendImage);
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Нам обязательно нужно вставить модуль безопасности на эту страницу, чтобы не выскакивала ошибка в электроне -->
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline'" />
<!-- Дальше уже идут подключения шрифтов, стилей и скрипта -->
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="css/style.css" />
<script src="js/renderer.js" defer></script>
<title>ImageResizer</title>
</head>
<body class="bg-teal-700">
<div class="max-w-xl m-auto h-screen flex flex-col align-center justify-center">
<div class="flex flex-col w-full items-center justify-center bg-grey-lighter">
<label
class="w-64 flex flex-col items-center px-4 py-7 bg-white text-gray-500 rounded-lg shadow-lg tracking-wide uppercase border border-blue cursor-pointer hover:text-teal-800">
<img src="./images/logo.svg" width="32" />
<span class="mt-2 leading-normal">Select an image to resize</span>
<input id="img" type="file" class="hidden" />
</label>
</div>
<!-- Form -->
<form id="img-form" class="hidden">
<div class="mt-6">
<label
class="mt-1 block text-white text-center w-80 m-auto py-3 shadow-sm border-gray-300 rounded-md">Width</label>
<input type="number" name="width" id="width"
class="mt-1 block w-80 m-auto p-3 shadow-sm border-gray-300 rounded-md" placeholder="Width" />
</div>
<div class="mt-4">
<label
class="mt-1 block text-white text-center w-80 m-auto py-3 shadow-sm border-gray-300 rounded-md">Height</label>
<input type="number" name="height" id="height"
class="mt-1 block w-80 m-auto p-3 shadow-sm border-gray-300 rounded-md" placeholder="Height" />
</div>
<!-- Button -->
<div class="mt-6">
<button type="submit"
class="w-80 m-auto flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-teal-500 hover:bg-teal-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Resize
</button>
</form>
<p class="text-white text-lg text-center font-mono mt-6"><strong>File: </strong><span id="filename"></span></p>
<p class="text-white text-lg text-center font-mono mt-2"><strong>Output: </strong><span id="output-path"></span></p>
</div>
</body>
</html>
about.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- И вставляем этот модуль безопасности и на эту страничку -->
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline'" />
<link rel="stylesheet" href="css/style.css" />
<title>About ImageShrink</title>
</head>
<body class="bg-teal-700">
<div class="max-w-xl m-auto h-screen flex flex-col align-center justify-center text-center">
<img src="./images/logo.svg" alt="ImageResizer" width="32" class="mx-auto mb-5" />
<h2 class="text-xl text-teal-100 text-center">FileResizer App</h2>
<p class="text-xl text-teal-300 mt-2">Version 1.0.0</p>
<p class="text-xl text-teal-300 mt-2">MIT License</p>
</div>
</body>
</html>
Структура папок и файлов