├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── app ├── clock.js ├── config.js ├── db.js ├── index.js ├── launch.js ├── menu.js ├── notice.js ├── preload.js ├── renderer.js ├── static │ ├── about-darkmode.svg │ ├── about.svg │ ├── check-darkmode.svg │ ├── check.svg │ ├── collapse-darkmode.svg │ ├── collapse.svg │ ├── donate-darkmode.svg │ ├── donate.svg │ ├── exit-darkmode.svg │ ├── exit.svg │ ├── eye-closed-darkmode.svg │ ├── eye-closed.svg │ ├── eye-open-darkmode.svg │ ├── eye-open.svg │ ├── icon.icns │ ├── icon.ico │ ├── icon.png │ ├── loading-darkmode.svg │ ├── loading.svg │ ├── moon-darkmode.svg │ ├── moon.svg │ ├── remove-darkmode.svg │ ├── remove.svg │ ├── search-darkmode.svg │ ├── search.svg │ ├── style.css │ ├── sun-darkmode.svg │ ├── sun.svg │ ├── tray-darkmodeTemplate.png │ ├── tray-darkmodeTemplate@2x.png │ ├── tray-darkmodeTemplate@3x.png │ ├── tray-darkmodeTemplate@4x.png │ ├── tray-zeroTemplate.png │ ├── trayTemplate.png │ ├── trayTemplate@2x.png │ ├── trayTemplate@3x.png │ └── trayTemplate@4x.png ├── templates │ └── index.html ├── tray.js ├── updater.js └── window.js └── package.json /.gitattributes: -------------------------------------------------------------------------------- 1 | *.zip filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: ['https://www.buymeacoffee.com/hovrly'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | builds 3 | macos 4 | electron-builder.yml 5 | package-lock.json 6 | 7 | .DS_* 8 | .Trashes 9 | Thumbs.db 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Alexey Tarutin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hovrly - A tiny time zone clocks for distributed teams | Product Hunt 4 | 5 | ## Download & Install 6 | via Brew 7 | ``` 8 | brew install hovrly 9 | ``` 10 | Or 11 | Download App from **hovrly.com** 12 | 13 | ## Contact Us 14 | Telegram — **@hovrly_feedback** 15 | or 16 | Mail — **bolshakov@onmail.com** 17 | 18 | ## License 19 | MIT 20 | -------------------------------------------------------------------------------- /app/clock.js: -------------------------------------------------------------------------------- 1 | module.exports = { init, formatTime, getTzTime, isCompactView, isTwentyFourHour, isCollapsed, isDate } 2 | 3 | const path = require('path') 4 | const electron = require('electron') 5 | const app = electron.app 6 | const settings = require('electron-settings') 7 | const config = require('./config') 8 | const tray = require('./tray') 9 | const ipc = electron.ipcMain 10 | const window = require('./window') 11 | const notice = require('./notice') 12 | 13 | var clock = null 14 | 15 | function init() { 16 | console.log('clock init') 17 | 18 | // settings.unsetSync() 19 | // console.log( settings.getSync() ) 20 | 21 | if(!settings.hasSync('clocks[0].timezone')) { 22 | resetClocks() 23 | } 24 | 25 | if(!settings.hasSync('twentyfourhour')) { 26 | settings.setSync('twentyfourhour', 'on') 27 | } 28 | 29 | if(!settings.hasSync('compact')) { 30 | settings.setSync('compact', 'off') 31 | } 32 | 33 | if(!settings.hasSync('collapse')) { 34 | settings.setSync('collapse', 'off') 35 | } 36 | 37 | if(!settings.hasSync('date')) { 38 | settings.setSync('date', 'off') 39 | } 40 | 41 | ipc.on('compact', () => { 42 | settings.setSync('compact', isCompactView() == 'on' ? 'off' : 'on') 43 | update() 44 | }) 45 | 46 | ipc.on('twentyfourhour', () => { 47 | settings.setSync('twentyfourhour', isTwentyFourHour() == 'off' ? 'on' : 'off') 48 | update() 49 | }) 50 | 51 | ipc.on('collapse', () => { 52 | settings.setSync('collapse', isCollapsed() == 'off' ? 'on' : 'off') 53 | update() 54 | }) 55 | 56 | ipc.on('date', () => { 57 | settings.setSync('date', isDate() == 'off' ? 'on' : 'off') 58 | update() 59 | }) 60 | 61 | ipc.on('clocks-get', () => { 62 | let clocks = settings.getSync('clocks') 63 | let win = window.getWin() 64 | for (let i in clocks) { 65 | win.webContents.send('add-clock', clocks[i]) 66 | } 67 | 68 | update() 69 | runClock() 70 | }) 71 | 72 | ipc.on('clocks-sort', (e, sortTo) => { 73 | let clocks = settings.getSync('clocks') 74 | let newClocks = [] 75 | 76 | sortTo.forEach(to => { 77 | clocks.forEach(from => { 78 | if(from.full.trim() == to.trim()) { 79 | newClocks.push(from) 80 | } 81 | }) 82 | }) 83 | 84 | settings.setSync('clocks', newClocks) 85 | update() 86 | }) 87 | 88 | ipc.on('clock-add', (e, city) => { 89 | if (!city) return 90 | 91 | let clocks = settings.getSync('clocks') 92 | let issetClock = false 93 | 94 | clocks.forEach(clock => { 95 | if (clock.full.trim() == city.full.trim()) { 96 | issetClock = true 97 | } 98 | }) 99 | 100 | if (!issetClock) { 101 | clocks.push(city) 102 | settings.setSync('clocks', clocks) 103 | let win = window.getWin() 104 | win.webContents.send('add-clock', city) 105 | } 106 | }) 107 | 108 | ipc.on('clock-rename', (e, oldName, newName) => { 109 | let clocks = settings.getSync('clocks') 110 | clocks.forEach((clock, index) => { 111 | if (clock.full.trim() == oldName.trim()) { 112 | clocks[index].name = newName 113 | clocks[index].full = newName 114 | } 115 | }) 116 | 117 | settings.setSync('clocks', clocks) 118 | update() 119 | }) 120 | 121 | ipc.on('clock-remove', (e, cityName) => { 122 | let clocks = settings.getSync('clocks') 123 | clocks.forEach((clock, index) => { 124 | if (clock.full.trim() == cityName.trim()) { 125 | clocks.splice(index, 1) 126 | } 127 | }) 128 | 129 | settings.setSync('clocks', clocks) 130 | update() 131 | }) 132 | 133 | ipc.on('clock-toggle', (e, cityName) => { 134 | let clocks = settings.getSync('clocks') 135 | clocks.forEach((clock, index) => { 136 | if (clock.full.trim() == cityName.trim()) { 137 | clocks[index].tray = clock.tray ? false : true 138 | } 139 | }) 140 | 141 | settings.setSync('clocks', clocks) 142 | update() 143 | }) 144 | } 145 | 146 | function isTwentyFourHour() { 147 | return settings.getSync('twentyfourhour') 148 | } 149 | 150 | function isCompactView() { 151 | return settings.getSync('compact') 152 | } 153 | 154 | function isCollapsed() { 155 | return settings.getSync('collapse') 156 | } 157 | 158 | function isDate() { 159 | return settings.getSync('date') 160 | } 161 | 162 | function parseClockName(name) { 163 | if(isCompactView() == 'on') { 164 | let isEmoji = /\p{Emoji}/ug.test(name) 165 | let isEmojiExtended = /\p{Extended_Pictographic}/ug.test(name) 166 | 167 | if(!isEmoji && !isEmojiExtended) { 168 | let spaces = (name.split(' ').length - 1) 169 | 170 | if(spaces == 1 || spaces == 2) { 171 | name = name 172 | .match(/[\p{Alpha}\p{Nd}]+/gu) 173 | .reduce((previous, next) => previous + ((+next === 0 || parseInt(next)) ? parseInt(next): next[0] || ''), '') 174 | .toUpperCase() 175 | } 176 | else { 177 | name = name.substring(0, 3).toUpperCase() 178 | } 179 | } 180 | } 181 | 182 | return name 183 | } 184 | 185 | function runClock() { 186 | var now = new Date() 187 | var tick = (60 - now.getSeconds()) * 1000 - now.getMilliseconds() 188 | setTimeout(function() { 189 | update() 190 | runClock() 191 | }, tick) 192 | } 193 | 194 | function update() { 195 | let title = [] 196 | let clocks = settings.getSync('clocks') 197 | let days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] 198 | let months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] 199 | let now = new Date() 200 | let date = isDate() == 'on' ? `${days[now.getDay()]} ${now.getDate()} ${months[now.getMonth()]}` : null 201 | let isVisibleClock = false 202 | 203 | for (let i in clocks) { 204 | if (clocks[i].tray) { 205 | isVisibleClock = true 206 | break 207 | } 208 | } 209 | 210 | for (let i in clocks) { 211 | if (clocks[i].tray) { 212 | if(!clocks[i].timezone) continue 213 | let tz = getTzTime(clocks[i].timezone) 214 | title.push(parseClockName(clocks[i].name) + ' ' + tz.time) 215 | } 216 | } 217 | 218 | tray.setTitle( 219 | ( 220 | isDate() == 'on' 221 | ? 222 | (!isVisibleClock ? ' ' : '') 223 | + date 224 | + (isVisibleClock ? ' ' : '') 225 | : 226 | '' 227 | ) 228 | + title.join(' ') 229 | ) 230 | tray.update() 231 | } 232 | 233 | function getTzTime(tz, offset) { 234 | let tzDate = new Date().toLocaleString('en-US', {timeZone: tz}) 235 | let utc_offset = new Date(tzDate).getTime() + (offset ? offset : 0) 236 | 237 | return formatTime(utc_offset) 238 | } 239 | 240 | function resetClocks() { 241 | settings.setSync('clocks', [ 242 | { name: 'Berlin', full: 'Berlin, DE', timezone: 'Europe/Berlin', tray: 1 }, 243 | { name: 'New York', full: 'New York, US', timezone: 'America/New_York', tray: 1 }, 244 | ]) 245 | } 246 | 247 | function formatTime(ts, local) { 248 | let date = new Date(ts) 249 | let hours = local ? date.getUTCHours() : date.getHours() 250 | let minutes = local ? date.getUTCMinutes() : date.getMinutes() 251 | let ampm = hours >= 12 ? 'PM' : 'AM' 252 | let morning = hours >= 4 && hours < 21 ? 'morning' : 'evening' 253 | 254 | minutes = minutes < 10 ? '0'+minutes : minutes 255 | 256 | if(isTwentyFourHour() == 'off') { 257 | hours = hours % 12 258 | hours = hours ? hours : 12 259 | 260 | return { 261 | time: `${hours}:${minutes} ${ampm}`, 262 | morning: morning 263 | } 264 | } 265 | else { 266 | if(hours < 10) hours = '0'+hours 267 | 268 | return { 269 | time: `${hours}:${minutes}`, 270 | morning: morning 271 | } 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /app/config.js: -------------------------------------------------------------------------------- 1 | const pkg = require('../package.json') 2 | const config = { 3 | APP_NAME: pkg.productName, 4 | APP_VERSION: pkg.version, 5 | TRAY_ICON_MAC: `${__dirname}/static/trayTemplate.png`, 6 | TRAY_ICON_MAC_DARKMODE: `${__dirname}/static/tray-darkmodeTemplate.png`, 7 | TRAY_ICON_WIN: `${__dirname}/static/tray-darkmodeTemplate.png`, 8 | TRAY_ICON_ZERO: `${__dirname}/static/tray-zeroTemplate.png`, 9 | DOCK_ICON: `${__dirname}/static/icon.ico`, 10 | WIN_WIDTH: 325, 11 | DELAYED_INIT: 3000, 12 | UPDATER_CHECK_TIME: 1000 * 60 * 30, 13 | UPDATER_CHECK_URL: 'https://app.hovrly.com', 14 | DB_CONNECT: 'mysql://hovrly:rocks@db.hovrly.com/hovrly?charset=UTF8_GENERAL_CI' /* JFYI: only SELECT access granted 😈 */, 15 | LINK_ABOUT: 'https://hovrly.com/', 16 | LINK_DONATE: 'https://hovrly.com/donate', 17 | DEV_TOOLS: false, 18 | FINTEZA_KEY: 'piigiltuhuaaursdcfgukmfmnchprejbav', 19 | } 20 | 21 | module.exports = config 22 | -------------------------------------------------------------------------------- /app/db.js: -------------------------------------------------------------------------------- 1 | module.exports = { init, find } 2 | 3 | const config = require('./config') 4 | const mysql = require('mysql2') 5 | 6 | var db 7 | var connect = false 8 | 9 | function init() { 10 | console.log('db init') 11 | 12 | db = mysql.createConnection(config.DB_CONNECT) 13 | 14 | db.connect(function(err) { 15 | if (err) { 16 | connect = false 17 | console.log('db not connected:', err.code) 18 | } else { 19 | connect = true 20 | } 21 | }) 22 | 23 | db.on('error', function() { 24 | connect = false 25 | }) 26 | 27 | setInterval(reconnect, 2000) 28 | } 29 | 30 | function find(q, callback) { 31 | if (!connect) return 32 | var results = null 33 | 34 | db.query(q, function(error, results, fields) { 35 | if (error) { 36 | console.log('db err', error.code) 37 | connect = false 38 | reconnect() 39 | } 40 | if(results[0]) callback(results[0], fields) 41 | else callback(false) 42 | }) 43 | } 44 | 45 | function disconnect() { 46 | connect = false 47 | db.end() 48 | } 49 | 50 | function reconnect() { 51 | if (!connect) { 52 | console.log('db', 'lost connect') 53 | db = mysql.createConnection(config.DB_CONNECT) 54 | db.connect(function(err) { 55 | if (!err) { 56 | connect = true 57 | console.log('db', 'reconnected') 58 | } 59 | }) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | console.time('inited') 2 | 3 | const config = require('./config') 4 | const path = require('path') 5 | const electron = require('electron') 6 | const app = electron.app 7 | const ipc = electron.ipcMain 8 | const shell = electron.shell 9 | const tray = require('./tray') 10 | const menu = require('./menu') 11 | const window = require('./window') 12 | const launch = require('./launch') 13 | const clock = require('./clock') 14 | const notice = require('./notice') 15 | const updater = require('./updater') 16 | const system = electron.systemPreferences 17 | const session = electron.session 18 | const db = require('./db') 19 | 20 | var isDev = process.env.DEV ? (process.env.DEV.trim() == 'true') : false 21 | process.on('uncaughtException', error) 22 | app.console = new console.Console(process.stdout, process.stderr) 23 | 24 | app.whenReady().then(() => { 25 | console.log('index init') 26 | 27 | window.init() 28 | menu.init() 29 | tray.init() 30 | launch.init() 31 | clock.init() 32 | updater.init() 33 | notice.init() 34 | db.init() 35 | 36 | ipc.on('ready', () => { 37 | if(process.platform == 'darwin') { 38 | app.dock.hide() 39 | } 40 | 41 | setTimeout(updater.auto, config.DELAYED_INIT) 42 | 43 | console.timeEnd('inited') 44 | }) 45 | 46 | ipc.on('exit', app.quit) 47 | 48 | ipc.on('about', () => { 49 | window.hide() 50 | shell.openExternal(config.LINK_ABOUT) 51 | }) 52 | 53 | ipc.on('donate', () => { 54 | window.hide() 55 | shell.openExternal(config.LINK_DONATE) 56 | }) 57 | }) 58 | 59 | app.on('window-all-closed', () => { 60 | if (process.platform !== 'darwin') { 61 | app.quit() 62 | } 63 | }) 64 | 65 | app.on('activate', window.show) 66 | 67 | app.on('before-quit', () => { 68 | app.quitting = true 69 | }) 70 | 71 | function error(error) { 72 | console.error(error) 73 | if(!isDev) return 74 | 75 | if(typeof error == 'object') notice.send('Error: ' + error.message) 76 | else notice.send('Error: ' + error) 77 | } 78 | -------------------------------------------------------------------------------- /app/launch.js: -------------------------------------------------------------------------------- 1 | module.exports = { init, isAutoOpen } 2 | 3 | const path = require('path') 4 | const electron = require('electron') 5 | const app = electron.app 6 | const config = require('./config') 7 | const AutoLaunch = require('auto-launch') 8 | const ipc = electron.ipcMain 9 | 10 | var launch 11 | 12 | function init() { 13 | console.log('launch init') 14 | 15 | let appPath = process.platform === 'darwin' ? app.getPath('exe').replace(/\.app\/Content.*/, '.app') : undefined 16 | launch = new AutoLaunch({ name:config.APP_NAME, path:appPath, isHidden:true }) 17 | 18 | ipc.on('startup', () => { 19 | launch.isEnabled().then(enabled => { 20 | if(!enabled) launch.enable() 21 | else launch.disable() 22 | }) 23 | }) 24 | } 25 | 26 | function isAutoOpen() 27 | { 28 | return !!app.getLoginItemSettings().openAtLogin 29 | } 30 | -------------------------------------------------------------------------------- /app/menu.js: -------------------------------------------------------------------------------- 1 | module.exports = { init } 2 | 3 | const electron = require('electron') 4 | const config = require('./config') 5 | const app = electron.app 6 | const Menu = electron.Menu 7 | 8 | function init() { 9 | console.log('menu init') 10 | 11 | let template = 12 | [ 13 | { 14 | label: config.APP_NAME, 15 | submenu: [ 16 | { role: 'about', label: `About ${config.APP_NAME}` }, 17 | { type: 'separator' }, 18 | { role: 'hide', label: `Hide`, }, 19 | { role: 'hideothers' }, 20 | { role: 'unhide' }, 21 | { type: 'separator' }, 22 | { role: 'quit', label: `Quit ${config.APP_NAME}`, }, 23 | ], 24 | }, 25 | { 26 | label: 'Edit', 27 | submenu: [ 28 | {role: 'cut'}, 29 | {role: 'copy'}, 30 | {role: 'paste'}, 31 | {role: 'selectall'} 32 | ] 33 | }, 34 | { 35 | label: 'Window', 36 | role: 'window', 37 | submenu: [ 38 | { role: 'minimize' }, 39 | { role: 'close' }, 40 | ], 41 | }, 42 | ] 43 | 44 | Menu.setApplicationMenu(Menu.buildFromTemplate(template)) 45 | } -------------------------------------------------------------------------------- /app/notice.js: -------------------------------------------------------------------------------- 1 | module.exports = { init, send } 2 | 3 | const electron = require('electron') 4 | const config = require('./config') 5 | const Notification = electron.Notification 6 | 7 | function init() { 8 | console.log('notice init') 9 | 10 | // console.log(Notification.isSupported()) 11 | } 12 | 13 | function send(text, callback) { 14 | let notice = new Notification({ 15 | title: config.APP_NAME, 16 | body: text, 17 | silent: true, 18 | // icon: `${__dirname}/static/icon.png`, 19 | }) 20 | 21 | notice.show() 22 | if (callback) notice.on('click', callback) 23 | } 24 | -------------------------------------------------------------------------------- /app/preload.js: -------------------------------------------------------------------------------- 1 | const $ = selector => document.querySelector(selector) 2 | const $all = selector => document.querySelectorAll(selector) 3 | const electron = require('electron') 4 | const remote = electron.remote 5 | const ipc = electron.ipcRenderer 6 | const config = remote.require('./config') 7 | 8 | function init() 9 | { 10 | document.title = config.APP_NAME 11 | 12 | ipc.send('clocks-get') 13 | 14 | $('.update .version').innerHTML = `v${config.APP_VERSION}` 15 | 16 | $all('.app-name').forEach(item => { item.innerText = config.APP_NAME }) 17 | } 18 | 19 | window.addEventListener('DOMContentLoaded', init) 20 | -------------------------------------------------------------------------------- /app/renderer.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron') 2 | const remote = electron.remote 3 | const app = remote.app 4 | const ipc = electron.ipcRenderer 5 | const config = remote.require('./config') 6 | const db = remote.require('./db') 7 | const clock = remote.require('./clock') 8 | const launch = remote.require('./launch') 9 | const nativeTheme = remote.nativeTheme 10 | const Sortable = require('sortablejs') 11 | const $ = selector => document.querySelector(selector) 12 | const $all = selector => document.querySelectorAll(selector) 13 | 14 | function init() 15 | { 16 | ipc.on('app-height-get', updateAppHeight) 17 | 18 | theme() 19 | slider() 20 | clocks() 21 | search() 22 | quit() 23 | update() 24 | startup() 25 | twentyforhour() 26 | compact() 27 | date() 28 | about() 29 | donate() 30 | collapse() 31 | sortable() 32 | 33 | ipc.send('ready') 34 | } 35 | 36 | 37 | function sortable() 38 | { 39 | let sortable = Sortable.create($('.clock'), { 40 | draggable: 'button', 41 | onUpdate: () => { 42 | let sortTo = [] 43 | $all('.clock button').forEach(item => { 44 | let name = item.querySelector('.name').innerText 45 | sortTo.push(name) 46 | }) 47 | 48 | ipc.send('clocks-sort', sortTo) 49 | }, 50 | }) 51 | } 52 | 53 | function collapse() 54 | { 55 | setTimeout(function() { 56 | if(clock.isCollapsed() == 'on') { 57 | $('.app').classList.add('tiny') 58 | $('.clock').style.maxHeight = '499px' 59 | } 60 | else { 61 | $('.app').classList.remove('tiny') 62 | $('.clock').style.maxHeight = '210px' 63 | } 64 | }, 1) 65 | 66 | $('.collapse .toggle').addEventListener('click', e => { 67 | if($('.app').classList.contains('tiny')) { 68 | $('.app').classList.remove('tiny') 69 | $('.clock').style.maxHeight = '210px' 70 | } 71 | else { 72 | $('.app').classList.add('tiny') 73 | $('.clock').style.maxHeight = '499px' 74 | } 75 | 76 | updateAppHeight() 77 | ipc.send('collapse') 78 | }) 79 | } 80 | 81 | function slider() 82 | { 83 | current() 84 | 85 | $('.slider input').addEventListener('input', sliderRecalc) 86 | 87 | // $('.slider input').addEventListener('mousedown', e => { 88 | // $all('.clock button:not(.active)').forEach(item => { 89 | // item.classList.add('focus') 90 | // }) 91 | // }) 92 | // 93 | // $('.slider input').addEventListener('mouseup', e => { 94 | // current() 95 | // 96 | // $all('.clock button:not(.active)').forEach(item => { 97 | // item.classList.remove('focus') 98 | // }) 99 | // }) 100 | 101 | $('.slider input').addEventListener('mouseup', e => { 102 | current() 103 | }) 104 | 105 | function current() 106 | { 107 | $('.slider input').value = (new Date().getHours() * 60) + new Date().getMinutes() 108 | sliderRecalc() 109 | } 110 | } 111 | 112 | function update() 113 | { 114 | $('.update').addEventListener('click', () => { 115 | if($('.update').classList.contains('install')) { 116 | $('.update').classList.add('loading') 117 | ipc.send('update-install') 118 | } 119 | else { 120 | $('.update').classList.add('loading') 121 | $('.update-message').innerText = 'Checking...' 122 | ipc.send('update-check') 123 | } 124 | }) 125 | 126 | ipc.on('update-finish', (e, result) => { 127 | if($('.update').classList.contains('install')) return 128 | 129 | if(result == 'dev-mode') { 130 | $('.update').classList.remove('loading') 131 | $('.update-message').innerHTML = `Not working on Dev` 132 | setTimeout(() => { $('.update-message').innerText = 'Check for Update' }, 3000) 133 | } 134 | 135 | if(result == 'downloaded') { 136 | $('.update').classList.add('install') 137 | $('.update').classList.remove('loading') 138 | $('.update-message').innerText = 'Install Update & Restart' // `Ready! Please Relaunch ${config.APP_NAME}` 139 | $('.app.tiny .collapse button').classList.add('install') 140 | 141 | } 142 | 143 | if(result == 'available') { 144 | $('.update-message').innerHTML = 'New version! Downloading...' 145 | } 146 | 147 | if(result == 'not-available') { 148 | $('.update').classList.remove('loading') 149 | $('.update-message').innerHTML = `You have latest version` 150 | setTimeout(() => { $('.update-message').innerText = 'Check for Update' }, 3000) 151 | } 152 | }) 153 | } 154 | 155 | function theme() 156 | { 157 | setTimeout(() => { 158 | $('.app').classList.add(nativeTheme.shouldUseDarkColors ? 'dark' : 'light') 159 | }, 1) 160 | 161 | nativeTheme.on('updated', () => { 162 | $('.app').classList.remove('dark', 'light') 163 | $('.app').classList.add(nativeTheme.shouldUseDarkColors ? 'dark' : 'light') 164 | }) 165 | } 166 | 167 | function twentyforhour() 168 | { 169 | setTimeout(function() { 170 | if(clock.isTwentyFourHour() == 'on') { 171 | $('.twentyfourhour').classList.add('active') 172 | $('.slider .from').innerText = '00:00' 173 | $('.slider .to').innerText = '23:59' 174 | } 175 | else { 176 | $('.clock').classList.add('ampm') 177 | $('.slider .from').innerText = '12:00 AM' 178 | $('.slider .to').innerText = '11:59 PM' 179 | } 180 | }, 1) 181 | 182 | $('.twentyfourhour').addEventListener('click', e => { 183 | e.target.classList.toggle('active') 184 | 185 | if($('.clock').classList.contains('ampm')) { 186 | $('.clock').classList.remove('ampm') 187 | $('.slider .from').innerText = '00:00' 188 | $('.slider .to').innerText = '23:59' 189 | } 190 | else { 191 | $('.clock').classList.add('ampm') 192 | $('.slider .from').innerText = '12:00 AM' 193 | $('.slider .to').innerText = '11:59 PM' 194 | } 195 | 196 | ipc.send('twentyfourhour') 197 | sliderRecalc() 198 | updateTime() 199 | }) 200 | } 201 | 202 | function compact() 203 | { 204 | setTimeout(function() { 205 | if(clock.isCompactView() == 'on') $('.compact').classList.add('active') 206 | }, 1) 207 | 208 | $('.compact').addEventListener('click', e => { 209 | e.target.classList.toggle('active') 210 | ipc.send('compact') 211 | }) 212 | } 213 | 214 | function date() 215 | { 216 | setTimeout(function() { 217 | if(clock.isDate() == 'on') $('.date').classList.add('active') 218 | }, 1) 219 | 220 | $('.date').addEventListener('click', e => { 221 | e.target.classList.toggle('active') 222 | ipc.send('date') 223 | }) 224 | } 225 | 226 | function donate() 227 | { 228 | $('.support').addEventListener('click', e => { 229 | ipc.send('donate') 230 | }) 231 | } 232 | 233 | function about() 234 | { 235 | $('.about').addEventListener('click', e => { 236 | ipc.send('about') 237 | }) 238 | } 239 | 240 | function startup() 241 | { 242 | setTimeout(function() { 243 | if(launch.isAutoOpen()) $('.startup').classList.add('active') 244 | }, 1) 245 | 246 | $('.startup').addEventListener('click', e => { 247 | e.target.classList.toggle('active') 248 | ipc.send('startup') 249 | }) 250 | } 251 | 252 | function quit() 253 | { 254 | $('.exit').addEventListener('click', () => { 255 | ipc.send('exit') 256 | }) 257 | } 258 | 259 | function search() 260 | { 261 | var newclock = null 262 | $('.search input').addEventListener('keyup', e => { 263 | let keycode = e.keyCode ? e.keyCode : e.which 264 | let q = $('.search input').value.trim().replace(',', '') 265 | 266 | // $('.search label').innerText = '' 267 | 268 | if(keycode == 13) { 269 | if(newclock) { 270 | ipc.send('clock-add', newclock) 271 | newclock = null 272 | } 273 | 274 | $('.search label').innerText = '' 275 | $('.search input').value = '' 276 | } 277 | else if(keycode == 27) { 278 | newclock = null 279 | $('.search label').innerText = '' 280 | $('.search input').value = '' 281 | } 282 | else { 283 | if (q == '') { 284 | newclock = null 285 | $('.search label').innerText = '' 286 | } 287 | else { 288 | let query = `SELECT name, UPPER(country) code, timezone FROM cities WHERE CONCAT(city, ' ', country) LIKE '%${q}%' ORDER BY popularity DESC LIMIT 1` 289 | 290 | db.find(query, city => { 291 | if(city.name && !city.timezone) return 292 | let fullName = city.name + (city.code ? ', ' + city.code : '') 293 | $('.search label').innerText = !city.name ? 'Not found' : fullName 294 | newclock = city.name ? { name: city.name, full: fullName, timezone: city.timezone, tray: 0 } : null 295 | }) 296 | } 297 | } 298 | }) 299 | } 300 | 301 | function clocks() 302 | { 303 | setTimeout(function() { 304 | updateTime() 305 | runClock() 306 | }, 1) 307 | 308 | ipc.on('add-clock', (e, clock) => { 309 | if(!clock.timezone) return 310 | 311 | let button = document.createElement('button') 312 | 313 | if(clock.tray) button.classList.add('active') 314 | 315 | button.innerHTML = ` 316 | 317 | ${clock.full} 318 | 319 | 320 | ` 321 | // button.setAttribute('data-id', $('.clock button').length+1) 322 | // button.setAttribute('data-name', clock.name) 323 | 324 | // rename 325 | 326 | var inputPrevName = '' 327 | 328 | // mouseover 329 | button.querySelector('.name').addEventListener('mouseover', e => { 330 | e.target.closest('.name').classList.add('hover') 331 | }) 332 | 333 | // mouseout 334 | button.querySelector('.name').addEventListener('mouseout', e => { 335 | let input = e.target.closest('.name') 336 | 337 | if(!input.classList.contains('focus')) { 338 | input.classList.remove('hover') 339 | } 340 | }) 341 | 342 | // activate 343 | button.querySelector('.name').addEventListener('focus', e => { 344 | e.stopPropagation() 345 | let input = e.target.closest('.name') 346 | input.classList.add('focus') 347 | input.scrollLeft = 0 348 | inputPrevName = input.innerText 349 | }) 350 | 351 | // deactivate 352 | button.querySelector('.name').addEventListener('blur', e => { 353 | e.stopPropagation() 354 | let input = e.target.closest('.name') 355 | input.classList.remove('focus', 'hover') 356 | input.blur() 357 | input.scrollLeft = 0 358 | input.innerText = inputPrevName 359 | }) 360 | 361 | // paste 362 | button.querySelector('.name').addEventListener('paste', e => { 363 | let ev = (e.originalEvent || e) 364 | let input = ev.target.closest('.name') 365 | let text = ev.clipboardData.getData('text/plain') 366 | inputPrevName = input.innerText 367 | text = text.replace(/<\/?[^>]+(>|$)/g, '').trim() 368 | document.execCommand('insertHTML', false, text) 369 | e.preventDefault() 370 | }) 371 | 372 | button.querySelector('.name').addEventListener('keydown', e => { 373 | let keycode = e.keyCode ? e.keyCode : e.which 374 | let input = e.target.closest('.name') 375 | 376 | if(keycode == 13) { 377 | ipc.send('clock-rename', inputPrevName, input.innerText) 378 | inputPrevName = input.innerText 379 | input.blur() 380 | e.preventDefault() 381 | } 382 | 383 | if(keycode == 27) { 384 | input.classList.remove('focus', 'hover') 385 | input.innerText = inputPrevName 386 | input.blur() 387 | e.preventDefault() 388 | } 389 | }) 390 | 391 | // show/hide 392 | button.querySelector('.eye').addEventListener('click', e => { 393 | e.stopPropagation() 394 | button.classList.toggle('active') 395 | ipc.send('clock-toggle', button.querySelector('.name').innerText) 396 | }) 397 | 398 | // delete 399 | button.querySelector('.delete').addEventListener('click', e => { 400 | e.stopPropagation() 401 | ipc.send('clock-remove', button.querySelector('.name').innerText) 402 | button.parentNode.removeChild(button) 403 | updateAppHeight() 404 | }) 405 | 406 | $('.clock').appendChild(button) 407 | $('.clock').scrollTop = $('.clock').scrollHeight 408 | 409 | updateTime() 410 | updateAppHeight() 411 | }) 412 | 413 | function runClock() { 414 | let now = new Date() 415 | let tick = (60 - now.getSeconds()) * 1000 - now.getMilliseconds() 416 | 417 | setTimeout(function() { 418 | $('.slider input').value = (new Date().getHours() * 60) + new Date().getMinutes() 419 | sliderRecalc() 420 | updateTime() 421 | runClock() 422 | }, tick) 423 | } 424 | } 425 | 426 | function updateTime() { 427 | 428 | let val = $('.slider input').value 429 | let now = new Date(), then = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0) 430 | let diff = Math.floor((now.getTime() - then.getTime()) / 1000) 431 | let offset = (Math.floor((val * 60) - diff)) * 1000 432 | 433 | $all('.clock button').forEach(item => { 434 | let time = item.querySelector('time') 435 | let tz = clock.getTzTime(time.getAttribute('data-timezone'), offset) 436 | 437 | time.classList.remove('morning', 'evening') 438 | time.classList.add(tz.morning) 439 | time.innerText = tz.time 440 | }) 441 | } 442 | 443 | function sliderRecalc() 444 | { 445 | let el = $('.slider input') 446 | let format = clock.formatTime(el.value * 60 * 1000, true) 447 | let is24H = clock.isTwentyFourHour() == 'on' ? true : false 448 | 449 | $('.slider .now').innerText = format.time 450 | $('.slider .from').style.opacity = el.value < (is24H ? 200 : 250) ? 0 : 0.3 451 | $('.slider .to').style.opacity = el.value > (is24H ? 1080 : 950) ? 0 : 0.3 452 | updateTime() 453 | 454 | let left = el.offsetWidth * (el.value - el.min) / (el.max - el.min) 455 | let ampm_offset = is24H ? 23 : 38 456 | left = el.value < 1260 ? left + 25 : left - ampm_offset 457 | $('.slider .now').style.left = `${left}px` 458 | } 459 | 460 | function updateAppHeight() 461 | { 462 | let appHeight = parseFloat(getComputedStyle($('.app'), null).height.replace('px', '')) 463 | ipc.send('app-height', appHeight) 464 | } 465 | 466 | window.addEventListener('DOMContentLoaded', init) 467 | document.addEventListener('dragover', event => event.preventDefault()) 468 | document.addEventListener('drop', event => event.preventDefault()) 469 | -------------------------------------------------------------------------------- /app/static/about-darkmode.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/static/about.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/static/check-darkmode.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/static/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/static/collapse-darkmode.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/static/collapse.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/static/donate-darkmode.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/static/donate.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/static/exit-darkmode.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/static/exit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/static/eye-closed-darkmode.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/static/eye-closed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/static/eye-open-darkmode.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/static/eye-open.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/static/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarutin/hovrly/2f1f20a89381f0b053413a62371e5a4596ff2398/app/static/icon.icns -------------------------------------------------------------------------------- /app/static/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarutin/hovrly/2f1f20a89381f0b053413a62371e5a4596ff2398/app/static/icon.ico -------------------------------------------------------------------------------- /app/static/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarutin/hovrly/2f1f20a89381f0b053413a62371e5a4596ff2398/app/static/icon.png -------------------------------------------------------------------------------- /app/static/loading-darkmode.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/static/loading.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/static/moon-darkmode.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/static/moon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/static/remove-darkmode.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/static/remove.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/static/search-darkmode.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/static/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/static/style.css: -------------------------------------------------------------------------------- 1 | /* @import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css'); */ 2 | 3 | * { margin:0; padding:0; outline:none; color:#fff; font-weight:400; font-family:-apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Ubuntu, sans-serif; box-sizing:border-box!important; -webkit-box-sizing:border-box!important; } 4 | *, *:before, *:after { box-sizing:inherit; -webkit-box-sizing:inherit; } 5 | html, body { font-size:13px; line-height:100%; -webkit-text-size-adjust:100%; -webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility; } 6 | button, html input[type='button'], input[type='reset'], input[type='submit'] { -webkit-appearance:button; cursor:pointer; } 7 | .btn { font-weight:400; display:flex; flex-direction:row; align-items:center; color:#fff; width:100%; border:none; line-height:100%; background:transparent; text-align:left; padding:10px 20px; font-size:13px; } 8 | .btn:hover { background:#33373c; } 9 | .hidden { display:none!important; } 10 | 11 | .titlebar { -webkit-app-region:drag; -webkit-user-select:none; text-align:center; font-size:80%; font-weight:600; padding:10px; opacity:.7; border-bottom:1px solid #222; } 12 | 13 | .app { background:#1B1E21; } 14 | .app section { border-bottom:1px solid rgba(255, 255, 255, .1); } 15 | .app section:last-of-type { border-bottom:none; } 16 | 17 | .search { position:relative; background:transparent; } 18 | .search input { position:relative; z-index:10; width:100%; background:transparent; font-weight:400; font-size:13px; color:#fff; border:none; padding:10px 20px 10px 40px; background:url('../static/search-darkmode.svg') no-repeat 16px center; background-size:16px 16px; } 19 | .search label { position:absolute; z-index:9; top:2px; right:0; z-index:9; text-align:right; display:block; width:100%; opacity:.5; padding:10px 15px; font-weight:400; font-size:13px; } 20 | 21 | .clock { padding:5px 15px; background:transparent; overflow-y:auto; } 22 | .clock button { display:flex; flex-direction:row; align-items:center; width:100%; position:relative; text-align:left; font-weight:400; border:none; padding:0 5px; margin:10px 0px; background:transparent; font-size:13px; } 23 | .clock button time { margin-left:-5px; margin-right:5px; font-weight:500; text-align:center; width:70px; border-radius:20px; background-position:8px center; background-repeat:no-repeat; background-size:16px 16px; padding:3px 4px 3px 24px; } 24 | .clock.ampm button time { width:90px; } 25 | .clock button time.morning { background-color:#F9C133; color:#000; background-image:url('../static/sun-darkmode.svg'); } 26 | .clock button time.evening { background-color:#2B2E75; color:#fff; background-image:url('../static/moon-darkmode.svg'); } 27 | .clock button .name { border:1px solid transparent; line-height:100%!important; padding:3px 6px; border-radius:4px; cursor:text; max-width:140px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } 28 | .clock button .name.hover { border-color:rgba(255, 255, 255, .15)!important; } 29 | .clock button .name.focus { text-overflow:clip!important; border-color:rgba(255, 255, 255, .15)!important; } 30 | .clock button .delete { display:none; margin-left:5px; width:24px; height:24px; background:url('../static/remove-darkmode.svg') no-repeat center center; background-size:16px 16px; } 31 | .clock button:hover .delete { display:inline-flex; } 32 | .clock button .eye { margin-left:auto; width:24px; height:24px; background:url('../static/eye-closed-darkmode.svg') no-repeat center center; background-size:16px 16px; } 33 | .clock button.active .eye { background-image:url('../static/eye-open-darkmode.svg'); } 34 | 35 | .settings button { padding-left:30px; display:flex; flex-direction:row; align-items:center; } 36 | .settings button:before { content:''; padding-left:8px; min-height:16px; } 37 | .settings button.active:before { content:''; width:16px; height:16px; margin-left:-18px; margin-right:10px; background:url('../static/check-darkmode.svg') no-repeat center center; background-size:16px 16px; display:inline-block; } 38 | 39 | .collapse { border-bottom:none!important; } 40 | .collapse button.toggle { display:block; line-height:0; border-color:#fff; } 41 | .collapse button.toggle:before { transform:rotate(180deg); height:8px; display:block; content:''; background:url('../static/collapse-darkmode.svg') no-repeat center center; background-size:16px 16px; } 42 | 43 | .app.tiny .collapse button.toggle:before { transform:rotate(0deg); } 44 | .app.tiny .settings { display:none; } 45 | .app.tiny .we, .app.tiny .quit { display:none; } 46 | .app.tiny .collapse button.install { background-color:#2662C1; color:#fff; opacity:1; } 47 | .app.tiny .collapse button.install:before { background-image:url('../static/collapse.svg'); } 48 | .app.tiny .collapse button.install:hover { background-color:#3b74cf; } 49 | 50 | /* .update { padding-left:30px; display:flex; flex-direction:row; align-items:center; } */ 51 | .update:before { content:''; width:16px; height:16px; margin-left:-18px; margin-right:10px; background:url('../static/loading-darkmode.svg') no-repeat center center; background-size:16px 16px; display:inline-block; } 52 | .update.loading:before { animation: spinner 1.2s linear infinite; } 53 | .update.install { background-color:#2662C1; color:#fff; } 54 | .update.install:hover { background-color:#3b74cf; } 55 | .update.install .version { opacity:.7; } 56 | .update .version { font-size:10px; margin-left:auto; } 57 | 58 | .settings button, .update, .about, .support, .exit { padding-left:30px; } 59 | .about:before { content:''; width:16px; height:16px; margin-left:-18px; margin-right:10px; background:url('../static/about-darkmode.svg') no-repeat center center; background-size:16px 16px; display:inline-block; } 60 | .support:before { content:''; width:16px; height:16px; margin-left:-18px; margin-right:10px; background:url('../static/donate-darkmode.svg') no-repeat center center; background-size:16px 16px; display:inline-block; } 61 | .exit:before { content:''; width:16px; height:16px; margin-left:-18px; margin-right:10px; background:url('../static/exit-darkmode.svg') no-repeat center center; background-size:16px 16px; display:inline-block; } 62 | 63 | .slider { position:relative; background:transparent; display:block; height:38px; padding:0 15px; } 64 | .slider input { position:relative; z-index:30; background:transparent; width:100%; height:37px; -webkit-appearance:none; appearance:none; } 65 | .slider input::-webkit-slider-thumb { width:1px; height:37px; background:#2278FF; cursor:pointer; -webkit-appearance:none; appearance:none; } 66 | .slider .now { position:absolute; z-index:28; top:13px; left:50%; color:#fff; font-size:10px; } 67 | .slider .from, .slider .to { position:absolute; z-index:29; top:13px; color:#fff; opacity:.3; transition:all 0.2s linear 0s; } 68 | .slider .from { font-size:10px; left:15px; } 69 | .slider .to { font-size:10px; right:15px; } 70 | 71 | .gray { opacity:.3; } 72 | .clearfix:after { content:''; display:table; clear:both; } 73 | a, form input[type='submit'], button { transition:all 0.1s linear 0s; } 74 | b, strong { font-weight:600; } 75 | ::selection { background:#2278FF; } 76 | 77 | .light * { color:#000; } 78 | .light { background:#fff; } 79 | .light section { border-color:#f7f7f7; } 80 | .light .search { background:#fff; border-color:#F2F2F2; } 81 | .light .search input { color:#000; background-image:url('../static/search.svg'); } 82 | .light .search label { color:#000; } 83 | .light .btn { color:#000; } 84 | .light .btn:hover { background:#f7f7f7; color:#000; } 85 | .light .clock { background:#fff; } 86 | .light .clock button { border-color:#000; color:#000; } 87 | .light .clock button .delete { background-image:url('../static/remove.svg'); } 88 | .light .clock button .eye { background-image:url('../static/eye-closed.svg'); } 89 | .light .clock button.active .eye { background-image:url('../static/eye-open.svg'); } 90 | .light .clock button .name.focus { border-color:rgba(255, 255, 255, .15); } 91 | .light .clock button time.morning { background-image:url('../static/sun.svg'); } 92 | .light .clock button time.evening { background-image:url('../static/moon.svg'); } 93 | .light .clock button .name.hover { border-color:rgba(0, 0, 0, .15)!important; } 94 | .light .clock button .name.focus { border-color:rgba(0, 0, 0, .15)!important; } 95 | .light .slider { background-color:#fff; border-color:#F2F2F2; } 96 | .light .slider .now, .light .slider .from, .light .slider .to { color:#000; } 97 | .light .settings button.active:before { background-image:url('../static/check.svg'); } 98 | .light .update:before { background-image:url('../static/loading.svg'); } 99 | .light .update.install:before { background-image:url('../static/loading-darkmode.svg'); } 100 | .light .update.install { background-color:#2662C1; } 101 | .light .update.install * { color:#fff; } 102 | .light .update.install:hover { background-color:#3b74cf; } 103 | .light .about:before { background-image:url('../static/about.svg'); } 104 | .light .support:before { background-image:url('../static/donate.svg'); } 105 | .light .exit:before { background-image:url('../static/exit.svg'); } 106 | .light .collapse button.toggle:before { background-image:url('../static/collapse.svg'); } 107 | .light .app.tiny .collapse button.install:before { background-image:url('../static/collapse-darkmode.svg')!important; } 108 | 109 | 110 | @keyframes spinner { 111 | 0% { transform: rotate(0deg); } 112 | 100% { transform: rotate(-360deg); } 113 | } 114 | -------------------------------------------------------------------------------- /app/static/sun-darkmode.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/static/sun.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/static/tray-darkmodeTemplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarutin/hovrly/2f1f20a89381f0b053413a62371e5a4596ff2398/app/static/tray-darkmodeTemplate.png -------------------------------------------------------------------------------- /app/static/tray-darkmodeTemplate@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarutin/hovrly/2f1f20a89381f0b053413a62371e5a4596ff2398/app/static/tray-darkmodeTemplate@2x.png -------------------------------------------------------------------------------- /app/static/tray-darkmodeTemplate@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarutin/hovrly/2f1f20a89381f0b053413a62371e5a4596ff2398/app/static/tray-darkmodeTemplate@3x.png -------------------------------------------------------------------------------- /app/static/tray-darkmodeTemplate@4x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarutin/hovrly/2f1f20a89381f0b053413a62371e5a4596ff2398/app/static/tray-darkmodeTemplate@4x.png -------------------------------------------------------------------------------- /app/static/tray-zeroTemplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarutin/hovrly/2f1f20a89381f0b053413a62371e5a4596ff2398/app/static/tray-zeroTemplate.png -------------------------------------------------------------------------------- /app/static/trayTemplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarutin/hovrly/2f1f20a89381f0b053413a62371e5a4596ff2398/app/static/trayTemplate.png -------------------------------------------------------------------------------- /app/static/trayTemplate@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarutin/hovrly/2f1f20a89381f0b053413a62371e5a4596ff2398/app/static/trayTemplate@2x.png -------------------------------------------------------------------------------- /app/static/trayTemplate@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarutin/hovrly/2f1f20a89381f0b053413a62371e5a4596ff2398/app/static/trayTemplate@3x.png -------------------------------------------------------------------------------- /app/static/trayTemplate@4x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarutin/hovrly/2f1f20a89381f0b053413a62371e5a4596ff2398/app/static/trayTemplate@4x.png -------------------------------------------------------------------------------- /app/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | APP 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 |
16 | 17 | 18 | 19 | 20 |
21 |
22 |
23 |
24 | 25 |
26 |
27 | 28 | 29 | 30 | 31 | 35 |
36 |
37 | 38 | 39 |
40 |
41 | 42 |
43 | 44 | 45 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /app/tray.js: -------------------------------------------------------------------------------- 1 | module.exports = { init, getBounds, setHighlightMode, setTitle, update } 2 | 3 | const path = require('path') 4 | const electron = require('electron') 5 | const platform = require('os').platform() 6 | const config = require('./config') 7 | const settings = require('electron-settings') 8 | 9 | const app = electron.app 10 | const Menu = electron.Menu 11 | const Tray = electron.Tray 12 | const nativeImage = electron.nativeImage 13 | const nativeTheme = electron.nativeTheme 14 | const window = require('./window') 15 | 16 | var tray = null 17 | 18 | function init() { 19 | console.log('tray init') 20 | 21 | tray = new Tray(nativeImage.createFromPath(getIcon())) 22 | 23 | update() 24 | 25 | tray.on('click', window.toggle) 26 | tray.on('double-click', window.toggle) 27 | 28 | tray.setToolTip(config.APP_NAME) 29 | } 30 | 31 | function update() 32 | { 33 | tray.setImage(getIcon()) 34 | 35 | nativeTheme.on('updated', () => { 36 | tray.setImage(getIcon()) 37 | }) 38 | } 39 | 40 | function getIcon() 41 | { 42 | let clocks = settings.getSync('clocks') 43 | let isVisibleClock = false 44 | 45 | for (let i in clocks) { 46 | if (clocks[i].tray) { 47 | isVisibleClock = true 48 | break 49 | } 50 | } 51 | 52 | if(isVisibleClock) { 53 | return config.TRAY_ICON_ZERO 54 | } 55 | else { 56 | return platform == 'win32' 57 | ? config.TRAY_ICON_WIN 58 | : (nativeTheme.shouldUseDarkColors 59 | ? config.TRAY_ICON_MAC_DARKMODE 60 | : config.TRAY_ICON_MAC) 61 | } 62 | } 63 | 64 | function setTitle(title) { 65 | tray.setTitle(title) 66 | } 67 | 68 | function setHighlightMode(mode) { 69 | return tray.setHighlightMode(mode) 70 | } 71 | 72 | function getBounds() { 73 | return tray.getBounds() 74 | } 75 | -------------------------------------------------------------------------------- /app/updater.js: -------------------------------------------------------------------------------- 1 | module.exports = { init, auto } 2 | 3 | const electron = require('electron') 4 | const config = require('./config') 5 | const notice = require('./notice') 6 | const window = require('./window') 7 | const ipc = electron.ipcMain 8 | const app = electron.app 9 | const BrowserWindow = electron.BrowserWindow 10 | const autoUpdater = electron.autoUpdater // sign: https://github.com/electron/electron/issues/7476 11 | 12 | var isDev = process.env.DEV ? (process.env.DEV.trim() == 'true') : false 13 | 14 | function init() { 15 | console.log('updater init') 16 | 17 | var win = window.getWin() 18 | 19 | autoUpdater.setFeedURL({ 20 | url: `${config.UPDATER_CHECK_URL}/update/${process.platform}/${app.getVersion()}` 21 | }); 22 | 23 | ipc.on('update-check', function() { 24 | if(isDev) { 25 | win.webContents.send('update-finish', 'dev-mode') 26 | return 27 | } 28 | 29 | autoUpdater.checkForUpdates() 30 | }) 31 | 32 | ipc.on('update-install', function() { 33 | autoUpdater.quitAndInstall() 34 | setTimeout(app.quit, 1000) 35 | }) 36 | 37 | autoUpdater.on('update-downloaded', (event, notes, name, date, url) => { 38 | if(process.platform == 'darwin' && !win.isVisible()) { 39 | app.dock.show() 40 | app.dock.bounce() 41 | app.dock.setBadge(' ') 42 | 43 | // temporary fix for Big Sur 44 | autoUpdater.quitAndInstall() 45 | } 46 | 47 | if(!win.isVisible()) { 48 | notice.send(`Update ready to install`, () => { 49 | win.show() 50 | }) 51 | } 52 | 53 | win.webContents.send('update-finish', 'downloaded') 54 | }) 55 | 56 | autoUpdater.on('checking-for-update', () => { 57 | if(win.isVisible()) { 58 | win.webContents.send('update-finish', 'checking') 59 | } 60 | }) 61 | 62 | autoUpdater.on('update-available', () => { 63 | win.webContents.send('update-finish', 'available') 64 | }) 65 | 66 | autoUpdater.on('update-not-available', () => { 67 | if(win.isVisible()) { 68 | win.webContents.send('update-finish', 'not-available') 69 | } 70 | }) 71 | 72 | autoUpdater.on('error', message => { 73 | if(isDev) { 74 | console.log(message) 75 | } 76 | }) 77 | } 78 | 79 | function auto() { 80 | if(isDev) return 81 | 82 | setInterval(autoUpdater.checkForUpdates, config.UPDATER_CHECK_TIME) 83 | } 84 | -------------------------------------------------------------------------------- /app/window.js: -------------------------------------------------------------------------------- 1 | module.exports = { init, toggle, show, hide, getWin } 2 | 3 | const electron = require('electron') 4 | const config = require('./config') 5 | const app = electron.app 6 | const BrowserWindow = electron.BrowserWindow 7 | const ipc = electron.ipcMain 8 | const tray = require('./tray') 9 | const Positioner = require('electron-positioner') 10 | 11 | var isDev = process.env.DEV ? (process.env.DEV.trim() == 'true') : false 12 | var win = null 13 | var positioner 14 | 15 | function init() { 16 | console.log('window init') 17 | 18 | win = new BrowserWindow({ 19 | width: config.WIN_WIDTH, 20 | height: 400, 21 | maxHeight: 670, 22 | frame: false, 23 | transparent: true, 24 | titleBarStyle: 'default', 25 | show: false, 26 | fullscreenable: false, 27 | resizable: false, 28 | movable: false, 29 | icon: config.DOCK_ICON, 30 | skipTaskbar: true, 31 | webPreferences: { 32 | preload: `${__dirname}/preload.js`, 33 | nodeIntegration: true, 34 | enableRemoteModule: true, 35 | contextIsolation: false 36 | } 37 | }) 38 | 39 | positioner = new Positioner(win) 40 | 41 | win.webContents.loadURL(`file://${__dirname}/templates/index.html`) 42 | win.setVisibleOnAllWorkspaces(true) 43 | if(isDev && config.DEV_TOOLS) win.webContents.openDevTools(); 44 | 45 | win.once('ready-to-show', () => { 46 | win.webContents.send('app-height-get') 47 | 48 | ipc.on('app-height', (event, height) => { 49 | win.setSize(config.WIN_WIDTH, height) 50 | }) 51 | }) 52 | 53 | win.on('blur', hide) 54 | 55 | win.on('close', (event) => { 56 | if (app.quitting) { 57 | win = null 58 | } else { 59 | event.preventDefault() 60 | hide() 61 | } 62 | }) 63 | } 64 | 65 | function getWin() { 66 | return win 67 | } 68 | 69 | function show() { 70 | let position = positioner.calculate('trayLeft', tray.getBounds()) 71 | win.setPosition(position.x - 7, position.y + 10, false) 72 | win.show() 73 | } 74 | 75 | function hide() { 76 | win.hide() 77 | } 78 | 79 | function toggle() { 80 | win.isVisible() ? hide() : show() 81 | } 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hovrly", 3 | "productName": "Hovrly", 4 | "version": "2.4.5", 5 | "description": "Best partner for disctributed teams", 6 | "main": "app/index.js", 7 | "scripts": { 8 | "start": "DEV=true electron .", 9 | "build": "electron-builder build --mac --universal --publish never", 10 | "publish": "electron-builder build --mac --universal --publish always", 11 | "sign-dev-electron-app": "codesign --deep --force --verbose --sign - node_modules/electron/dist/Electron.app", 12 | "postinstall": "npm run sign-dev-electron-app" 13 | }, 14 | "author": "Treasy, OU", 15 | "private": true, 16 | "homepage": "https://github.com/tarutin/hovrly", 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/tarutin/hovrly.git" 20 | }, 21 | "bugs": { 22 | "url": "https://github.com/tarutin/hovrly/issues" 23 | }, 24 | "keywords": [ 25 | "desktop", 26 | "electron", 27 | "electron-app", 28 | "clock", 29 | "multi clock", 30 | "hovrly" 31 | ], 32 | "license": "MIT", 33 | "devDependencies": { 34 | "electron": "13.1.0", 35 | "electron-builder": "^23.6.0" 36 | }, 37 | "dependencies": { 38 | "auto-launch": "^5.0.5", 39 | "electron-notarize": "^1.0.0", 40 | "electron-positioner": "^4.1.0", 41 | "electron-settings": "^4.0.2", 42 | "mysql2": "^2.2.5", 43 | "sortablejs": "^1.13.0" 44 | } 45 | } 46 | --------------------------------------------------------------------------------