Собственный VPN клиент на JavaScript. 8 часть — Electron компонент Setting

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

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

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

    context
    │
    │   index.js
    │
    └───client // все что относится к клиенту
        │   creater-option.js
        │   index.html
        │   style.css
        │
        └───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
    • +10
    • 1,3k
    • 2
    Поделиться публикацией
    Комментарии 2
      0
      Вопрос немного не по теме, но стоит ли добавлять свои шрифты и утяжелять приложение? Стандартный шрифт не годится?
        +1
        Само приложение будет весить около 50мб, так как нужно тащить Chromium, так что шрифт погоды не сделает)

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое