NotifyElectron компонент, элемент представления уведомлений.

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

notify
¦
¦   index.js
¦
L---client // Все что относится к клиенту
    ¦   fly.html
    ¦   static.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) {
        const window_tmp = {
            frame: false, // Убираем рамку 
            transparent: true, // Устанавливаем прозрачность
            resizable: false, // Запрещаем масштабирование
            show: false, // Запрещаем показывать окно после загрузки 
            focusable: true, // Окно может принимать фокус
            height: 175,
            width: 200,
            maxWidth: 200,
            parent
        }

        // Создаем окна
        this.root = new BrowserWindow(window_tmp)
        this.root2 = new BrowserWindow(window_tmp)
        
        // Уведомление порящее над основным окном
        this.root.loadURL(`${__dirname}/client/fly.html`)
        // Статическое уведомление  
        this.root2.loadURL(`${__dirname}/client/static.html`)

        // Тип уведомлений используемый по дефолту
        this.type = 'fly'
        
        // Ожидается ли ответ на уведомление confirm если 
        // ожидается то он не может быть скрыт обычным alert
        this.waitingConfirm = 0
    }

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

    setType(value) {
        // Метод изменяющий тип уведомления
        this.hide()
        this.type = value
    }

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

    alert(message = 'message', hideTime = 6000, callback = () => {}) {
        // Если есть активный confirm пропускаем
        if (this.waitingConfirm) {
            return
        }
        // Удаляем старый таймер
        clearTimeout(this.t) 
        // Скрываем старое уведомление
        this.hide()
        // В зависимости от типа уведомлений выбираем к какому окну обращаться
        const ROOT = this.type == 'fly' ? 'root' : 'root2'
        // Сигнал ALERT и сообщение на страницу
        this[ROOT].webContents.send('ALERT', message)
        // Показываем окно
        this[ROOT].show()
        // Снимаем фокус с окна
        this[ROOT].blur()
        
        // Окно ROOT будет скрыто через hideTime миллисекунд и вызван callback
        this.t = setTimeout(() => {
            this[ROOT].hide()
            callback()
        }, hideTime)

        // Если из окна поступает сигнал ALERT_HIDE то окно будет
        // скрыто, а таймер удален ну и вызван callback 
        ipcMain.once('ALERT_HIDE', () => {
            clearTimeout(this.t)
            this[ROOT].hide()
            callback()
        })
    }

    confirm(message, button, callback) {
        // Удаляем старый таймер
        clearTimeout(this.t)
        // Ставим блок
        this.waitingConfirm = 1
        // Скрываем старое уведомление
        this.hide()
        // В зависимости от типа уведомлений выбираем к какому окну обращаться
        const ROOT = this.type == 'fly' ? 'root' : 'root2'
        // Показываем окно
        this[ROOT].show()
        // Снимаем фокус с окна
        this[ROOT].blur()
        // Сигнал ALERT и сообщение на страницу и кнопки
        this[ROOT].webContents.send('CONFIRM', JSON.stringify({
            message, button
        }))

        // Если из окна поступает сигнал CONFIRM_CALLBACK то окно будет
        // скрыто и вызван callback в который будет передана data 
        // информация о той кнопке которая выбрана
        ipcMain.once('CONFIRM_CALLBACK', (e, data) => {
           // Снимаем блокировку  
           this.waitingConfirm = 0
            try {
                this[ROOT].hide()
                callback(data)
            } catch (e) {}
        })
    }
}


fly.html — HTML страница окна уведомлений типа «fly».

Cодержимое файла fly.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='message-box'>
        <div class='message'>
            <div class='text'>Загрузка ...</div>
            <div class='other'>
            </div>
        </div>
        <div class='vector-box'>
            <div class='vector'></div>
        </div>
    </div>
</div>
<script>
const Text = document.getElementsByClassName('text')[0]
    , Other = document.getElementsByClassName('other')[0]
    , { ipcRenderer, remote } = require('electron')

// Получаем контроль над окном
const win = remote.getCurrentWindow()

