@ -0,0 +1,53 @@ |
|||
.. image:: https://img.shields.io/badge/license-AGPL--3-blue.svg |
|||
:target: https://www.gnu.org/licenses/agpl-3.0-standalone.html |
|||
:alt: License: AGPL-3 |
|||
|
|||
Systray World Clock |
|||
=================== |
|||
Module enables user to add world clocks of selected timezones in |
|||
the systray dropdown. |
|||
|
|||
Configuration |
|||
============= |
|||
The desired timezone and title for the clocks can be configured in |
|||
the settings page. |
|||
|
|||
Company |
|||
------- |
|||
* `Cybrosys Techno Solutions <https://cybrosys.com/>`__ |
|||
|
|||
License |
|||
------- |
|||
GNU AFFERO GENERAL PUBLIC LICENSE, Version 3 (AGPLv3) |
|||
(http://www.gnu.org/licenses/agpl-3.0-standalone.html) |
|||
|
|||
Credits |
|||
------- |
|||
* Developer: (V18) Mufeeda Shirin, Nivedhya T, |
|||
(V17)Busthana Shirin, |
|||
(V15) Abhijith PG, |
|||
Contact: odoo@cybrosys.com |
|||
|
|||
Contacts |
|||
-------- |
|||
* Mail Contact : odoo@cybrosys.com |
|||
* Website : https://cybrosys.com |
|||
|
|||
Bug Tracker |
|||
----------- |
|||
Bugs are tracked on GitHub Issues. In case of trouble, please check there if |
|||
your issue has already been reported. |
|||
|
|||
Maintainer |
|||
========== |
|||
.. image:: https://cybrosys.com/images/logo.png |
|||
:target: https://cybrosys.com |
|||
|
|||
This module is maintained by Cybrosys Technologies. |
|||
|
|||
For support and more information, please visit |
|||
`Our Website <https://cybrosys.com/>`__ |
|||
|
|||
Further information |
|||
=================== |
|||
HTML Description: `<static/description/index.html>`__ |
@ -0,0 +1,22 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Cybrosys Techno Solutions (odoo@cybrosys.com) |
|||
# |
|||
# You can modify it under the terms of the GNU AFFERO |
|||
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE |
|||
# (AGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
from . import models |
@ -0,0 +1,52 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Cybrosys Techno Solutions (odoo@cybrosys.com) |
|||
# |
|||
# You can modify it under the terms of the GNU AFFERO |
|||
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE |
|||
# (AGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
{ |
|||
'name': "Systray World Clock", |
|||
'version': '18.0.1.0.0', |
|||
'category': 'Productivity', |
|||
'summary': """Keep track of the time in different countries around the |
|||
world.""", |
|||
'description': """This module adds a world clock in the systray which |
|||
displays the time in different countries around the world. |
|||
The time zone can be customized in the settings""", |
|||
'author': "Cybrosys Techno Solutions", |
|||
'company': 'Cybrosys Techno Solutions', |
|||
'maintainer': 'Cybrosys Techno Solutions', |
|||
'website': "https://www.cybrosys.com", |
|||
'depends': ['base', 'web'], |
|||
'data': [ |
|||
'security/ir.model.access.csv', |
|||
'views/systray_world_clock_config_views.xml', |
|||
], |
|||
'assets': { |
|||
'web.assets_backend': [ |
|||
'systray_world_clock/static/src/js/SystrayWorldClock.js', |
|||
'systray_world_clock/static/src/xml/systray_world_clock.xml', |
|||
'systray_world_clock/static/src/scss/systray_world_clock.scss' |
|||
], |
|||
}, |
|||
'images': ['static/description/banner.png'], |
|||
'license': 'AGPL-3', |
|||
'installable': True, |
|||
'auto_install': False, |
|||
'application': False, |
|||
} |
@ -0,0 +1,6 @@ |
|||
## Module <systray_world_clock> |
|||
|
|||
#### 15.07.2025 |
|||
#### Version 18.0.1.0.0 |
|||
##### ADD |
|||
- Initial commit for Systray World Clock |
@ -0,0 +1,22 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Cybrosys Techno Solutions (odoo@cybrosys.com) |
|||
# |
|||
# You can modify it under the terms of the GNU AFFERO |
|||
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE |
|||
# (AGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
from . import systray_world_clock_config |
@ -0,0 +1,52 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Cybrosys Techno Solutions (odoo@cybrosys.com) |
|||
# |
|||
# You can modify it under the terms of the GNU AFFERO |
|||
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE |
|||
# (AGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
import pytz |
|||
from datetime import datetime |
|||
from odoo import api, fields, models |
|||
from odoo.addons.base.models.res_partner import _tz_get |
|||
|
|||
|
|||
class SystrayWorldClockConfig(models.Model): |
|||
"""A model representing the configuration of a world clock in the systray. |
|||
""" |
|||
_name = 'systray.world.clock.config' |
|||
_description = 'Systray World Clock Configuration' |
|||
|
|||
name = fields.Char(string='Location', required=True, |
|||
help='Location associated with this clock.') |
|||
tz = fields.Selection(_tz_get, required=True, string='Timezone', |
|||
help='Timezone of the clock.') |
|||
offset = fields.Float(string="Offset", |
|||
help='The time difference between the timezone and ' |
|||
'UTC, in hours.') |
|||
|
|||
@api.onchange('tz') |
|||
def _onchange_tz(self): |
|||
"""Calculate the time offset between the selected timezone and UTC. |
|||
This method is called automatically whenever the timezone field is |
|||
changed. |
|||
""" |
|||
if self.tz: |
|||
utc_dt = pytz.utc.localize(datetime.utcnow()) |
|||
tz = pytz.timezone(self.tz) |
|||
local_dt = utc_dt.astimezone(tz) |
|||
self.offset = local_dt.utcoffset().total_seconds() / 3600 |
|
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 210 KiB |
After Width: | Height: | Size: 209 KiB |
After Width: | Height: | Size: 109 KiB |
After Width: | Height: | Size: 495 B |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 624 B |
After Width: | Height: | Size: 136 KiB |
After Width: | Height: | Size: 214 KiB |
After Width: | Height: | Size: 36 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 310 B |
After Width: | Height: | Size: 929 B |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 542 B |
After Width: | Height: | Size: 576 B |
After Width: | Height: | Size: 733 B |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 4.0 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 150 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 911 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 600 B |
After Width: | Height: | Size: 673 B |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 462 B |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 926 B |
After Width: | Height: | Size: 9.0 KiB |
After Width: | Height: | Size: 23 KiB |
After Width: | Height: | Size: 7.0 KiB |
After Width: | Height: | Size: 878 B |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 653 B |
After Width: | Height: | Size: 800 B |
After Width: | Height: | Size: 905 B |
After Width: | Height: | Size: 189 KiB |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 839 B |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 5.9 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 23 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 427 B |
After Width: | Height: | Size: 627 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 988 B |
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 875 B |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 790 KiB |
After Width: | Height: | Size: 89 KiB |
After Width: | Height: | Size: 746 KiB |
After Width: | Height: | Size: 716 KiB |
After Width: | Height: | Size: 742 KiB |
After Width: | Height: | Size: 87 KiB |
After Width: | Height: | Size: 448 KiB |
After Width: | Height: | Size: 454 KiB |
After Width: | Height: | Size: 35 KiB |
After Width: | Height: | Size: 485 KiB |
After Width: | Height: | Size: 503 KiB |
After Width: | Height: | Size: 91 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 33 KiB |
After Width: | Height: | Size: 50 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 1.4 KiB |
@ -0,0 +1,136 @@ |
|||
/** @odoo-module **/ |
|||
import { Component, useState, onMounted,useRef } from "@odoo/owl"; |
|||
import { useService } from "@web/core/utils/hooks"; |
|||
import { Dropdown } from "@web/core/dropdown/dropdown"; |
|||
import { registry } from "@web/core/registry"; |
|||
|
|||
export class Analogue extends Component { |
|||
static template = "systray_world_clock.AnalogueInterface"; |
|||
|
|||
setup() { |
|||
super.setup(); |
|||
this.hoursHandRef = useRef("hoursHand"); |
|||
this.minutesHandRef = useRef("minutesHand"); |
|||
this.secondsHandRef = useRef("secondsHand"); |
|||
this.clockElementRef = useRef("clockElement"); |
|||
onMounted(() => { |
|||
this.renderClock(); |
|||
this.interval = setInterval(() => { |
|||
this.renderClock(); |
|||
}, 1000); |
|||
}); |
|||
} |
|||
|
|||
calcTime(offset) { |
|||
const d = new Date(); |
|||
const utc = d.getTime() + (d.getTimezoneOffset() * 60000); |
|||
const nd = new Date(utc + (3600000 * offset)); |
|||
const options = { |
|||
year: 'numeric', |
|||
month: 'numeric', |
|||
day: 'numeric', |
|||
hour: 'numeric', |
|||
minute: 'numeric', |
|||
hour12: true |
|||
}; |
|||
return [nd, nd.toLocaleString('en-US', options)]; |
|||
} |
|||
|
|||
renderClock() { |
|||
const [nd, localestring] = this.calcTime(this.props.offset); |
|||
const h = ((nd.getHours() % 12) + nd.getMinutes() / 59) * 30; |
|||
const m = nd.getMinutes() * 6; |
|||
const s = nd.getSeconds() * 6; |
|||
|
|||
// Accessing the refs safely
|
|||
if (this.hoursHandRef.el) this.hoursHandRef.el.style.transform = `rotate(${h}deg)`; |
|||
if (this.minutesHandRef.el) this.minutesHandRef.el.style.transform = `rotate(${m}deg)`; |
|||
if (this.secondsHandRef.el) this.secondsHandRef.el.style.transform = `rotate(${s}deg)`; |
|||
|
|||
const clockElement = this.clockElementRef.el; |
|||
if (clockElement) { |
|||
if (18 <= nd.getHours() || nd.getHours() < 6) { |
|||
clockElement.classList.add('night-clock'); |
|||
} else { |
|||
clockElement.classList.remove('night-clock'); |
|||
} |
|||
} |
|||
} |
|||
|
|||
willUnmount() { |
|||
clearInterval(this.interval); |
|||
} |
|||
} |
|||
|
|||
export class WorldClock extends Component { |
|||
static components = { Dropdown, Analogue }; |
|||
static template = "systray_world_clock.Systray_clock"; |
|||
|
|||
setup() { |
|||
super.setup(); |
|||
this.orm = useService("orm"); |
|||
this.action = useService("action"); |
|||
this.state = useState({ |
|||
isOpen: false, |
|||
notes: [], |
|||
}); |
|||
|
|||
this.loadClocks(); |
|||
} |
|||
|
|||
async loadClocks() { |
|||
const fields = ['name', 'tz', 'offset']; |
|||
const result = await this.orm.call("systray.world.clock.config", "search_read", [], { fields }); |
|||
|
|||
if (result.length > 0) { |
|||
this.state.isOpen = true; |
|||
const data = result.map(clock => { |
|||
const offsetStr = clock.offset.toString().replace('.', '_'); |
|||
const currentClockClass = 'clock' + offsetStr + clock.id; |
|||
const [nd, localestring] = this.calcTime(clock.offset); |
|||
return { |
|||
id: clock.id, |
|||
name: clock.name, |
|||
offset: clock.offset, |
|||
tz: clock.tz, |
|||
currentClockClass, |
|||
nd, |
|||
localestring, |
|||
}; |
|||
}); |
|||
this.state.notes = data; |
|||
} |
|||
} |
|||
|
|||
calcTime(offset) { |
|||
const d = new Date(); |
|||
const utc = d.getTime() + (d.getTimezoneOffset() * 60000); |
|||
const nd = new Date(utc + (3600000 * offset)); |
|||
const options = { |
|||
year: 'numeric', |
|||
month: 'numeric', |
|||
day: 'numeric', |
|||
hour: 'numeric', |
|||
minute: 'numeric', |
|||
hour12: true, |
|||
}; |
|||
return [nd, nd.toLocaleString('en-US', options)]; |
|||
} |
|||
|
|||
onClickSettings(ev) { |
|||
ev.stopPropagation(); |
|||
this.action.doAction({ |
|||
type: 'ir.actions.act_window', |
|||
name: 'Clock Settings', |
|||
res_model: 'systray.world.clock.config', |
|||
view_mode: 'list,form', |
|||
target: 'current', |
|||
views: [[false, 'list'], [false, 'form']], |
|||
}); |
|||
} |
|||
} |
|||
|
|||
export const systrayItem = { |
|||
Component: WorldClock, |
|||
}; |
|||
registry.category("systray").add("WorldClock", systrayItem, { sequence: 0 }); |
@ -0,0 +1,143 @@ |
|||
.systray_clock_dropdown { |
|||
padding: 2em; |
|||
width: 1100px; |
|||
} |
|||
.settings_tz { |
|||
position: relative; |
|||
top: -0.5rem; |
|||
color: blueviolet; |
|||
background: lavender; |
|||
border: aliceblue; |
|||
float:right; |
|||
} |
|||
|
|||
#clocks_container { |
|||
display: flex; |
|||
justify-content: space-evenly; |
|||
flex-wrap: wrap; |
|||
.time-info { |
|||
padding: 10px 30px; |
|||
margin: 2em 0; |
|||
min-height: 50px; |
|||
border: 1px solid #dddddd; |
|||
border-radius: 10px; |
|||
background-color: transparent; |
|||
} |
|||
} |
|||
.clocks_container:not(:last-child) { |
|||
padding: 10px; |
|||
min-height: 50px; |
|||
border-bottom: 1px solid #9DA1AA; |
|||
background-color: transparent; |
|||
} |
|||
.clocks_container > p { |
|||
font-size: 20px; |
|||
} |
|||
|
|||
//button |
|||
:root { |
|||
--primary-light: #8abdff; |
|||
--primary: #6d5dfc; |
|||
--primary-dark: #5b0eeb; |
|||
--white: #FFFFFF; |
|||
--greyLight-1: #E4EBF5; |
|||
--greyLight-2: #c8d0e7; |
|||
--greyLight-3: #bec8e4; |
|||
--greyDark: #9baacf; |
|||
} |
|||
$shadow: .3rem .3rem .6rem var(--greyLight-2), |
|||
-.2rem -.2rem .5rem var(--white); |
|||
$inner-shadow: inset .2rem .2rem .5rem var(--greyLight-2), |
|||
inset -.2rem -.2rem .5rem var(--white); |
|||
|
|||
/* CLOCK */ |
|||
.clock { |
|||
grid-column: 2 / 3; |
|||
grid-row: 1 / 3; |
|||
width: 12rem; |
|||
height: 12rem; |
|||
justify-self: center; |
|||
box-shadow: $shadow; |
|||
border-radius: 50%; |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
position: relative; |
|||
.hand { |
|||
position: absolute; |
|||
transform-origin: bottom; |
|||
bottom: 6rem; |
|||
border-radius: .2rem; |
|||
z-index: 200; |
|||
} |
|||
.hours { |
|||
width: .4rem; |
|||
height: 3.2rem; |
|||
background: var(--greyLight-3); |
|||
|
|||
} |
|||
.minutes { |
|||
width: .4rem; |
|||
height: 4.6rem; |
|||
background: var(--greyDark); |
|||
} |
|||
.seconds { |
|||
width: .2rem; |
|||
height: 5.2rem; |
|||
background: var(--primary); |
|||
} |
|||
.point { |
|||
position: absolute; |
|||
width: .8rem; |
|||
height: .8rem; |
|||
border-radius: 50%; |
|||
background: var(--primary); |
|||
z-index: 300; |
|||
} |
|||
.in-border { |
|||
width: 95%; |
|||
height: 95%; |
|||
border-radius: 50%; |
|||
display: flex; |
|||
position: relative; |
|||
box-shadow: $inner-shadow; |
|||
.center { |
|||
width: 60%; |
|||
height: 60%; |
|||
position: absolute; |
|||
box-shadow: inset 1px 1px 1px var(--greyLight-2), |
|||
inset -1px -1px 1px var(--white); |
|||
border-radius: 50%; |
|||
top: 20%; |
|||
left: 20%; |
|||
filter: blur(1px); |
|||
} |
|||
div.digit { |
|||
position: absolute; |
|||
inset: 1px; |
|||
text-align: center; |
|||
transform: rotate(calc(var(--i) * (360deg / 12))); |
|||
span { |
|||
display: inline-block; |
|||
font-size: 80%; |
|||
font-weight: 600; |
|||
transform: rotate(calc(var(--i) * (-360deg / 12))); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
.night-clock { |
|||
background-color: black; |
|||
color: white; |
|||
} |
|||
.no_clocks_span { |
|||
font-size: 16px; |
|||
font-weight: 500; |
|||
color: #714b67; |
|||
text-align: center; |
|||
background-color: #f2f2f2; |
|||
padding: 10px; |
|||
border-radius: 5px; |
|||
display: inline-block; |
|||
margin-top: -17px; |
|||
} |