├── .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 |
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 |
8 |
--------------------------------------------------------------------------------
/app/static/about.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/app/static/check-darkmode.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/app/static/check.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/app/static/collapse-darkmode.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/app/static/collapse.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/app/static/donate-darkmode.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/app/static/donate.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/app/static/exit-darkmode.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/app/static/exit.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/app/static/eye-closed-darkmode.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/app/static/eye-closed.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/app/static/eye-open-darkmode.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/app/static/eye-open.svg:
--------------------------------------------------------------------------------
1 |
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 |
7 |
--------------------------------------------------------------------------------
/app/static/loading.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/app/static/moon-darkmode.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/app/static/moon.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/app/static/remove-darkmode.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/app/static/remove.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/app/static/search-darkmode.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/app/static/search.svg:
--------------------------------------------------------------------------------
1 |
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 |
12 |
--------------------------------------------------------------------------------
/app/static/sun.svg:
--------------------------------------------------------------------------------
1 |
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 |