P.S. Каждая часть — это часть, сама по себе смысла не имеет, чтобы обзавестись необходимым контекстом и не испытывать когнитивный диссонанс от отсутствия так необходимых блоков текста начните читать с 1 части

SettingElectron компонент, элемент настройки приложения.

Структура папок.

context
¦
¦   index.js
¦
L---client // все что относится к клиенту
    ¦   creater-option.js
    ¦   index.html
    ¦   style.css
    ¦
    L---fonts
            font1.woff2
            font2.woff2
            font3.woff2
            font4.woff2
            font5.woff2
            font6.woff2
            font7.woff2



index.js — Файл, в котором создается Electron компонент.

Cодержимое файла index.js.
Код
const { BrowserWindow, ipcMain } = require('electron')

module.exports = class Notification {
    constructor(parent) {
        this.root = new BrowserWindow({
            frame: false, // убираем рамку 
            transparent: true, // устанавливаем прозрачность
            resizable: false, // запрещаем масштабирование
            show: false, // запрещаем показывать окно после загрузки 
            height: 520,
            width: 350,
            center: true,
            parent
        })
        
        // загружаем страницу
        this.root.loadURL(`${__dirname}/client/index.html`)
        
        // Обработчик сигнала SETTING_CLOSE (Скрывает окна) 
        ipcMain.on('SETTING_CLOSE', e => {
            this.root.hide()
            e.returnValue = 'ok'
        })
    }

    onSave(cb) {
        // Обработчик сигнала SETTING_SAVE сохраняет и скрывает окно
        ipcMain.on('SETTING_SAVE', e => {
            cb()
            this.root.hide()
            e.returnValue = 'ok'
        })
    }

    ready() {
        // Ожидаем пока окно полностью инициализируется
        return new Promise(res => {
            this.root.once('ready-to-show', res)
        })
    }

    show() {
        // Показываем окно
        this.root.show()
    }

    hide() {
        // Скрываем окно
        this.root.hide()
    }

    get() {
        // Запрашиваем настройки
        return new Promise(resolve => {
            this.root.webContents.send('SETTING_GET')
            ipcMain.once('SETTING_DATA', (e, data) => {
                resolve(JSON.parse(data))
            })
        })
    }
}



index.html — HTML страница окна.

Cодержимое файла index.html.
Код
<!DOCTYPE html>
<html lang='en'>
<head>
    <meta charset='UTF-8'>
    <link rel='stylesheet' type='text/css' href='style.css'>
</head>
<body>
<div class='body'>
    <div class='settings'>
        <div class='wrapper'>
            <div>Количество сессий (от)</div>
            <select name='NumVpnSessions'>
                <option value='-'>---</option>
            </select>
        </div>
        <div class='wrapper'>
            <div>Ping (до)</div>
            <select name='Ping'>
                <option value='-'>---</option>
            </select>
        </div>
        <div class='wrapper'>
            <div>Всего подключений (от)</div>
            <select name='TotalUsers'>
                <option value='-'>---</option>
            </select>
        </div>
        <div class='wrapper'>
            <div>Страна</div>
            <select name='CountryLong'>
                <option value='-'>---</option>
            </select>
        </div>
        <div class='wrapper'>
            <div>Время работы (от)</div>
            <select name='Uptime'>
                <option value='-'>---</option>
            </select>
        </div>
        <div class='wrapper'>
            <div>Скорость (от)</div>
            <select name='Speed'>
                <option value='-'>---</option>
            </select>
        </div>
        <div class='wrapper'>
            <div>Общий трафик (от)</div>
            <select name='TotalTraffic'>
                <option value='-'>---</option>
            </select>
        </div>
        <div class='wrapper'>
            <div>Качество соединения (от)</div>
            <select name='Score'>
                <option value='-'>---</option>
            </select>
        </div>
        <div class='wrapper'>
            <div>Расположен. увед.</div>
            <select name='PositionNotify'>
                <option value='BR'>Низ право</option>
                <option value='TR'>Верх право</option>
                <option value='BL'>Низ лево</option>
                <option value='TL'>Верх лево</option>
            </select>
        </div>
        <label for='auto-reconnect'>
            <div class='wrapper'>
                <div>Авто-переподключение к VPN</div>
                <input type="checkbox" name='AutoReconnect' id='auto-reconnect' hidden>
                <div class='new_check'></div>
            </div>
        </label>
        <label for='auto-update'>
            <div class='wrapper'>
                <div>Авто-обновление серверов</div>
                <input type="checkbox" name='AutoUpdate' id='auto-update' hidden>
                <div class='new_check'></div>
            </div>
        </label>
        <label for='permutation'>
            <div class='wrapper'>
                <div>Показывать по центру (при запуске)</div>
                <input type="checkbox" name='Permutation' id='permutation' hidden>
                <div class='new_check'></div>
            </div>
        </label>
        <label for='start-hidden'>
            <div class='wrapper'>
                <div>Запускать свернутым</div>
                <input type="checkbox" name='StartHidden' id='start-hidden' hidden>
                <div class='new_check'></div>
            </div>
        </label>
    </div>
    <div class='btn-nav'>
        <div class='btn save'>Сохранить</div>
        <div class='btn close'>Закрыть</div>
    </div>
