Vpn — Electron компонент, основной элемент управления приложением.
Electron компонент — под этим термином я подразумеваю как раз ту организацию Electron кода, о которой я говорил в 1 части.
Структура папок.
index.js — Файл, в котором создается Electron компонент.
Cодержимое файла index.js.
index.html — HTML страница окна.
Cодержимое файла index.html.
script.js — Вся анимация окна.
По мимо script.js для анимации также используется TweenMax.js.
Cодержимое файла script.js.
style.css — Стили для страницы.
В моем приложении очень важно, чтобы в стилях глобально былзапрещен, overflow: hidden; без этого свойства бывает баг с окном при изменении положения «панели быстрого запуска», а именно появляется scroll.
Cодержимое файла style.css.
Application
Использование в приложении: /app/Vpn.
API
Интерфейс компонента Vpn. Уверен, после изучения следующего куска кода станет очевидно удобство того подхода, о котором я говорил в 1 части. После того как компонент приобрел такую оболочку в виде класса со своими методами, работать с ним стало гораздо проще. Если вы все же не убедились в этом сейчас, то обязательно убедитесь позже.
Test
Версия для тестирования: /app_test/Vpn.
Возможные статусы и цвета для Vpn.setStatus.
6 часть — Notify компонент
Electron компонент — под этим термином я подразумеваю как раз ту организацию Electron кода, о которой я говорил в 1 части.
Структура папок.
vpn
¦
¦ index.js
¦
+---client // все что относится к клиенту
¦ index.html
¦ paper-plane.svg
¦ script.js
¦ style.css
¦ TweenMax.min.js
¦
L---icon
blue.ico
orange.ico
red.ico
yellow.ico
index.js — Файл, в котором создается Electron компонент.
Cодержимое файла index.js.
Код
const { BrowserWindow, Tray, ipcMain } = require('electron')
module.exports = class VPN {
constructor() {
// Создаем окно
this.root = new BrowserWindow({
title: `JS.VPN-Client`,
frame: false, // Убираем рамку
transparent: true, // Устанавливаем прозрачность
alwaysOnTop: true, // Устанавливаем поверх всех окон
resizable: false, // Запрещаем масштабирование
center: true,
show: false, // Запрещаем показывать окно после загрузки
acceptFirstMouse: true, // Разрешает взаимодействие с окном до фокуса на окне
width: 116,
height: 116,
fullscreenable: false
})
// Загружаем страницу
this.root.loadURL(`${__dirname}/client/index.html`)
this.tray = {}
// Хранит статус переподключения
this.isReconnect = false
// Хранит таймеры переподключений
this.reconnect_stack = []
// Изначальный статус подключения
this.status = false
// Эти методы хранят коллбеки подключения и отключения
this.cb_connect = () => {}
this.cb_disconnect = () => {}
}
ready() {
// Ожидаем пока окно полностью инициализируется
return new Promise(res => {
this.root.once('ready-to-show', res)
})
}
showTray() {
// Инициализирует Tray (см. доку Electron) и устанавливает Title
this.tray = new Tray(`${__dirname}/icon/red.ico`)
this.tray.setToolTip('Отключен')
}
show() {
// Показывает окно
this.root.show()
}
hide() {
// Скрывает окно
this.root.hide()
}
isVisible() {
// Возвращает boolean - в зависимости от того показано окно или скрыто
return this.root.isVisible()
}
onTrayClick(cb) {
// Устанавливаем обработчик на Tray
this.tray.on('click', cb)
}
onTrayRightClick(cb) {
// Устанавливаем обработчик на Tray
this.tray.on('right-click', cb)
}
center() {
// Устанавливаем окно по центру
this.root.center()
}
onDisconnect(cb) {
// Передаем оригинальную функцию
this.cb_disconnect = cb
// Обработчик сигнала VPN_DISCONNECT (отключение от VPN)
ipcMain.on('VPN_DISCONNECT', e => {
cb()
e.returnValue = 'ok'
})
}
onConnect(cb) {
// Передаем оригинальную функцию
this.cb_connect = cb
// Обработчик сигнала VPN_CONNECT (подключение от VPN)
ipcMain.on('VPN_CONNECT', (e, data) => {
cb()
e.returnValue = 'ok'
})
}
connect() {
// Имитация подключения
this.cb_connect()
}
disconnect() {
// Имитация отключения
this.cb_disconnect()
}
onContext(cb) {
// Обработчик сигнала VPN_CONTEXT (вызов контекстного меню)
ipcMain.on('VPN_CONTEXT', (e, data) => {
cb(data)
e.returnValue = 'ok'
})
}
stopReconnect() {
// Останавливаем и удаляем все таймеры переподключений
this.reconnect_stack.map(i => clearTimeout(i))
this.reconnect_stack = []
}
reconnect(cb, time = 15000) {
// Останавливаем все предыдущие таймеры переподключения
this.stopReconnect()
// Добавляем в массив таймер
this.reconnect_stack.push(
// Таймер переподключения
setTimeout(() => {
// Переподключение началось
this.isReconnect = true
cb(() => {
this.disconnect()
this.connect()
})
// Переподключение закончилось
this.isReconnect = false
}, time)
)
}
setStatus(animation, color) {
let statusText = 'Отключен'
if (color == 'blue') {
statusText = 'Подключен'
}
if (color == 'yellow') {
statusText = 'Подключение'
}
if (color == 'orange') {
statusText = 'Установка компонентов'
}
// Устанавливает Title для Tray
this.tray.setToolTip(statusText)
// Изменяем иконку
this.tray.setImage(`${__dirname}/icon/${color}.ico`)
this.status = color == 'red' ? false : true;
// Отсылаем сигнал VPN_STATUS и статус подключения в окно
this.root.webContents.send('VPN_STATUS', JSON.stringify({
animation, color
}))
}
}
index.html — HTML страница окна.
Cодержимое файла index.html.
Код
<!DOCTYPE html>
<html lang='ru'>
<head>
<meta charset='UTF-8'>
</head>
<body>
<link rel='stylesheet' type='text/css' href='style.css'>
<script src='TweenMax.min.js'></script>
<script src='script.js'></script>
<div class='btn-border'>
<div class='btn-body'>
<div class='but-plane'></div>
</div>
</div>
<script>
const { ipcRenderer, remote } = require('electron')
, plane = document.getElementsByClassName('but-plane')[0]
, btnBorder = document.getElementsByClassName('btn-border')[0]
, btnBody = document.getElementsByClassName('btn-body')[0]
// Получаем контроль над окном
const win = remote.getCurrentWindow()
// Если окно имело уже какую-то позицию в локальном хранилище то устанавливаем ее
localStorage.x && win.setPosition(
parseInt(localStorage.x),
parseInt(localStorage.y)
)
// Обновляем позицию в локальном хранилище
setInterval(() => {
const [x, y] = win.getPosition()
localStorage.setItem('x', x)
localStorage.setItem('y', y)
}, 3)
// Обработчик вызова контекстного меню
document.body.addEventListener('contextmenu', e => {
// Сохраняем позицию курсора в локальное хранилище
localStorage.setItem('context_x', e.pageX)
localStorage.setItem('context_y', e.pageY)
// Сигнал компоненту на вызов контекста
ipcRenderer.sendSync('VPN_CONTEXT')
e.preventDefault()
});
// Подключаемся и отключаемся от VPN
let start = false
plane.addEventListener('click', () => {
start = !start
if (start) {
// Сигнал компоненту на подключение от VPN
ipcRenderer.sendSync('VPN_CONNECT')
} else {
// Сигнал компоненту на отключение от VPN
ipcRenderer.sendSync('VPN_DISCONNECT')
}
})
// Тут начинается анимация
const statVPN = new VPNstatus(plane, btnBody, btnBorder)
// Отключен
statVPN.color('red')
statVPN.reject()
// Обработчик сигналов с статусами от компонента
ipcRenderer.on('VPN_STATUS', (e, data) => {
const { animation, color } = JSON.parse(data)
statVPN.color(color)
statVPN[animation]()
})
</script>
</body>
</html>
script.js — Вся анимация окна.
По мимо script.js для анимации также используется TweenMax.js.
Cодержимое файла script.js.
Код
class VPNstatus {
constructor (plane, btnBody, btnBorder) {
this.plane = plane
this.btnBody = btnBody
this.btnBorder = btnBorder
// скорость Анимации
this.speedAnimation = 0.9
this.colors = {
red: {
boxShadow: '1px 1px 5px 1px rgba(255, 24, 37, 0.4)',
borderBackground: '#e2333d',
background: '#f06069'
},
blue: {
background: '#60a2f0',
borderBackground: '#3286e3',
boxShadow: '2px 2px 5px 1px rgba(40, 138, 255, 0.4)'
},
yellow: {
background: '#f5cc5b',
borderBackground: '#f1c131',
boxShadow: '2px 2px 5px 1px rgba(255, 197, 37, 0.4)'
},
orange: {
background: '#f09c60',
borderBackground: '#e37a32',
boxShadow: '2px 2px 5px 1px rgba(255, 127, 35, 0.4)'
}
}
this.step = {
start: {
left: '-40px',
top: '73px'
},
center: {
top: '19px',
left: '13px'
},
end: {
top: '-45px',
left: '71px'
}
}
this.status = 0
}
color (value) {
btnBody.style.background = this.colors[value].background
btnBorder.style.background = this.colors[value].borderBackground
btnBorder.style.boxShadow = this.colors[value].boxShadow
}
waiting () {
statVPN.center_to_end(() => {
this.start_to_end(() => {
if (this.status == 1) {
TweenMax.killAll()
this.plane.style.left = this.step.start.left;
this.plane.style.top = this.step.start.top;
statVPN.start_to_center()
this.status = 2
}
})
})
}
resolve () {
this.status = 1
}
reject () {
this.status = 2
statVPN.center_to_end(() => {
statVPN.start_to_center()
})
}
start_to_end (callback) {
this.plane.style.left = this.step.start.left;
this.plane.style.top = this.step.start.top;
TweenMax.to(this.plane, this.speedAnimation * 2, {
top: this.step.end.top,
left: this.step.end.left,
repeat: -1,
ease: Elastic.ease,
onRepeat: callback ? callback : () => {}
})
}
center_to_end (callback) {
this.plane.style.left = this.step.center.left;
this.plane.style.top = this.step.center.top;
TweenMax.to(this.plane, this.speedAnimation, {
top: this.step.end.top,
left: this.step.end.left,
ease: Elastic.ease,
onComplete: callback ? callback : () => {}
})
}
start_to_center (callback) {
this.plane.style.left = this.step.start.left;
this.plane.style.top = this.step.start.top;
TweenMax.to(this.plane, this.speedAnimation, {
top: this.step.center.top,
left: this.step.center.left,
ease: Elastic.ease,
onComplete: callback ? callback : () => {}
})
}
}
style.css — Стили для страницы.
В моем приложении очень важно, чтобы в стилях глобально был
Cодержимое файла style.css.
Код
* {
padding: 0;
margin: 0;
overflow: hidden;
-webkit-app-region: drag;
}
body {
width: 100%;
height: 100vh;
background: rgba(0, 0, 0, 0);
}
.btn-border {
position: absolute;
left: 1px;
top: 1px;
width: 111px;
height: 111px;
background: #e2333d;
border-radius: 100%;
box-shadow: 1px 1px 5px 1px rgba(255, 24, 37, 0.4);
display: flex;
justify-content: center;
align-items: center;
}
.btn-body {
border-radius: 100%;
overflow: hidden;
width: 85px;
height: 85px;
background: #f06069;
cursor: pointer;
-webkit-app-region: no-drag;
}
.but-plane {
position: relative;
background: url('paper-plane.svg');
width: 60%;
height: 60%;
top: 71px;
left: -38px;
cursor: pointer;
-webkit-app-region: no-drag;
}
Application
Использование в приложении: /app/Vpn.
API
Интерфейс компонента Vpn. Уверен, после изучения следующего куска кода станет очевидно удобство того подхода, о котором я говорил в 1 части. После того как компонент приобрел такую оболочку в виде класса со своими методами, работать с ним стало гораздо проще. Если вы все же не убедились в этом сейчас, то обязательно убедитесь позже.
const { app } = require('electron')
, VPN = require('./../../app/components/vpn')
app.on('ready', async() => {
const Vpn = new VPN()
// Только после того как окно инициализируется программа продолжит исполнятся
await Vpn.ready()
// Показываем окно
Vpn.show()
// Показываем Tray
Vpn.showTray()
// Обработчик подключения
Vpn.onConnect(() => {
console.log('Connect')
// Передаем в окно статус
Vpn.setStatus('waiting', 'yellow')
setTimeout(() => {
console.log('Connected!')
// Передаем в окно статус
Vpn.setStatus('resolve', 'blue')
}, 4000)
})
// Обработчик отключения
Vpn.onDisconnect(() => {
console.log('Disconnect')
// Передаем в окно статус
Vpn.setStatus('reject', 'red')
})
Vpn.onContext(() => {
console.log('Context menu')
})
// Переподключатель если в течении 5 сек после объявления
// не будет вызвана функция Vpn.stopReconnect()
// произойдет переподключение
Vpn.reconnect(next => {
console.log('Reconnect')
next()
}, 5000)
// Предотвращает или останавливает переподключение
//stopReconnect()
// Обработчик клика на Tray
Vpn.onTrayClick(() => {
// Имитация подключения и отключения
if (!Vpn.status) {
Vpn.connect()
} else {
Vpn.disconnect()
}
})
// Обработчик клика правой кнопкой мыши на Tray
Vpn.onTrayRightClick(() => {
if (Vpn.isVisible()) {
Vpn.hide()
} else {
Vpn.show()
}
})
})
Test
Версия для тестирования: /app_test/Vpn.
Возможные статусы и цвета для Vpn.setStatus.
Цвета | Анимации | Описание |
---|---|---|
red | reject | Отключение |
blue | resolve | Подключение |
yellow | waiting | Ожидание |
orange |
6 часть — Notify компонент
Навигация
1 часть — Вводная
2 часть — Разработка
3 часть — OpenVPN компонент
4 часть — Configs компонент
5 часть — Vpn компонент
6 часть — Notify компонент
7 часть — Context компонент
8 часть — Setting компонент
9 часть — Callback компонент
10 часть — Объединение всех компонентов
11 часть — Сборка приложения под Windows
2 часть — Разработка
3 часть — OpenVPN компонент
4 часть — Configs компонент
5 часть — Vpn компонент
6 часть — Notify компонент
7 часть — Context компонент
8 часть — Setting компонент
9 часть — Callback компонент
10 часть — Объединение всех компонентов
11 часть — Сборка приложения под Windows
hardex
EventEmitter уже вроде как изобрели