// Устанавливаем позицию окна основываясь на позиции окна vpn
setInterval(() => {
    win.setPosition(
        parseInt(localStorage.x - 43),
        parseInt(localStorage.y - 150)
    )
}, 3)

// Обработчик сигналов от компонента типа "alert"
ipcRenderer.on('ALERT', (e, data) => {
    Text.innerHTML = data
    Other.innerHTML = ''
})

// Обработчик сигналов от компонента типа "confirm"
ipcRenderer.on('CONFIRM', (e, data) => {
    const {
        message, button
    } = JSON.parse(data)

    Text.innerHTML = message
    Other.innerHTML = ''
    
    // Рендерим кнопки
    button.forEach(elem => {
        const btn = document.createElement('div')
        btn.className = 'btn'
        btn.innerHTML = elem.title
        btn.onclick = e => {
            ipcRenderer.send('CONFIRM_CALLBACK', elem.return)
            e.stopPropagation()
        }
        Other.appendChild(btn)
    })
})

// Отправляем компоненту сигнал что уведомление нужно скрыть
document.body.addEventListener('click', () => {
    ipcRenderer.send('ALERT_HIDE')
})
</script>
</body>
</html>


static.html — HTML страница окна уведомлений типа «static».

Cодержимое файла static.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='message-box'>
        <div class='message'>
            <div class='text'>Загрузка ...</div>
            <div class='other'>
            </div>
        </div>
    </div>
</div>
<script>
const Text = document.getElementsByClassName('text')[0]
    , Other = document.getElementsByClassName('other')[0]
    , Body = document.getElementsByClassName('body')[0] 
    , { ipcRenderer, remote } = require('electron')

// Получаем контроль над окном
const win = remote.getCurrentWindow()

// Отступы от окна
const padding = 13

setInterval(() => {

    // Получаем расположение уведомлений
    const static_notify = JSON.parse(localStorage.vpn_setting || "{}").PositionNotify || 'BR'

    const quickLaunch_Width = (window.screen.width - window.screen.availWidth),
        quickLaunch_Height = (window.screen.height - window.screen.availHeight),
        screen_Width = window.screen.width,
        screen_Height = window.screen.height,
        notify_Width = 200,
        notify_Heigth = 175
    
    // Низ право
    if (static_notify == 'BR') {
        win.setPosition(
            (screen_Width - quickLaunch_Width) - (notify_Width + padding), (screen_Height - quickLaunch_Height) - (notify_Heigth + padding)
        )
        Body.style.alignItems = 'flex-end'
        Body.style.justifyContent = 'flex-end'
    }

    // Верх право
    if (static_notify == 'TR') {
        win.setPosition((screen_Width - quickLaunch_Width) - (notify_Width + padding), quickLaunch_Height + padding)
        Body.style.alignItems = 'flex-end'
        Body.style.justifyContent = 'flex-start'
    }

    // Верх лево
    if (static_notify == 'TL') {
        win.setPosition(quickLaunch_Width + padding, quickLaunch_Height + padding)
        Body.style.alignItems = 'flex-start'
        Body.style.justifyContent = 'flex-start'
    }
    
    // Низ лево
    if (static_notify == 'BL') {
        win.setPosition(quickLaunch_Width + padding, window.screen.availHeight - 188)
        Body.style.alignItems = 'flex-start'
        Body.style.justifyContent = 'flex-end'
    }

}, 100)

// Обработчик сигналов от компонента типа "alert"
ipcRenderer.on('ALERT', (e, data) => {
    Text.innerHTML = data
    Other.innerHTML = ''
})

// Обработчик сигналов от компонента типа "confirm"
ipcRenderer.on('CONFIRM', (e, data) => {
    const {
        message, button
    } = JSON.parse(data)

    Text.innerHTML = message
    Other.innerHTML = ''

    // Рендерим кнопки
    button.forEach(elem => {
        const btn = document.createElement('div')
        btn.className = 'btn'
        btn.innerHTML = elem.title
        btn.onclick = e => {
            ipcRenderer.send('CONFIRM_CALLBACK', elem.return)
            e.stopPropagation()
        }
        Other.appendChild(btn)
    })
})