</div>
<script>
const save = document.getElementsByClassName('save')[0]
    , close = document.getElementsByClassName('close')[0]
    , NumVpnSessions = document.getElementsByName('NumVpnSessions')[0]
    , Ping = document.getElementsByName('Ping')[0]
    , TotalUsers = document.getElementsByName('TotalUsers')[0]
    , CountryLong = document.getElementsByName('CountryLong')[0]
    , Uptime = document.getElementsByName('Uptime')[0]
    , Speed = document.getElementsByName('Speed')[0]
    , TotalTraffic = document.getElementsByName('TotalTraffic')[0]
    , Score = document.getElementsByName('Score')[0]
    , AutoReconnect = document.getElementsByName('AutoReconnect')[0]
    , Permutation = document.getElementsByName('Permutation')[0]
    , AutoUpdate = document.getElementsByName('AutoUpdate')[0]
    , StartHidden = document.getElementsByName('StartHidden')[0]
    , PositionNotify = document.getElementsByName('PositionNotify')[0]
    , { ipcRenderer, shell } = require('electron')


// преобразуем настройки в строку JSON
const getSetting = () => JSON.stringify({
    Ping: Ping.value || '-',
    NumVpnSessions: NumVpnSessions.value || '-',
    TotalUsers: TotalUsers.value || '-',
    CountryLong: CountryLong.value || '-',
    Uptime: Uptime.value || '-',
    Speed: Speed.value || '-',
    TotalTraffic: TotalTraffic.value || '-',
    Score: Score.value || '-',
    AutoReconnect: AutoReconnect.checked || false,
    AutoUpdate: AutoUpdate.checked || false,
    Permutation: Permutation.checked || false,
    StartHidden: StartHidden.checked || false,
    PositionNotify: PositionNotify.value || 'BR'
})


// сохранение настроек 
save.addEventListener('click', () => {
    // сохраняем в настройки в localStorage
    localStorage.setItem('vpn_setting', getSetting())
    // отсылам сигнал SETTING_SAVE в компонент
    ipcRenderer.sendSync('SETTING_SAVE')
})

// закрытие окна
close.addEventListener('click', () => {
    // отсылам сигнал SETTING_CLOSE в компонент
    ipcRenderer.sendSync('SETTING_CLOSE')
})
</script>
<script src='creater-option.js'></script>
<script>

// устанавливаем всем полям значения из настроен
const vpn_setting = JSON.parse(localStorage.vpn_setting || "{}")

Ping.value = vpn_setting.Ping || '-'
NumVpnSessions.value = vpn_setting.NumVpnSessions || '-'
TotalUsers.value = vpn_setting.TotalUsers || '-'
CountryLong.value = vpn_setting.CountryLong || '-'
Uptime.value = vpn_setting.Uptime || '-'
Speed.value = vpn_setting.Speed || '-'
TotalTraffic.value = vpn_setting.TotalTraffic || '-'
Score.value = vpn_setting.Score || '-'
AutoReconnect.checked = vpn_setting.AutoReconnect || false
AutoUpdate.checked = vpn_setting.AutoUpdate || false
Permutation.checked = vpn_setting.Permutation || false
StartHidden.checked = vpn_setting.StartHidden || false
PositionNotify.value = vpn_setting.PositionNotify || 'BR'

// обработчик сигналов от компонента
ipcRenderer.on('SETTING_GET', (e, data) => {
    // отсылам сигнал SETTING_DATA в компонент и с ним настройки
    ipcRenderer.send('SETTING_DATA', getSetting())
})
</script>
</body>
</html>



creater-option.js — Создает option для всех select на странице.

Cодержимое файла creater-option.js.
Код
// Активных сессий
NumVpnSessions.innerHTML += Array(100).fill(1).map((e, i) => {
    if (i < 10) {
        return `<option value='${i+1}'>${i+1}</option>`
    }
    return `<option value='${parseInt(i*1.5)}'>${parseInt(i*1.5)}</option>`
}).join('')

// Пинг
Ping.innerHTML += Array(150).fill(1).map((e, i) => {
    if (i < 10) {
        return `<option value='${i+1}'>${i+1}ms</option>`
    }
    return `<option value='${parseInt(i*1.5)}'>${parseInt(i*1.5)}ms</option>`
}).join('')

// Общее количество сессий
TotalUsers.innerHTML += Array(150).fill(1).map((e, i) => `<option value='${parseInt((i+1)*(i+1))}'>${parseInt((i+1)*(i+1))}</option>`).join('')

// Страна
const Contrys = ['Japan', 'Korea Republic of', 'United States', 'Thailand', 'Germany',
    'New Zealand', 'Argentina', 'Poland', 'United Kingdom', 'Hong Kong',
    'Viet Nam', 'Russian Federation', 'France', 'China', 'Singapore',
    'Trinidad and Tobago', 'Moldova Republic of', 'Canada', 'Romania',
    'Algeria', 'Venezuela', 'Indonesia', 'Brazil', 'Mexico', 'Cyprus',
    'Iceland', 'Bangladesh', 'Morocco', 'Iraq', 'Ukraine', 'Iran',
    'Turkey', 'Angola', 'El Salvador', 'Chile', 'Egypt', 'Spain',
    'Netherlands', 'Colombia'
]

CountryLong.innerHTML += Contrys.map(e => `<option value='${e}'>${e}</option>`).join('')

// Время работы
Uptime.innerHTML += Array(79).fill(1).map((e, i) => {
    if (i < 15) {
        return `<option value='${(i+1)*60*1000}'>${i+1} мин.</option>`
    }

    if (i < 18) {
        return `<option value='${(i-13)*15*60*1000}'>${(i-13)*15} мин.</option>`
    }

    if (i < 40) {
        return `<option value='${(i-16)*60*60*1000}'>${(i-16)} час.</option>`
    }

    if (i < 69) {
        return `<option value='${(i-38)*24*60*60*1000}'>${(i-38)} ден.</option>`
    }

    if (i < 79) {
        return `<option value='${(i-67)*30*24*60*60*1000}'>${(i-67)} мес.</option>`
    }
}).join('')

// Скорость
Speed.innerHTML += Array(100).fill(1).map((e, i) => {
    if (i < 5) {
        return `<option value='${parseInt(1048576*((i+1)/100))}'>${(i+1)/100} Mbit</option>`
    }

    if (i < 9) {
        return `<option value='${parseInt(1048576*((i+1)/10))}'>${(i+1)/10} Mbit</option>`
    }

    if (i < 19) {
        return `<option value='${parseInt(1048576*(i-8))}'>${(i-8)} Mbit</option>`
    }

    return `<option value='${parseInt(1048576*((i-18)*i))}'>${(i-18)*i} Mbit</option>`
}).join('')

// Общий трафик
TotalTraffic.innerHTML += Array(150).fill(1).map((e, i) => {
    if (i < 10) {
        return `<option value='${parseInt(1048576*(i+1))}'>${i+1} Mbit</option>`
    }

    if (i < 20) {
        return `<option value='${parseInt(1048576*((i+1)*10))}'>${(i+1)*10} Mbit</option>`
    }

    if (i < 50) {
        return `<option value='${parseInt(1048576*((i+1)*100))}'>${(i+1)*100} Mbit</option>`
    }

    if (i < 60) {
        return `<option value='${parseInt(1073741824*((i-45)*1))}'>${(i-45)*1} Gbit</option>`
    }

    if (i < 70) {
        return `<option value='${parseInt(1073741824*((i-45)*10))}'>${(i-45)*10} Gbit</option>`
    }

    if (i < 150) {
        return `<option value='${parseInt(1073741824*((i-45)*100))}'>${(i-45)*100} Gbit</option>`
    }
}).join('')

// Качество соединения
Score.innerHTML += Array(100).fill(1).map((e, i) => `<option value='${(i+25)*(i+25)*(i+25)}'>${i+1}%</option>`).reverse().join('')



style.css — Стили для страницы.

Cодержимое файла style.css.
Код
@font-face {
    font-family: 'Source Sans Pro';
    font-style: normal;
    font-weight: 400;
    src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url(fonts/font1.woff2) format('woff2');
    unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}


/* cyrillic */

@font-face {
    font-family: 'Source Sans Pro';
    font-style: normal;
    font-weight: 400;
    src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url(fonts/font2.woff2) format('woff2');
    unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}


/* greek-ext */

@font-face {
    font-family: 'Source Sans Pro';
    font-style: normal;
    font-weight: 400;
    src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url(fonts/font3.woff2) format('woff2');
    unicode-range: U+1F00-1FFF;
}


/* greek */

@font-face {
    font-family: 'Source Sans Pro';
    font-style: normal;
    font-weight: 400;
    src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url(fonts/font4.woff2) format('woff2');
    unicode-range: U+0370-03FF;
}


/* vietnamese */

@font-face {
    font-family: 'Source Sans Pro';
    font-style: normal;
    font-weight: 400;
    src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url(fonts/font5.woff2) format('woff2');
    unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
}


/* latin-ext */

@font-face {
    font-family: 'Source Sans Pro';
    font-style: normal;
    font-weight: 400;
    src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url(fonts/font6.woff2) format('woff2');
    unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}


/* latin */

@font-face {
    font-family: 'Source Sans Pro';
    font-style: normal;
    font-weight: 400;
    src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url(fonts/font7.woff2) format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

* {
    font-family: 'Source Sans Pro', sans-serif;
    padding: 0;
    margin: 0;
    overflow: hidden;
    background: rgba(0, 0, 0, 0);
    color: #eee;
}

.body {
    font-size: 14px;
    width: 345px;
    border-radius: 10px;
    background: rgba(36, 39, 39, 0.9);
    padding: 20px 0px 20px 0px;
    display: flex;
    justify-content: center;
    align-items: center;
    flex-direction: column;
    height: 480px;
}

.settings {
    height: 100%;
    display: flex;
    justify-content: space-between;
    align-items: center;
    flex-direction: column;
}

.wrapper {
    display: flex;
    align-items: center;
    justify-content: space-between;
    width: 300px
}

select {
    outline: none;
    border: none;
    background: rgba(51, 55, 55, 0.9);
    border-radius: 6px;
    padding: 4px 10px 4px 10px;
    cursor: pointer;
}

select:hover {
    background: rgba(53, 57, 57, 0.9);
}

option {
    background: rgba(56, 60, 60, 0.9);
}

.btn-nav {
    display: flex;
    justify-content: space-around;
    align-items: center;
    margin-top: 30px;
}

.btn {
    cursor: pointer;
    margin-top: 10px;
    padding: 4px 10px 4px 10px;
    border-radius: 6px;
    user-select: none;
    margin: 0px 10px 0px 10px;
}

.btn:hover {
    color: #ccc
}

.button {
    margin-top: 10px;
}

.new_check {
    cursor: pointer;
    width: 20px;
    height: 20px;
    border-radius: 100%;
    margin-right: 30px;
    background: rgba(51, 55, 55, 0.9);
}

#auto-reconnect:checked + .new_check {
    background: radial-gradient(rgba(244, 244, 244, 0.9) 30%, rgba(51, 55, 55, 0.9) 29%);
}

#auto-update:checked + .new_check {
    background: radial-gradient(rgba(244, 244, 244, 0.9) 30%, rgba(51, 55, 55, 0.9) 29%);
}

#permutation:checked + .new_check {
    background: radial-gradient(rgba(244, 244, 244, 0.9) 30%, rgba(51, 55, 55, 0.9) 29%);
}

#start-hidden:checked + .new_check {
    background: radial-gradient(rgba(244, 244, 244, 0.9) 30%, rgba(51, 55, 55, 0.9) 29%);
}



Application
Использование в приложении: /app/Setting.

API
Интерфейс компонента Setting.

const { app } = require('electron')
    , SETTING = require('./../../app/components/setting')

app.on('ready', async() => {

    const Setting = new SETTING()

    // Только после того как окно инициализируются программа продолжит исполнятся
    await Setting.ready()

    // Показывает окно
    Setting.show()
    
    // Обработчик сохранения
    Setting.onSave(async () => {
        // Запрашиваем настройки
        const vpn_setting = await Setting.get()
        console.log(vpn_setting)
    })

})

Test
Версия для тестирования: /app_test/Setting.

image

9 часть — Callback компонент


Собственный VPN клиент на JavaScript by JSus

Комментарии (2)


  1. Sabubu
    30.11.2018 13:36

    Вопрос немного не по теме, но стоит ли добавлять свои шрифты и утяжелять приложение? Стандартный шрифт не годится?


    1. trickst_r
      30.11.2018 15:39

      Само приложение будет весить около 50мб, так как нужно тащить Chromium, так что шрифт погоды не сделает)