// Отправляем компоненту что уведомление нужно скрыть
document.body.addEventListener('click', () => {
    ipcRenderer.send('ALERT_HIDE')
})
</script>
</body>
</html>


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;
}

@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;
}

@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;
}

@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;
}

@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;
}

@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;
}

@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);
}

.body {
    display: flex;
    justify-content: flex-end;
    align-items: center;
    flex-direction: column;
    height: 175px;
}

.message-box {
    display: flex;
    justify-content: center;
    align-items: center;
    flex-direction: column;
}

.message {
    border-radius: 10px;
    font-size: 14px;
    background: rgba(36, 39, 39, 0.9);
    padding: 10px 10px 10px 10px;
    color: #eee;
    width: auto;
    max-width: 180px;
    min-width: 40px;
}

.other {
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.btn {
    min-width: 35px;
    width: 100%;
    color: #dcdcdc;
    background: #7b7b7b;
    margin: 8px 2px 0px 2px;
    padding: 3px;
    font-size: 13px;
    border-radius: 3px;
    text-align: center;
    user-select: none;
    cursor: pointer;
}

.button:hover {
    background: #e6e6e6;
    background: #868686;
}

input {
    cursor: pointer;
    border: none;
    border-radius: 6px;
    padding: 4px 10px 4px 10px;
    margin: 2px;
    color: #eee;
    outline: none;
    background: rgba(51, 55, 55, 0.9);
}

input:hover {
    color: #ccc
}

.vector {
    width: 30px;
    height: 30px;
    background: rgba(36, 39, 39, 0.9);
    transform: rotateZ(45deg);
    border-radius: 5px;
}

.vector-box {
    padding: 4px;
    margin-top: 0px;
    height: 8px;
    overflow: hidden;
    position: relative;
    z-index: 0;
    transform: rotateZ(180deg);
}


Application

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

API

Интерфейс компонента Notify. В этом примере уже более явно должна просматриваться та организация кода, о которой я говорил.

const { app }   = require('electron')
     , VPN      = require('./../../app/components/vpn')
     , NOTIFY   = require('./../../app/components/notify')

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

    const Vpn    = new VPN()
        , Notify = new NOTIFY(Vpn.root)

    // Только после того как окна инициализируются программа продолжит исполнятся
    await Promise.all([
        Notify.ready(),
        Vpn.ready()
    ])

    Vpn.show()
    Vpn.showTray()

    const CONFIRN_FSB = [{
        title: 'Да',
        return: 'Да, подключайте меня!'
    }, {
        title: 'Нет',
        return: false
    }]

    setTimeout(() => {
        // Уведомление типа "alert"
        Notify.alert('Прогресс не остановить!', 3000, () => {
            // Уведомление типа "confirm"
            Notify.confirm('Вы хотите подключиться к VPN', CONFIRN_FSB, data => {
                console.log(data)
                if (data == false) {
                    // Устанавливает тип уведомления
                    Notify.setType('static')
                        // Уведомление типа "confirm"
                    Notify.confirm('Вы хотите выйти ?', CONFIRN_FSB, data => {
                        if (data != false) {
                            app.quit()
                        }
                    })
                } else {
                    Vpn.setStatus('resolve', 'blue')
                }
            })
        })
    }, 2000)

    // Устанавливает тип уведомления
    // Notify.setType('static')

    // Уведомление типа "alert"
    // Notify.alert('Прогресс не остановить!', 3000, () => {})

    // Уведомление типа "confirm"
    //Notify.confirm('Вы хотите подключиться к VPN', CONFIRN_FSB, console.log)
})

Test

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

image

Типы уведомлений Notify.setType.
Тип Описание
static Уведомление, всплывающее в одной из четырех сторон экрана
fly Уведомление, всплывающее всегда над основным окном

7 часть — Context компонент


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

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