├── .projectile
├── .gitignore
├── .img
└── azan.png
├── src
├── stylesheet.css
├── prefs_keys.js
├── metadata.json
├── schemas
│ └── org.gnome.shell.extensions.azan.gschema.xml
├── HijriCalendarKuwaiti.js
├── convenience.js
├── prefs.js
├── extension.js
└── PrayTimes.js
├── README.md
└── Makefile
/.projectile:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | gschemas.compiled
2 | build/
--------------------------------------------------------------------------------
/.img/azan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/faissaloo/azan-gnome-shell-extension/HEAD/.img/azan.png
--------------------------------------------------------------------------------
/src/stylesheet.css:
--------------------------------------------------------------------------------
1 | .azan-panel {
2 | font-family: Lotus, Arial, sans-serif;
3 | font-size: 14px;
4 | font-weight: bold;
5 | direction: rtl;
6 | }
7 |
8 | .prefs_s_action:hover {
9 | color: white;
10 | border: none;
11 | }
12 |
--------------------------------------------------------------------------------
/src/prefs_keys.js:
--------------------------------------------------------------------------------
1 | var AUTO_LOCATION = 'auto-location';
2 | var CALCULATION_METHOD = 'calculation-method';
3 | var MADHAB = 'madhab';
4 | var LATITUDE = 'latitude';
5 | var LONGITUDE = 'longitude';
6 | var TIMEZONE = 'timezone';
7 | var TIME_FORMAT_12 = 'time-format-12';
8 | var CONCISE_LIST = 'concise-list';
9 | var HIJRI_DATE_ADJUSTMENT = 'hijri-date-adjustment';
10 |
--------------------------------------------------------------------------------
/src/metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Azan is an Islamic prayer times extension for Gnome Shell based on the extension by Fahrinh.\n\nFeatures\n- List compulsory prayer times\n Optionally display Imsak, Sunrise, Sunset and Midnight\n- Show remaining time for the upcoming prayer.\n- Show current date in Hijri calendar.\n- Display a notification when it's time for prayer.\n- Automatic Geoclue2 location detection\n- Show times in 24 hour and 12 hour formats\n- Hijri date adjusment\n- Moon status icon",
3 | "name": "Azan Islamic Prayer Times",
4 | "settings-schema": "org.gnome.shell.extensions.azan",
5 | "shell-version": [
6 | "3.36.1",
7 | "3.36.9",
8 | "3.38.0",
9 | "40",
10 | "41",
11 | "42",
12 | "43"
13 | ],
14 | "url": "https://github.com/faissaloo/azan-gnome-shell-extension",
15 | "uuid": "azan@faissal.bensefia.id",
16 | "version": 11
17 | }
18 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### Description
2 |
3 | Azan is an Islamic prayer times extension for Gnome Shell.
4 |
5 | 
6 |
7 | ### Features
8 |
9 | - List 5 prayer times
10 | - Optionally display Imsak, Sunrise, Sunset, Midnight
11 | - Show remaining time for the upcoming prayer
12 | - Show current date in Hijri calendar
13 | - Display a notification when it's time for prayer
14 | - Automatic location detection
15 | - Display times in either 24 hour or 12 hour format
16 | - Adjust the Hijri date
17 |
18 | ### Installation
19 |
20 | 1. clone this repository
21 | 2. run `make && make install`
22 |
23 | ### Changelog
24 |
25 | - 01 : initial upload
26 | - 02 : Add automatic location detection & bugfixes
27 | - 03 : 12 hour times and optional hiding of non-prayer times
28 | - 04 : Add support for Hijri date adjustment
29 | - 05 : Add support for Gnome 40+
30 | - 06 : Bump version Gnome 42
31 | - 07 : Added support for Gnome 3.36+
32 | - 10 : Bugfixes
33 | - 11 : MUI calculation method and addition of disclaimer
34 |
35 | ### License
36 |
37 | Licensed under the GNU General Public License, version 3
38 |
39 | ### Third-Party Assets & Components
40 |
41 | - [PrayTimes.js](http://praytimes.org/manual/)
42 | - [HijriCalendar-Kuwaiti.js](http://www.al-habib.info/islamic-calendar/hijricalendar-kuwaiti.js)
43 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | #=============================================================================
2 | UUID=$(shell cat src/metadata.json | python3 -c "import json,sys;obj=json.load(sys.stdin);print(obj['uuid']);")
3 | SRCDIR=src
4 | BUILDDIR=build
5 | FILES=metadata.json *.js stylesheet.css schemas
6 | MKFILE_PATH := $(lastword $(MAKEFILE_LIST))
7 | MKFILE_DIR := $(dir $(MKFILE_PATH))
8 | ABS_MKFILE_PATH := $(abspath $(MKFILE_PATH))
9 | ABS_MKFILE_DIR := $(abspath $(MKFILE_DIR))
10 | ABS_BUILDDIR=$(ABS_MKFILE_DIR)/$(BUILDDIR)
11 | INSTALL_PATH=~/.local/share/gnome-shell/extensions
12 | #=============================================================================
13 | default_target: all
14 | .PHONY: clean all zip install reloadGnome
15 |
16 | clean:
17 | rm -rf $(BUILDDIR)
18 |
19 | # compile the schemas
20 | all: clean
21 | mkdir -p $(BUILDDIR)/$(UUID)
22 | cp -r src/* $(BUILDDIR)/$(UUID)
23 | @if [ -d $(BUILDDIR)/$(UUID)/schemas ]; then \
24 | glib-compile-schemas $(BUILDDIR)/$(UUID)/schemas; \
25 | fi
26 |
27 | xz: all
28 | (cd $(BUILDDIR)/$(UUID); \
29 | tar -czvf $(ABS_BUILDDIR)/$(UUID).tar.xz $(FILES:%=%); \
30 | );
31 |
32 | zip: all
33 | (cd $(BUILDDIR)/$(UUID); \
34 | zip -rq $(ABS_BUILDDIR)/$(UUID).zip $(FILES:%=%); \
35 | );
36 |
37 | install: all
38 | mkdir -p $(INSTALL_PATH)/$(UUID)
39 | cp -R -p build/$(UUID)/* $(INSTALL_PATH)/$(UUID)
40 |
41 | reloadGnome:
42 | dbus-send --type=method_call --print-reply --dest=org.gnome.Shell /org/gnome/Shell org.gnome.Shell.Eval string:'global.reexec_self()'
43 |
--------------------------------------------------------------------------------
/src/schemas/org.gnome.shell.extensions.azan.gschema.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | "MWL"
7 | Calculation method
8 | Calculation method
9 |
10 |
11 |
12 | "Standard"
13 | Madhab
14 | Madhab for calculating of Asr time
15 |
16 |
17 |
18 | true
19 | Automatic location
20 | Whether or not to use GeoClue2 to detect the location
21 |
22 |
23 |
24 | 34.8406
25 | Latitude
26 | Latitude of your location
27 |
28 |
29 |
30 | 10.7603
31 | Longitude
32 | Longitude of your location
33 |
34 |
35 |
36 | false
37 | AM/PM time format
38 | Set time format to AM/PM hours
39 |
40 |
41 |
42 | "auto"
43 | Timezone
44 | Timezone of your location
45 |
46 |
47 |
48 | 0
49 | Adjustment
50 | Adjustment of the Hijri date
51 |
52 |
53 |
54 | "1"
55 | Which times to show
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/src/HijriCalendarKuwaiti.js:
--------------------------------------------------------------------------------
1 | //source : http://www.al-habib.info/islamic-calendar/hijricalendar-kuwaiti.js
2 |
3 | function gmod(n, m) {
4 | return ((n % m) + m) % m;
5 | }
6 |
7 | function KuwaitiCalendar(adjust) {
8 | var today = new Date();
9 | if (adjust) {
10 | adjustmili = 1000 * 60 * 60 * 24 * adjust;
11 | todaymili = today.getTime() + adjustmili;
12 | today = new Date(todaymili);
13 | }
14 | let day = today.getDate();
15 | let month = today.getMonth();
16 | let year = today.getFullYear();
17 | let m = month + 1;
18 | let y = year;
19 | if (m < 3) {
20 | y -= 1;
21 | m += 12;
22 | }
23 |
24 | let a = Math.floor(y / 100.);
25 | let b = 2 - a + Math.floor(a / 4.);
26 | if (y < 1583) b = 0;
27 | if (y == 1582) {
28 | if (m > 10) b = -10;
29 | if (m == 10) {
30 | b = 0;
31 | if (day > 4) b = -10;
32 | }
33 | }
34 |
35 | let jd = Math.floor(365.25 * (y + 4716)) + Math.floor(30.6001 * (m + 1)) + day + b - 1524;
36 |
37 | b = 0;
38 | if (jd > 2299160) {
39 | a = Math.floor((jd - 1867216.25) / 36524.25);
40 | b = 1 + a - Math.floor(a / 4.);
41 | }
42 | let bb = jd + b + 1524;
43 | let cc = Math.floor((bb - 122.1) / 365.25);
44 | let dd = Math.floor(365.25 * cc);
45 | let ee = Math.floor((bb - dd) / 30.6001);
46 | day = (bb - dd) - Math.floor(30.6001 * ee);
47 | month = ee - 1;
48 | if (ee > 13) {
49 | cc += 1;
50 | month = ee - 13;
51 | }
52 | year = cc - 4716;
53 |
54 | let wd;
55 | if (adjust) {
56 | wd = gmod(jd + 1 - adjust, 7) + 1;
57 | } else {
58 | wd = gmod(jd + 1, 7) + 1;
59 | }
60 |
61 | let iyear = 10631. / 30.;
62 | let epochastro = 1948084;
63 | let epochcivil = 1948085;
64 |
65 | let shift1 = 8.01 / 60.;
66 |
67 | let z = jd - epochastro;
68 | let cyc = Math.floor(z / 10631.);
69 | z = z - 10631 * cyc;
70 | let j = Math.floor((z - shift1) / iyear);
71 | let iy = 30 * cyc + j;
72 | z = z - Math.floor(j * iyear + shift1);
73 | let im = Math.floor((z + 28.5001) / 29.5);
74 | if (im == 13) im = 12;
75 | let id = z - Math.floor(29.5001 * im - 29);
76 |
77 | var myRes = new Array(8);
78 |
79 | myRes[0] = day; //calculated day (CE)
80 | myRes[1] = month - 1; //calculated month (CE)
81 | myRes[2] = year; //calculated year (CE)
82 | myRes[3] = jd - 1; //julian day number
83 | myRes[4] = wd - 1; //weekday number
84 | myRes[5] = id; //islamic date
85 | myRes[6] = im - 1; //islamic month
86 | myRes[7] = iy; //islamic year
87 |
88 | return myRes;
89 | }
90 |
91 | function writeIslamicDate(adjustment) {
92 | var wdNames = new Array("Ahad", "Ithnin", "Thulatha", "Arbaa", "Khams", "Jumuah", "Sabt");
93 | var iMonthNames = new Array("Muharram", "Safar", "Rabi'ul Awwal", "Rabi'ul Akhir",
94 | "Jumadal Ula", "Jumadal Akhira", "Rajab", "Sha'ban",
95 | "Ramadan", "Shawwal", "Dhul Qa'ada", "Dhul Hijja");
96 | var iDate = KuwaitiCalendar(adjustment);
97 | var outputIslamicDate = wdNames[iDate[4]] + ", " + iDate[5] + " " + iMonthNames[iDate[6]] + " " + iDate[7] + " AH";
98 | return outputIslamicDate;
99 | }
100 |
--------------------------------------------------------------------------------
/src/convenience.js:
--------------------------------------------------------------------------------
1 | /* -*- mode: js; js-basic-offset: 4; indent-tabs-mode: nil -*- */
2 | /*
3 | Copyright (c) 2011-2012, Giovanni Campagna
4 |
5 | Redistribution and use in source and binary forms, with or without
6 | modification, are permitted provided that the following conditions are met:
7 | * Redistributions of source code must retain the above copyright
8 | notice, this list of conditions and the following disclaimer.
9 | * Redistributions in binary form must reproduce the above copyright
10 | notice, this list of conditions and the following disclaimer in the
11 | documentation and/or other materials provided with the distribution.
12 | * Neither the name of the GNOME nor the
13 | names of its contributors may be used to endorse or promote products
14 | derived from this software without specific prior written permission.
15 |
16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
20 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 | */
27 |
28 | const Gettext = imports.gettext;
29 | const Gio = imports.gi.Gio;
30 |
31 | const Config = imports.misc.config;
32 | const ExtensionUtils = imports.misc.extensionUtils;
33 |
34 | /**
35 | * initTranslations:
36 | * @domain: (optional): the gettext domain to use
37 | *
38 | * Initialize Gettext to load translations from extensionsdir/locale.
39 | * If @domain is not provided, it will be taken from metadata['gettext-domain']
40 | */
41 | function initTranslations(domain) {
42 | let extension = ExtensionUtils.getCurrentExtension();
43 |
44 | domain = domain || extension.metadata['gettext-domain'];
45 |
46 | // check if this extension was built with "make zip-file", and thus
47 | // has the locale files in a subfolder
48 | // otherwise assume that extension has been installed in the
49 | // same prefix as gnome-shell
50 | let localeDir = extension.dir.get_child('locale');
51 | if (localeDir.query_exists(null))
52 | Gettext.bindtextdomain(domain, localeDir.get_path());
53 | else
54 | Gettext.bindtextdomain(domain, Config.LOCALEDIR);
55 | }
56 |
57 | /**
58 | * getSettings:
59 | * @schema: (optional): the GSettings schema id
60 | *
61 | * Builds and return a GSettings schema for @schema, using schema files
62 | * in extensionsdir/schemas. If @schema is not provided, it is taken from
63 | * metadata['settings-schema'].
64 | */
65 | function getSettings(schema) {
66 | let extension = ExtensionUtils.getCurrentExtension();
67 |
68 | schema = schema || extension.metadata['settings-schema'];
69 |
70 | const GioSSS = Gio.SettingsSchemaSource;
71 |
72 | // check if this extension was built with "make zip-file", and thus
73 | // has the schema files in a subfolder
74 | // otherwise assume that extension has been installed in the
75 | // same prefix as gnome-shell (and therefore schemas are available
76 | // in the standard folders)
77 | let schemaDir = extension.dir.get_child('schemas');
78 | let schemaSource;
79 | if (schemaDir.query_exists(null))
80 | schemaSource = GioSSS.new_from_directory(schemaDir.get_path(),
81 | GioSSS.get_default(),
82 | false);
83 | else
84 | schemaSource = GioSSS.get_default();
85 |
86 | let schemaObj = schemaSource.lookup(schema, true);
87 | if (!schemaObj)
88 | throw new Error('Schema ' + schema + ' could not be found for extension '
89 | + extension.metadata.uuid + '. Please check your installation.');
90 |
91 | return new Gio.Settings({ settings_schema: schemaObj });
92 | }
93 |
94 |
--------------------------------------------------------------------------------
/src/prefs.js:
--------------------------------------------------------------------------------
1 | const GObject = imports.gi.GObject;
2 | const Gtk = imports.gi.Gtk;
3 | const Gio = imports.gi.Gio;
4 | const Params = imports.misc.params;
5 | const Me = imports.misc.extensionUtils.getCurrentExtension();
6 | const PrayTimes = Me.imports.PrayTimes;
7 | const Convenience = Me.imports.convenience;
8 | const PrefsKeys = Me.imports.prefs_keys;
9 | const Config = imports.misc.config;
10 |
11 | const IS_3_XX_SHELL_VERSION = Config.PACKAGE_VERSION.startsWith("3");
12 |
13 | const PagePrefsGrid = new GObject.Class({
14 | Name: 'Page.Prefs.Grid',
15 | GTypeName: 'PagePrefsGrid',
16 | Extends: Gtk.Grid,
17 |
18 | _init: function(params) {
19 | this.parent(params);
20 | this._settings = Convenience.getSettings();
21 | this.margin = this.row_spacing = this.column_spacing = 10;
22 | this._rownum = 0;
23 | },
24 |
25 | add_entry: function(text, key) {
26 | let item = new Gtk.Entry({
27 | hexpand: false
28 | });
29 | item.text = this._settings.get_string(key);
30 | this._settings.bind(key, item, 'text', Gio.SettingsBindFlags.DEFAULT);
31 |
32 | return this.add_row(text, item);
33 | },
34 |
35 | add_shortcut: function(text, settings_key) {
36 | let item = new Gtk.Entry({
37 | hexpand: false
38 | });
39 | item.set_text(this._settings.get_strv(settings_key)[0]);
40 | item.connect('changed', (entry) => {
41 | let [key, mods] = Gtk.accelerator_parse(entry.get_text());
42 |
43 | if(Gtk.accelerator_valid(key, mods)) {
44 | let shortcut = Gtk.accelerator_name(key, mods);
45 | this._settings.set_strv(settings_key, [shortcut]);
46 | }
47 | });
48 |
49 | return this.add_row(text, item);
50 | },
51 |
52 | add_boolean: function(text, key, callback) {
53 | let item = new Gtk.Switch({
54 | active: this._settings.get_boolean(key)
55 | });
56 |
57 | if (callback) {
58 | callback(item, this._settings.get_boolean(key))
59 | }
60 |
61 | this._settings.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT);
62 | return this.add_row(text, item);
63 | },
64 |
65 | add_combo: function(text, key, list, type) {
66 | let item = new Gtk.ComboBoxText();
67 |
68 | for(let i = 0; i < list.length; i++) {
69 | let title = list[i].title.trim();
70 | let id = list[i].value.toString();
71 | item.insert(-1, id, title);
72 | }
73 |
74 | if(type === 'string') {
75 | item.set_active_id(this._settings.get_string(key));
76 | }
77 | else {
78 | item.set_active_id(this._settings.get_int(key).toString());
79 | }
80 |
81 | item.connect('changed', (combo) => {
82 | let value = combo.get_active_id();
83 |
84 | if(type === 'string') {
85 | if(this._settings.get_string(key) !== value) {
86 | this._settings.set_string(key, value);
87 | }
88 | }
89 | else {
90 | value = parseInt(value, 10);
91 |
92 | if(this._settings.get_int(key) !== value) {
93 | this._settings.set_int(key, value);
94 | }
95 | }
96 | });
97 |
98 | return this.add_row(text, item);
99 | },
100 |
101 | add_spin: function(label, key, adjustment_properties, spin_properties) {
102 | adjustment_properties = Params.parse(adjustment_properties, {
103 | lower: 0,
104 | upper: 100,
105 | step_increment: 100
106 | });
107 | let adjustment = new Gtk.Adjustment(adjustment_properties);
108 |
109 | spin_properties = Params.parse(spin_properties, {
110 | adjustment: adjustment,
111 | numeric: true,
112 | digits: 4,
113 | snap_to_ticks: true
114 | }, true);
115 | let spin_button = new Gtk.SpinButton(spin_properties);
116 |
117 | spin_button.set_value(this._settings.get_double(key));
118 | spin_button.connect('value-changed', (spin) => {
119 | let value = spin.get_value();
120 |
121 | if(this._settings.get_double(key) !== value) {
122 | this._settings.set_double(key, value);
123 | }
124 | });
125 |
126 | this._settings.connect('change-event', (settings, key_set) => {
127 | spin_button.set_value(this._settings.get_double(key));
128 | });
129 |
130 | return this.add_row(label, spin_button, true);
131 | },
132 |
133 | add_row: function(text, widget, wrap) {
134 | let label;
135 | if (IS_3_XX_SHELL_VERSION){
136 | label = new Gtk.Label({
137 | label: text,
138 | hexpand: true,
139 | halign: Gtk.Align.START
140 | });
141 | label.set_line_wrap(wrap || false);
142 |
143 | } else {
144 | label= new Gtk.Label({
145 | label: text,
146 | hexpand: true,
147 | halign: Gtk.Align.START
148 | });
149 | }
150 |
151 | if (widget) {
152 | this.attach(label, 0, this._rownum, 1, 1); // col, row, colspan, rowspan
153 | this.attach(widget, 1, this._rownum, 1, 1);
154 | } else {
155 | this.attach(label, 0, this._rownum, 2, 1); // col, row, colspan, rowspan
156 | }
157 | this._rownum++;
158 | if (widget) {
159 | return widget;
160 | }
161 | },
162 |
163 | add_item: function(widget, col, colspan, rowspan) {
164 | this.attach(
165 | widget,
166 | col || 0,
167 | this._rownum,
168 | colspan || 2,
169 | rowspan || 1
170 | );
171 | this._rownum++;
172 |
173 | return widget;
174 | },
175 |
176 | add_range: function(label, key, range_properties) {
177 | range_properties = Params.parse(range_properties, {
178 | min: 0,
179 | max: 100,
180 | step: 10,
181 | mark_position: 0,
182 | add_mark: false,
183 | size: 200,
184 | draw_value: true
185 | });
186 |
187 | let range = Gtk.Scale.new_with_range(
188 | Gtk.Orientation.HORIZONTAL,
189 | range_properties.min,
190 | range_properties.max,
191 | range_properties.step
192 | );
193 | range.set_value(this._settings.get_int(key));
194 | range.set_draw_value(range_properties.draw_value);
195 |
196 | if(range_properties.add_mark) {
197 | range.add_mark(
198 | range_properties.mark_position,
199 | Gtk.PositionType.BOTTOM,
200 | null
201 | );
202 | }
203 |
204 | range.set_size_request(range_properties.size, -1);
205 |
206 | range.connect('value-changed', (slider) => {
207 | this._settings.set_int(key, slider.get_value());
208 | });
209 |
210 | return this.add_row(label, range, true);
211 | }
212 | });
213 |
214 | const AzanPrefsWidget = new GObject.Class({
215 | Name: 'Azan.Prefs.Widget',
216 | GTypeName: 'AzanPrefsWidget',
217 | Extends: Gtk.Box,
218 |
219 | _init: function(params) {
220 | this.parent(params);
221 | this.set_orientation(Gtk.Orientation.VERTICAL);
222 | this._settings = Convenience.getSettings();
223 |
224 | let stack = new Gtk.Stack({
225 | transition_type: Gtk.StackTransitionType.SLIDE_LEFT_RIGHT,
226 | transition_duration: 500
227 | });
228 |
229 | let stack_switcher
230 | if (IS_3_XX_SHELL_VERSION){
231 | stack_switcher = new Gtk.StackSwitcher({
232 | margin_left: 5,
233 | margin_top: 5,
234 | margin_bottom: 5,
235 | margin_right: 5,
236 | stack: stack
237 | });
238 | this._init_stack(stack);
239 | this.add(stack_switcher);
240 | this.add(stack);
241 | } else {
242 | stack_switcher = new Gtk.StackSwitcher({
243 | margin_start: 5,
244 | margin_end: 5,
245 | margin_top: 5,
246 | margin_bottom: 5,
247 | stack: stack
248 | });
249 | this._init_stack(stack);
250 | this.append(stack_switcher);
251 | this.append(stack);
252 | }
253 | },
254 |
255 | _get_tab_config: function() {
256 |
257 | let calculation_page;
258 | if (IS_3_XX_SHELL_VERSION) {
259 | calculation_page = new PagePrefsGrid();
260 | } else {
261 | calculation_page = new PagePrefsGrid();
262 | calculation_page.set_margin_top(10);
263 | calculation_page.set_margin_start(5);
264 | calculation_page.set_margin_end(5);
265 | }
266 | calculation_page.add_row('Please note that all prayer calculations by their nature can only be a guideline and are not definitive.', false, true);
267 |
268 | calculation_page.add_combo('Calculation method',
269 | PrefsKeys.CALCULATION_METHOD,
270 | Object
271 | .entries(PrayTimes.getMethods())
272 | .map(([value,{name}]) => ({value, title: name})),
273 | 'string'
274 | );
275 |
276 | calculation_page.add_combo('Madhab', PrefsKeys.MADHAB, [
277 | {'title': 'Standard (Shafii, Maliki, Hanbali, Dhahiri)', 'value': 'Standard'},
278 | {'title': 'Hanafi', 'value': 'Hanafi'}
279 | ], 'string');
280 |
281 | let location_page;
282 | if (IS_3_XX_SHELL_VERSION){
283 | location_page = new PagePrefsGrid();
284 | } else {
285 | location_page = new PagePrefsGrid();
286 | location_page.set_margin_top(10);
287 | location_page.set_margin_start(5);
288 | location_page.set_margin_end(5);
289 | }
290 |
291 | this.latitude_box = location_page.add_spin('Latitude', PrefsKeys.LATITUDE, {
292 | lower: -90.0000,
293 | upper: 90.0000,
294 | step_increment: 0.0001
295 | });
296 |
297 | this.longitude_box = location_page.add_spin('Longitude', PrefsKeys.LONGITUDE, {
298 | lower: -180.0000,
299 | upper: 180.0000,
300 | step_increment: 0.0001
301 | });
302 |
303 | let updateLocationState = (entry,state) => {
304 | this.latitude_box.set_sensitive(!state);
305 | this.longitude_box.set_sensitive(!state);
306 | }
307 |
308 | this.auto_location = location_page.add_boolean('Automatic location', PrefsKeys.AUTO_LOCATION, updateLocationState);
309 |
310 | this.auto_location.connect('state-set', updateLocationState);
311 |
312 | location_page.add_combo('Timezone', PrefsKeys.TIMEZONE, [
313 | {'title': 'Auto', 'value': 'auto'},
314 | {'title': 'GMT -12:00', 'value': '-12'},
315 | {'title': 'GMT -11:00', 'value': '-11'},
316 | {'title': 'GMT -10:00', 'value': '-10'},
317 | {'title': 'GMT -09:30', 'value': '-9.5'},
318 | {'title': 'GMT -09:00', 'value': '-9'},
319 | {'title': 'GMT -08:00', 'value': '-8'},
320 | {'title': 'GMT -07:00', 'value': '-7'},
321 | {'title': 'GMT -06:00', 'value': '-6'},
322 | {'title': 'GMT -05:00', 'value': '-5'},
323 | {'title': 'GMT -04:00', 'value': '-4'},
324 | {'title': 'GMT -03:30', 'value': '-3.5'},
325 | {'title': 'GMT -03:00', 'value': '-3'},
326 | {'title': 'GMT -02:00', 'value': '-2'},
327 | {'title': 'GMT -01:00', 'value': '-1'},
328 | {'title': 'GMT +00:00', 'value': '0'},
329 | {'title': 'GMT +01:00', 'value': '1'},
330 | {'title': 'GMT +02:00', 'value': '2'},
331 | {'title': 'GMT +03:00', 'value': '3'},
332 | {'title': 'GMT +03:30', 'value': '3.5'},
333 | {'title': 'GMT +04:00', 'value': '4'},
334 | {'title': 'GMT +04:30', 'value': '4'},
335 | {'title': 'GMT +05:00', 'value': '5'},
336 | {'title': 'GMT +05:30', 'value': '5.5'},
337 | {'title': 'GMT +05:45', 'value': '5.75'},
338 | {'title': 'GMT +06:00', 'value': '6'},
339 | {'title': 'GMT +06:30', 'value': '6.5'},
340 | {'title': 'GMT +07:00', 'value': '7'},
341 | {'title': 'GMT +08:00', 'value': '8'},
342 | {'title': 'GMT +08:45', 'value': '8.75'},
343 | {'title': 'GMT +09:00', 'value': '9'},
344 | {'title': 'GMT +09:30', 'value': '9.5'},
345 | {'title': 'GMT +10:00', 'value': '10'},
346 | {'title': 'GMT +10:30', 'value': '10.5'},
347 | {'title': 'GMT +11:00', 'value': '11'},
348 | {'title': 'GMT +12:00', 'value': '12'},
349 | {'title': 'GMT +13:00', 'value': '13'},
350 | {'title': 'GMT +14:00', 'value': '14'}
351 | ], 'string');
352 |
353 | let display_page;
354 | if (IS_3_XX_SHELL_VERSION){
355 | display_page = new PagePrefsGrid();
356 | } else {
357 | display_page = new PagePrefsGrid();
358 | display_page.set_margin_top(10);
359 | display_page.set_margin_start(5);
360 | display_page.set_margin_end(5);
361 | }
362 |
363 | this.time_format_12 = display_page.add_boolean('AM/PM time format', PrefsKeys.TIME_FORMAT_12);
364 |
365 | display_page.add_combo('Which times?', PrefsKeys.CONCISE_LIST, [
366 | {'title': 'All times', 'value': '0'},
367 | {'title': 'Concise', 'value': '1'}
368 | ], 'string');
369 |
370 | calculation_page.add_range('Date adjustment', PrefsKeys.HIJRI_DATE_ADJUSTMENT, {
371 | min: -2,
372 | max: 2,
373 | step: 1,
374 | mark_position: 0,
375 | add_mark: true,
376 | size: 200,
377 | draw_value: true
378 | });
379 |
380 | let pages = [
381 | {
382 | name: 'Calculation',
383 | page: calculation_page
384 | },
385 | {
386 | name: 'Your Location',
387 | page: location_page
388 | },
389 | {
390 | name: 'Display',
391 | page: display_page
392 | }
393 | ];
394 |
395 | return pages;
396 | },
397 |
398 | _init_stack: function(stack) {
399 | let config = this._get_tab_config();
400 | for (let index in config) {
401 | stack.add_titled(config[index].page, config[index].name, config[index].name);
402 | }
403 | }
404 | });
405 |
406 | function init() {
407 |
408 | }
409 |
410 | function buildPrefsWidget() {
411 | let widget = new AzanPrefsWidget();
412 | if (IS_3_XX_SHELL_VERSION){
413 | widget.show_all();
414 | } else {
415 | widget.show();
416 | }
417 |
418 | return widget;
419 | }
420 |
--------------------------------------------------------------------------------
/src/extension.js:
--------------------------------------------------------------------------------
1 | const Geoclue = imports.gi.Geoclue;
2 | const St = imports.gi.St;
3 | const Main = imports.ui.main;
4 | const Soup = imports.gi.Soup;
5 | const Mainloop = imports.mainloop;
6 | const GObject = imports.gi.GObject;
7 | const GLib = imports.gi.GLib;
8 | const Gio = imports.gi.Gio;
9 | const Clutter = imports.gi.Clutter;
10 | const PanelMenu = imports.ui.panelMenu;
11 | const PopupMenu = imports.ui.popupMenu;
12 | const MessageTray = imports.ui.messageTray;
13 | const Util = imports.misc.util;
14 | const PermissionStore = imports.misc.permissionStore;
15 | const ExtensionUtils = imports.misc.extensionUtils;
16 |
17 | const Extension = imports.misc.extensionUtils.getCurrentExtension();
18 |
19 | const PrayTimes = Extension.imports.PrayTimes;
20 | const HijriCalendarKuwaiti = Extension.imports.HijriCalendarKuwaiti;
21 | const Convenience = Extension.imports.convenience;
22 | const PrefsKeys = Extension.imports.prefs_keys;
23 |
24 | const Azan = GObject.registerClass(
25 | class Azan extends PanelMenu.Button {
26 |
27 | _init() {
28 | super._init(0.5, _('Azan'));
29 |
30 | this.indicatorText = new St.Label({text: _("Loading..."), y_align: Clutter.ActorAlign.CENTER});
31 | this.add_child(this.indicatorText);
32 |
33 | this._gclueLocationChangedId = 0;
34 | this._weatherAuthorized = false;
35 |
36 | this._opt_calculationMethod = null;
37 | this._opt_madhab = null;
38 | this._opt_latitude = null;
39 | this._opt_longitude = null;
40 | this._opt_timezone = null;
41 | this._opt_timeformat12 = false;
42 | this._opt_concise_list = null;
43 | this._opt_hijriDateAdjustment = null;
44 |
45 | this._settings = Convenience.getSettings();
46 | this._bindSettings();
47 | this._loadSettings();
48 |
49 | this._dateFormatFull = _("%A %B %e, %Y");
50 |
51 | this._prayTimes = new PrayTimes.PrayTimes('MWL');
52 |
53 |
54 | this._dayNames = new Array("Ahad", "Ithnin", "Thulatha", "Arbiaa", "Khamees", "Jomuah", "Issabt");
55 | this._monthNames = new Array("Muharram", "Safar", "Rabi'ul Awwal", "Rabi'ul Akhir",
56 | "Jumadal Ula", "Jumadal Akhira", "Rajab", "Sha'ban",
57 | "Ramadhan", "Shawwal", "Dhul Qa'ada", "Dhul Hijja");
58 |
59 | this._timeNames = {
60 | fajr: 'Fajr',
61 | sunrise: 'Sunrise',
62 | dhuhr: 'Dhuhr',
63 | asr: 'Asr',
64 | sunset: 'Sunset',
65 | maghrib: 'Maghrib',
66 | isha: 'Isha',
67 | midnight: 'Midnight'
68 | };
69 |
70 | this._timeConciseLevels = {
71 | fajr: 1,
72 | sunrise: 0,
73 | dhuhr: 1,
74 | asr: 1,
75 | sunset: 0,
76 | maghrib: 1,
77 | isha: 1,
78 | midnight: 0
79 | };
80 |
81 | this._prayItems = {};
82 |
83 | this._dateMenuItem = new PopupMenu.PopupMenuItem(_("TODO"), {
84 | style_class: 'azan-panel', reactive: false, hover: false, activate: false
85 | });
86 |
87 | this.menu.addMenuItem(this._dateMenuItem);
88 |
89 | this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
90 |
91 | for (let prayerId in this._timeNames) {
92 |
93 | let prayerName = this._timeNames[prayerId];
94 |
95 | let prayMenuItem = new PopupMenu.PopupMenuItem(_(prayerName), {
96 | reactive: false, hover: false, activate: false
97 | });
98 |
99 | let bin = new St.Bin({x_expand: true,x_align: Clutter.ActorAlign.END});
100 |
101 | let prayLabel = new St.Label();
102 | bin.add_actor(prayLabel);
103 |
104 | prayMenuItem.actor.add_actor(bin);
105 |
106 | this.menu.addMenuItem(prayMenuItem);
107 |
108 | this._prayItems[prayerId] = { menuItem: prayMenuItem, label: prayLabel };
109 | };
110 |
111 | this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
112 |
113 | // making mordernize
114 | this.prefs_s = new PopupMenu.PopupBaseMenuItem ({ reactive: false, can_focus: false});
115 | let l = new St.Label ({text: ' '});
116 | l.x_expand = true;
117 | this.prefs_s.actor.add(l);
118 | this.prefs_b = new St.Button ({ child: new St.Icon ({ icon_name: 'preferences-system-symbolic', icon_size: 30 }), style_class: 'prefs_s_action'});
119 |
120 | this.prefs_b.connect ('clicked', () => {
121 | ExtensionUtils.openPrefs()
122 | });
123 |
124 | this.prefs_s.actor.add(this.prefs_b);
125 | l = new St.Label ({text: ' '});
126 | l.x_expand = true;
127 | this.prefs_s.actor.add(l);
128 |
129 | this.menu.addMenuItem(this.prefs_s);
130 |
131 | this._updateLabelPeriodic();
132 | this._updatePrayerVisibility();
133 |
134 | this._permStore = new PermissionStore.PermissionStore((proxy, error) => {
135 | if (error) {
136 | log('Failed to connect to permissionStore: ' + error.message);
137 | return;
138 | }
139 |
140 | this._permStore.LookupRemote('gnome', 'geolocation', (res, error) => {
141 | if (error)
142 | log('Error looking up permission: ' + error.message);
143 |
144 | let [perms, data] = error ? [{}, null] : res;
145 | let params = ['gnome', 'geolocation', false, data, perms];
146 | this._onPermStoreChanged(this._permStore, '', params);
147 | });
148 | });
149 | }
150 |
151 | _startGClueService() {
152 | if (this._gclueStarting)
153 | return;
154 |
155 | this._gclueStarting = true;
156 |
157 | Geoclue.Simple.new('org.gnome.Shell', Geoclue.AccuracyLevel.EXACT, null,
158 | (o, res) => {
159 | try {
160 | this._gclueService = Geoclue.Simple.new_finish(res);
161 | } catch(e) {
162 | log('Failed to connect to Geoclue2 service: ' + e.message);
163 | return;
164 | }
165 | this._gclueStarted = true;
166 | this._gclueService.get_client().distance_threshold = 100;
167 | this._updateLocationMonitoring();
168 | });
169 | }
170 |
171 | _onPermStoreChanged(proxy, sender, params) {
172 | let [table, id, deleted, data, perms] = params;
173 |
174 | if (table != 'gnome' || id != 'geolocation')
175 | return;
176 |
177 | let permission = perms['org.gnome.Weather.Application'] || ['NONE'];
178 | let [accuracy] = permission;
179 | this._weatherAuthorized = accuracy != 'NONE';
180 |
181 | this._updateAutoLocation();
182 | }
183 |
184 | _onGClueLocationChanged() {
185 | let geoLocation = this._gclueService.location;
186 | this._opt_latitude = geoLocation.latitude;
187 | this._opt_longitude = geoLocation.longitude;
188 | this._settings.set_double(PrefsKeys.LATITUDE, this._opt_latitude);
189 | this._settings.set_double(PrefsKeys.LONGITUDE, this._opt_longitude);
190 | }
191 |
192 | _updateLocationMonitoring() {
193 | if (this._opt_autoLocation) {
194 | if (this._gclueLocationChangedId != 0 || this._gclueService == null)
195 | return;
196 |
197 | this._gclueLocationChangedId =
198 | this._gclueService.connect('notify::location',
199 | this._onGClueLocationChanged.bind(this));
200 | this._onGClueLocationChanged();
201 | } else {
202 | if (this._gclueLocationChangedId)
203 | this._gclueService.disconnect(this._gclueLocationChangedId);
204 | this._gclueLocationChangedId = 0;
205 | }
206 | }
207 |
208 | _updateAutoLocation() {
209 | this._updateLocationMonitoring();
210 |
211 | if (this._opt_autoLocation) {
212 | this._startGClueService();
213 | }
214 | }
215 |
216 | _loadSettings() {
217 | this._opt_calculationMethod = this._settings.get_string(PrefsKeys.CALCULATION_METHOD);
218 | this._opt_madhab = this._settings.get_string(PrefsKeys.MADHAB);
219 | this._opt_autoLocation = this._settings.get_boolean(PrefsKeys.AUTO_LOCATION);
220 | this._updateAutoLocation();
221 | this._opt_latitude = this._settings.get_double(PrefsKeys.LATITUDE);
222 | this._opt_longitude = this._settings.get_double(PrefsKeys.LONGITUDE);
223 | this._opt_timeformat12 = this._settings.get_boolean(PrefsKeys.TIME_FORMAT_12);
224 | this._opt_timezone = this._settings.get_string(PrefsKeys.TIMEZONE);
225 | this._opt_concise_list = this._settings.get_string(PrefsKeys.CONCISE_LIST);
226 | this._opt_hijriDateAdjustment = this._settings.get_double(PrefsKeys.HIJRI_DATE_ADJUSTMENT);
227 | }
228 | _bindSettings() {
229 | this._settings.connect('changed::' + PrefsKeys.AUTO_LOCATION, (settings, key) => {
230 | this._opt_autoLocation = settings.get_boolean(key);
231 | this._updateAutoLocation();
232 | this._updateLabel();
233 | });
234 |
235 | this._settings.connect('changed::' + PrefsKeys.CALCULATION_METHOD, (settings, key) => {
236 | this._opt_calculationMethod = settings.get_string(key);
237 |
238 | this._updateLabel();
239 | });
240 |
241 | this._settings.connect('changed::' + PrefsKeys.MADHAB, (settings, key) => {
242 | this._opt_madhab = settings.get_string(key);
243 |
244 | this._updateLabel();
245 | });
246 |
247 | this._settings.connect('changed::' + PrefsKeys.LATITUDE, (settings, key) => {
248 | this._opt_latitude = settings.get_double(key);
249 |
250 | this._updateLabel();
251 | });
252 | this._settings.connect('changed::' + PrefsKeys.LONGITUDE, (settings, key) => {
253 | this._opt_longitude = settings.get_double(key);
254 |
255 | this._updateLabel();
256 | });
257 | this._settings.connect('changed::' + PrefsKeys.TIME_FORMAT_12, (settings, key) => {
258 | this._opt_timeformat12 = settings.get_boolean(key);
259 | this._updateLabel();
260 | });
261 | this._settings.connect('changed::' + PrefsKeys.TIMEZONE, (settings, key) => {
262 | this._opt_timezone = settings.get_string(key);
263 |
264 | this._updateLabel();
265 | });
266 |
267 | this._settings.connect('changed::' + PrefsKeys.CONCISE_LIST, (settings, key) => {
268 | this._opt_concise_list = settings.get_string(key);
269 | this._updateLabel();
270 | this._updatePrayerVisibility();
271 | });
272 |
273 | this._settings.connect('changed::' + PrefsKeys.HIJRI_DATE_ADJUSTMENT, (settings, key) => {
274 | this._opt_hijriDateAdjustment = settings.get_double(key);
275 |
276 | this._updateLabel();
277 | });
278 | }
279 |
280 | _updatePrayerVisibility() {
281 | for (let prayerId in this._timeNames) {
282 | this._prayItems[prayerId].menuItem.actor.visible = this._isVisiblePrayer(prayerId);
283 | }
284 | }
285 |
286 | _isVisiblePrayer(prayerId) {
287 | return this._timeConciseLevels[prayerId] >= this._opt_concise_list;
288 | }
289 |
290 | _updateLabelPeriodic() {
291 | let currentSeconds = new Date().getSeconds();
292 | if (currentSeconds === 0) {
293 | this._periodicTimeoutId = Mainloop.timeout_add_seconds(60,
294 | this._updateLabelPeriodic.bind(this));
295 | } else {
296 | this._periodicTimeoutId = Mainloop.timeout_add_seconds(60 - currentSeconds,
297 | this._updateLabelPeriodic.bind(this));
298 | }
299 |
300 | this._updateLabel();
301 | }
302 |
303 | _updateLabel() {
304 | let displayDate = GLib.DateTime.new_now_local();
305 | let dateFormattedFull = displayDate.format(this._dateFormatFull);
306 |
307 | let myLocation = [this._opt_latitude, this._opt_longitude];
308 | let myTimezone = this._opt_timezone;
309 | this._prayTimes.setMethod(this._opt_calculationMethod);
310 | this._prayTimes.adjust({asr: this._opt_madhab});
311 |
312 | let currentDate = new Date();
313 |
314 | let currentSeconds = this._calculateSecondsFromDate(currentDate);
315 |
316 | let timesStr;
317 |
318 | if (this._opt_timeformat12) {
319 | timesStr = this._prayTimes.getTimes(currentDate, myLocation, myTimezone, 'auto', '12h');
320 | } else {
321 | timesStr = this._prayTimes.getTimes(currentDate, myLocation, myTimezone, 'auto', '24h');
322 | }
323 |
324 | let timesFloat = this._prayTimes.getTimes(currentDate, myLocation, myTimezone, 'auto', 'Float');
325 |
326 | let nearestPrayerId;
327 | let minDiffMinutes = Number.MAX_VALUE;
328 | let isTimeForPraying = false;
329 | for (let prayerId in this._timeNames) {
330 |
331 | let prayerName = this._timeNames[prayerId];
332 | let prayerTime = timesStr[prayerId];
333 |
334 | this._prayItems[prayerId].label.text = prayerTime;
335 |
336 | if (this._isPrayerTime(prayerId)) {
337 |
338 | let prayerSeconds = this._calculateSecondsFromHour(timesFloat[prayerId]);
339 |
340 | let ishaSeconds = this._calculateSecondsFromHour(timesFloat['isha']);
341 | let fajrSeconds = this._calculateSecondsFromHour(timesFloat['fajr']);
342 |
343 | if (prayerId === 'fajr' && currentSeconds > ishaSeconds) {
344 | prayerSeconds = fajrSeconds + (24 * 60 *60);
345 | }
346 |
347 | let diffSeconds = prayerSeconds - currentSeconds;
348 |
349 | if (diffSeconds <= 0 && diffSeconds > -60) {
350 | isTimeForPraying = true;
351 | nearestPrayerId = prayerId;
352 | break;
353 | }
354 |
355 | if (diffSeconds > 0) {
356 | let diffMinutes = ~~(diffSeconds / 60);
357 |
358 | if (diffMinutes <= minDiffMinutes) {
359 | minDiffMinutes = diffMinutes;
360 | nearestPrayerId = prayerId;
361 | }
362 | }
363 |
364 | }
365 | };
366 |
367 |
368 | let hijriDate = HijriCalendarKuwaiti.KuwaitiCalendar(this._opt_hijriDateAdjustment);
369 |
370 | let outputIslamicDate = this._formatHijriDate(hijriDate);
371 |
372 | this._dateMenuItem.label.text = outputIslamicDate;
373 |
374 | if ( (minDiffMinutes === 15) || (minDiffMinutes === 10) || (minDiffMinutes === 5) ) {
375 | Main.notify(_(minDiffMinutes + " minutes remaining until " + this._timeNames[nearestPrayerId]) + " prayer.", _("Prayer time : " + timesStr[nearestPrayerId]));
376 | }
377 |
378 | if (isTimeForPraying) {
379 | Main.notify(_("It's time for the " + this._timeNames[nearestPrayerId]) + " prayer.", _("Prayer time : " + timesStr[nearestPrayerId]));
380 | this.indicatorText.set_text(_("It's time for " + this._timeNames[nearestPrayerId]));
381 | } else {
382 | this.indicatorText.set_text(this._timeNames[nearestPrayerId] + ' -' + this._formatRemainingTimeFromMinutes(minDiffMinutes));
383 | }
384 | }
385 |
386 | _calculateSecondsFromDate(date) {
387 | return this._calculateSecondsFromHour(date.getHours()) + (date.getMinutes() * 60);
388 | }
389 |
390 | _calculateSecondsFromHour(hour) {
391 | return (hour * 60 * 60);
392 | }
393 |
394 | _isPrayerTime(prayerId) {
395 | return prayerId === 'fajr' || prayerId === 'dhuhr' || prayerId === 'asr' || prayerId === 'maghrib' || prayerId === 'isha';
396 | }
397 |
398 | _formatRemainingTimeFromMinutes(diffMinutes) {
399 | let hours = ~~(diffMinutes / 60);
400 | let minutes = ~~(diffMinutes % 60);
401 |
402 | let hoursStr = (hours < 10 ? "0" : "") + hours;
403 | let minutesStr = (minutes < 10 ? "0" : "") + minutes;
404 |
405 | return hoursStr + ":" + minutesStr;
406 | }
407 |
408 | _formatHijriDate(hijriDate) {
409 | return this._dayNames[hijriDate[4]] + ", " + hijriDate[5] + " " + this._monthNames[hijriDate[6]] + " " + hijriDate[7];
410 | }
411 |
412 | stop() {
413 |
414 | this.menu.removeAll();
415 |
416 | if (this._periodicTimeoutId) {
417 | Mainloop.source_remove(this._periodicTimeoutId);
418 | }
419 | }
420 | });
421 |
422 | let azan;
423 |
424 | function init() {
425 | }
426 |
427 | function enable() {
428 | azan = new Azan();
429 | Main.panel.addToStatusArea('azan', azan, 1, 'center');
430 | }
431 |
432 | function disable() {
433 | azan.stop();
434 | azan.destroy();
435 | }
436 |
--------------------------------------------------------------------------------
/src/PrayTimes.js:
--------------------------------------------------------------------------------
1 | //--------------------- Copyright Block ----------------------
2 | /*
3 |
4 | PrayTimes.js: Prayer Times Calculator (ver 2.3)
5 | Copyright (C) 2007-2011 PrayTimes.org
6 |
7 | Developer: Hamid Zarrabi-Zadeh
8 | License: GNU LGPL v3.0
9 |
10 | TERMS OF USE:
11 | Permission is granted to use this code, with or
12 | without modification, in any website or application
13 | provided that credit is given to the original work
14 | with a link back to PrayTimes.org.
15 |
16 | This program is distributed in the hope that it will
17 | be useful, but WITHOUT ANY WARRANTY.
18 |
19 | PLEASE DO NOT REMOVE THIS COPYRIGHT BLOCK.
20 |
21 | */
22 |
23 |
24 | //--------------------- Help and Manual ----------------------
25 | /*
26 |
27 | User's Manual:
28 | http://praytimes.org/manual
29 |
30 | Calculation Formulas:
31 | http://praytimes.org/calculation
32 |
33 |
34 |
35 | //------------------------ User Interface -------------------------
36 |
37 |
38 | getTimes (date, coordinates [, timeZone [, dst [, timeFormat]]])
39 |
40 | setMethod (method) // set calculation method
41 | adjust (parameters) // adjust calculation parameters
42 | tune (offsets) // tune times by given offsets
43 |
44 | getMethod () // get calculation method
45 | getSetting () // get current calculation parameters
46 | getOffsets () // get current time offsets
47 |
48 |
49 | //------------------------- Sample Usage --------------------------
50 |
51 |
52 | var PT = new PrayTimes('ISNA');
53 | var times = PT.getTimes(new Date(), [43, -80], -5);
54 | document.write('Sunrise = '+ times.sunrise)
55 |
56 |
57 | */
58 |
59 | function getMethods() {
60 | return {
61 | MUI: {
62 | name: 'Majelis Ulama Indonesia',
63 | params: { fajr: 20, dhuhr: '4 mins', maghrib: 2, isha: 18.75 } },
64 | MWL: {
65 | name: 'Muslim World League',
66 | params: { fajr: 18, isha: 17 } },
67 | ISNA: {
68 | name: 'Islamic Society of North America (ISNA)',
69 | params: { fajr: 15, isha: 15 } },
70 | Egypt: {
71 | name: 'Egyptian General Authority of Survey',
72 | params: { fajr: 19.5, isha: 17.5 } },
73 | Makkah: {
74 | name: 'Umm Al-Qura University, Makkah',
75 | params: { fajr: 18.5, isha: '90 min' } }, // fajr was 19 degrees before 1430 hijri
76 | Karachi: {
77 | name: 'University of Islamic Sciences, Karachi',
78 | params: { fajr: 18, isha: 18 } },
79 | Tehran: {
80 | name: 'Institute of Geophysics, University of Tehran',
81 | params: { fajr: 17.7, isha: 14, maghrib: 4.5, midnight: 'Jafari' } } // isha is not explicitly specified in this method
82 | };
83 | }
84 | //----------------------- PrayTimes Class ------------------------
85 |
86 | function PrayTimes(method) {
87 |
88 |
89 | //------------------------ Constants --------------------------
90 | var
91 |
92 | // Time Names
93 | timeNames = {
94 | imsak : 'Imsak',
95 | fajr : 'Fajr',
96 | sunrise : 'Sunrise',
97 | dhuhr : 'Dhuhr',
98 | asr : 'Asr',
99 | sunset : 'Sunset',
100 | maghrib : 'Maghrib',
101 | isha : 'Isha',
102 | midnight : 'Midnight'
103 | },
104 |
105 |
106 | // Calculation Methods
107 | methods = getMethods(),
108 |
109 |
110 | // Default Parameters in Calculation Methods
111 | defaultParams = {
112 | maghrib: '0 min', midnight: 'Standard'
113 |
114 | },
115 |
116 |
117 | //----------------------- Parameter Values ----------------------
118 | /*
119 |
120 | // Asr Juristic Methods
121 | asrJuristics = [
122 | 'Standard', // Shafi`i, Maliki, Ja`fari, Hanbali
123 | 'Hanafi' // Hanafi
124 | ],
125 |
126 |
127 | // Midnight Mode
128 | midnightMethods = [
129 | 'Standard', // Mid Sunset to Sunrise
130 | 'Jafari' // Mid Sunset to Fajr
131 | ],
132 |
133 |
134 | // Adjust Methods for Higher Latitudes
135 | highLatMethods = [
136 | 'NightMiddle', // middle of night
137 | 'AngleBased', // angle/60th of night
138 | 'OneSeventh', // 1/7th of night
139 | 'None' // No adjustment
140 | ],
141 |
142 |
143 | // Time Formats
144 | timeFormats = [
145 | '24h', // 24-hour format
146 | '12h', // 12-hour format
147 | '12hNS', // 12-hour format with no suffix
148 | 'Float' // floating point number
149 | ],
150 | */
151 |
152 |
153 | //---------------------- Default Settings --------------------
154 |
155 | calcMethod = 'MWL',
156 |
157 | // do not change anything here; use adjust method instead
158 | setting = {
159 | imsak : '10 min',
160 | dhuhr : '0 min',
161 | asr : 'Standard',
162 | highLats : 'NightMiddle'
163 | },
164 |
165 | timeFormat = '24h',
166 | timeSuffixes = ['am', 'pm'],
167 | invalidTime = '-----',
168 |
169 | numIterations = 1,
170 | offset = {},
171 |
172 |
173 | //----------------------- Local Variables ---------------------
174 |
175 | lat, lng, elv, // coordinates
176 | timeZone, jDate; // time variables
177 |
178 |
179 | //---------------------- Initialization -----------------------
180 |
181 |
182 | // set methods defaults
183 | var defParams = defaultParams;
184 | for (var i in methods) {
185 | var params = methods[i].params;
186 | for (var j in defParams)
187 | if ((typeof(params[j]) == 'undefined'))
188 | params[j] = defParams[j];
189 | };
190 |
191 | // initialize settings
192 | calcMethod = methods[method] ? method : calcMethod;
193 | var params = methods[calcMethod].params;
194 | for (var id in params)
195 | setting[id] = params[id];
196 |
197 | // init time offsets
198 | for (var i in timeNames)
199 | offset[i] = 0;
200 |
201 |
202 |
203 | //----------------------- Public Functions ------------------------
204 | return {
205 |
206 |
207 | // set calculation method
208 | setMethod: function(method) {
209 | if (methods[method]) {
210 | this.adjust(methods[method].params);
211 | calcMethod = method;
212 | }
213 | },
214 |
215 |
216 | // set calculating parameters
217 | adjust: function(params) {
218 | for (var id in params)
219 | setting[id] = params[id];
220 | },
221 |
222 |
223 | // set time offsets
224 | tune: function(timeOffsets) {
225 | for (var i in timeOffsets)
226 | offset[i] = timeOffsets[i];
227 | },
228 |
229 |
230 | // get current calculation method
231 | getMethod: function() { return calcMethod; },
232 |
233 | // get current setting
234 | getSetting: function() { return setting; },
235 |
236 | // get current time offsets
237 | getOffsets: function() { return offset; },
238 |
239 | // get default calc parametrs
240 | getDefaults: function() { return methods; },
241 |
242 |
243 | // return prayer times for a given date
244 | getTimes: function(date, coords, timezone, dst, format) {
245 | lat = 1* coords[0];
246 | lng = 1* coords[1];
247 | elv = coords[2] ? 1* coords[2] : 0;
248 | timeFormat = format || timeFormat;
249 | if (date.constructor === Date)
250 | date = [date.getFullYear(), date.getMonth()+ 1, date.getDate()];
251 | if (typeof(timezone) == 'undefined' || timezone == 'auto')
252 | timezone = this.getTimeZone(date);
253 | if (typeof(dst) == 'undefined' || dst == 'auto')
254 | dst = this.getDst(date);
255 | timeZone = 1* timezone+ (1* dst ? 1 : 0);
256 | jDate = this.julian(date[0], date[1], date[2])- lng/ (15* 24);
257 |
258 | return this.computeTimes();
259 | },
260 |
261 |
262 | // convert float time to the given format (see timeFormats)
263 | getFormattedTime: function(time, format, suffixes) {
264 | if (isNaN(time))
265 | return invalidTime;
266 | // if (format == 'Float') return time;
267 | suffixes = suffixes || timeSuffixes;
268 |
269 | time = DMath.fixHour(time); // add 0.5 minutes to round
270 | var hours = Math.floor(time);
271 | var minutes = Math.floor((time- hours)* 60);
272 | var suffix = (format == '12h') ? suffixes[hours < 12 ? 0 : 1] : '';
273 | var hour = (format == '24h') ? this.twoDigitsFormat(hours) : ((hours+ 12 -1)% 12+ 1);
274 |
275 | if (format == 'Float') return (hours) + (minutes/60);
276 |
277 | return hour+ ':'+ this.twoDigitsFormat(minutes)+ (suffix ? ' '+ suffix : '');
278 | },
279 |
280 |
281 | //---------------------- Calculation Functions -----------------------
282 |
283 |
284 | // compute mid-day time
285 | midDay: function(time) {
286 | var eqt = this.sunPosition(jDate+ time).equation;
287 | var noon = DMath.fixHour(12- eqt);
288 | return noon;
289 | },
290 |
291 |
292 | // compute the time at which sun reaches a specific angle below horizon
293 | sunAngleTime: function(angle, time, direction) {
294 | var decl = this.sunPosition(jDate+ time).declination;
295 | var noon = this.midDay(time);
296 | var t = 1/15* DMath.arccos((-DMath.sin(angle)- DMath.sin(decl)* DMath.sin(lat))/
297 | (DMath.cos(decl)* DMath.cos(lat)));
298 | return noon+ (direction == 'ccw' ? -t : t);
299 | },
300 |
301 |
302 | // compute asr time
303 | asrTime: function(factor, time) {
304 | var decl = this.sunPosition(jDate+ time).declination;
305 | var angle = -DMath.arccot(factor+ DMath.tan(Math.abs(lat- decl)));
306 | return this.sunAngleTime(angle, time);
307 | },
308 |
309 |
310 | // compute declination angle of sun and equation of time
311 | // Ref: http://aa.usno.navy.mil/faq/docs/SunApprox.php
312 | sunPosition: function(jd) {
313 | var D = jd - 2451545.0;
314 | var g = DMath.fixAngle(357.529 + 0.98560028* D);
315 | var q = DMath.fixAngle(280.459 + 0.98564736* D);
316 | var L = DMath.fixAngle(q + 1.915* DMath.sin(g) + 0.020* DMath.sin(2*g));
317 |
318 | var R = 1.00014 - 0.01671* DMath.cos(g) - 0.00014* DMath.cos(2*g);
319 | var e = 23.439 - 0.00000036* D;
320 |
321 | var RA = DMath.arctan2(DMath.cos(e)* DMath.sin(L), DMath.cos(L))/ 15;
322 | var eqt = q/15 - DMath.fixHour(RA);
323 | var decl = DMath.arcsin(DMath.sin(e)* DMath.sin(L));
324 |
325 | return {declination: decl, equation: eqt};
326 | },
327 |
328 |
329 | // convert Gregorian date to Julian day
330 | // Ref: Astronomical Algorithms by Jean Meeus
331 | julian: function(year, month, day) {
332 | if (month <= 2) {
333 | year -= 1;
334 | month += 12;
335 | };
336 | var A = Math.floor(year/ 100);
337 | var B = 2- A+ Math.floor(A/ 4);
338 |
339 | var JD = Math.floor(365.25* (year+ 4716))+ Math.floor(30.6001* (month+ 1))+ day+ B- 1524.5;
340 | return JD;
341 | },
342 |
343 |
344 | //---------------------- Compute Prayer Times -----------------------
345 |
346 |
347 | // compute prayer times at given julian date
348 | computePrayerTimes: function(times) {
349 | times = this.dayPortion(times);
350 | var params = setting;
351 |
352 | var imsak = this.sunAngleTime(this.eval(params.imsak), times.imsak, 'ccw');
353 | var fajr = this.sunAngleTime(this.eval(params.fajr), times.fajr, 'ccw');
354 | var sunrise = this.sunAngleTime(this.riseSetAngle(), times.sunrise, 'ccw');
355 | var dhuhr = this.midDay(times.dhuhr);
356 | var asr = this.asrTime(this.asrFactor(params.asr), times.asr);
357 | var sunset = this.sunAngleTime(this.riseSetAngle(), times.sunset);;
358 | var maghrib = this.sunAngleTime(this.eval(params.maghrib), times.maghrib);
359 | var isha = this.sunAngleTime(this.eval(params.isha), times.isha);
360 |
361 | return {
362 | imsak: imsak, fajr: fajr, sunrise: sunrise, dhuhr: dhuhr,
363 | asr: asr, sunset: sunset, maghrib: maghrib, isha: isha
364 | };
365 | },
366 |
367 |
368 | // compute prayer times
369 | computeTimes: function() {
370 | // default times
371 | var times = {
372 | imsak: 5, fajr: 5, sunrise: 6, dhuhr: 12,
373 | asr: 13, sunset: 18, maghrib: 18, isha: 18
374 | };
375 |
376 | // main iterations
377 | for (var i=1 ; i<=numIterations ; i++)
378 | times = this.computePrayerTimes(times);
379 |
380 | times = this.adjustTimes(times);
381 |
382 | // add midnight time
383 | times.midnight = (setting.midnight == 'Jafari') ?
384 | times.sunset+ this.timeDiff(times.sunset, times.fajr)/ 2 :
385 | times.sunset+ this.timeDiff(times.sunset, times.sunrise)/ 2;
386 |
387 | times = this.tuneTimes(times);
388 | return this.modifyFormats(times);
389 | },
390 |
391 |
392 | // adjust times
393 | adjustTimes: function(times) {
394 | var params = setting;
395 | for (var i in times)
396 | times[i] += timeZone- lng/ 15;
397 |
398 | if (params.highLats != 'None')
399 | times = this.adjustHighLats(times);
400 |
401 | if (this.isMin(params.imsak))
402 | times.imsak = times.fajr- this.eval(params.imsak)/ 60;
403 | if (this.isMin(params.maghrib))
404 | times.maghrib = times.sunset+ this.eval(params.maghrib)/ 60;
405 | if (this.isMin(params.isha))
406 | times.isha = times.maghrib+ this.eval(params.isha)/ 60;
407 | times.dhuhr += this.eval(params.dhuhr)/ 60;
408 |
409 | return times;
410 | },
411 |
412 |
413 | // get asr shadow factor
414 | asrFactor: function(asrParam) {
415 | var factor = {Standard: 1, Hanafi: 2}[asrParam];
416 | return factor || this.eval(asrParam);
417 | },
418 |
419 |
420 | // return sun angle for sunset/sunrise
421 | riseSetAngle: function() {
422 | //var earthRad = 6371009; // in meters
423 | //var angle = DMath.arccos(earthRad/(earthRad+ elv));
424 | var angle = 0.0347* Math.sqrt(elv); // an approximation
425 | return 0.833+ angle;
426 | },
427 |
428 |
429 | // apply offsets to the times
430 | tuneTimes: function(times) {
431 | for (var i in times)
432 | times[i] += offset[i]/ 60;
433 | return times;
434 | },
435 |
436 |
437 | // convert times to given time format
438 | modifyFormats: function(times) {
439 | for (var i in times)
440 | times[i] = this.getFormattedTime(times[i], timeFormat);
441 | return times;
442 | },
443 |
444 |
445 | // adjust times for locations in higher latitudes
446 | adjustHighLats: function(times) {
447 | var params = setting;
448 | var nightTime = this.timeDiff(times.sunset, times.sunrise);
449 |
450 | times.imsak = this.adjustHLTime(times.imsak, times.sunrise, this.eval(params.imsak), nightTime, 'ccw');
451 | times.fajr = this.adjustHLTime(times.fajr, times.sunrise, this.eval(params.fajr), nightTime, 'ccw');
452 | times.isha = this.adjustHLTime(times.isha, times.sunset, this.eval(params.isha), nightTime);
453 | times.maghrib = this.adjustHLTime(times.maghrib, times.sunset, this.eval(params.maghrib), nightTime);
454 |
455 | return times;
456 | },
457 |
458 |
459 | // adjust a time for higher latitudes
460 | adjustHLTime: function(time, base, angle, night, direction) {
461 | var portion = this.nightPortion(angle, night);
462 | var timeDiff = (direction == 'ccw') ?
463 | this.timeDiff(time, base):
464 | this.timeDiff(base, time);
465 | if (isNaN(time) || timeDiff > portion)
466 | time = base+ (direction == 'ccw' ? -portion : portion);
467 | return time;
468 | },
469 |
470 |
471 | // the night portion used for adjusting times in higher latitudes
472 | nightPortion: function(angle, night) {
473 | var method = setting.highLats;
474 | var portion = 1/2 // MidNight
475 | if (method == 'AngleBased')
476 | portion = 1/60* angle;
477 | if (method == 'OneSeventh')
478 | portion = 1/7;
479 | return portion* night;
480 | },
481 |
482 |
483 | // convert hours to day portions
484 | dayPortion: function(times) {
485 | for (var i in times)
486 | times[i] /= 24;
487 | return times;
488 | },
489 |
490 |
491 | //---------------------- Time Zone Functions -----------------------
492 |
493 |
494 | // get local time zone
495 | getTimeZone: function(date) {
496 | var year = date[0];
497 | var t1 = this.gmtOffset([year, 0, 1]);
498 | var t2 = this.gmtOffset([year, 6, 1]);
499 | return Math.min(t1, t2);
500 | },
501 |
502 |
503 | // get daylight saving for a given date
504 | getDst: function(date) {
505 | return 1* (this.gmtOffset(date) != this.getTimeZone(date));
506 | },
507 |
508 |
509 | // GMT offset for a given date
510 | gmtOffset: function(date) {
511 | var localDate = new Date(date[0], date[1]- 1, date[2], 12, 0, 0, 0);
512 | var GMTString = localDate.toGMTString();
513 | var GMTDate = new Date(GMTString.substring(0, GMTString.lastIndexOf(' ')- 1));
514 | var hoursDiff = (localDate- GMTDate) / (1000* 60* 60);
515 | return hoursDiff;
516 | },
517 |
518 |
519 | //---------------------- Misc Functions -----------------------
520 |
521 | // convert given string into a number
522 | eval: function(str) {
523 | return 1* (str+ '').split(/[^0-9.+-]/)[0];
524 | },
525 |
526 |
527 | // detect if input contains 'min'
528 | isMin: function(arg) {
529 | return (arg+ '').indexOf('min') != -1;
530 | },
531 |
532 |
533 | // compute the difference between two times
534 | timeDiff: function(time1, time2) {
535 | return DMath.fixHour(time2- time1);
536 | },
537 |
538 |
539 | // add a leading 0 if necessary
540 | twoDigitsFormat: function(num) {
541 | return (num <10) ? '0'+ num : num;
542 | }
543 |
544 | }}
545 |
546 |
547 |
548 | //---------------------- Degree-Based Math Class -----------------------
549 |
550 |
551 | var DMath = {
552 |
553 | dtr: function(d) { return (d * Math.PI) / 180.0; },
554 | rtd: function(r) { return (r * 180.0) / Math.PI; },
555 |
556 | sin: function(d) { return Math.sin(this.dtr(d)); },
557 | cos: function(d) { return Math.cos(this.dtr(d)); },
558 | tan: function(d) { return Math.tan(this.dtr(d)); },
559 |
560 | arcsin: function(d) { return this.rtd(Math.asin(d)); },
561 | arccos: function(d) { return this.rtd(Math.acos(d)); },
562 | arctan: function(d) { return this.rtd(Math.atan(d)); },
563 |
564 | arccot: function(x) { return this.rtd(Math.atan(1/x)); },
565 | arctan2: function(y, x) { return this.rtd(Math.atan2(y, x)); },
566 |
567 | fixAngle: function(a) { return this.fix(a, 360); },
568 | fixHour: function(a) { return this.fix(a, 24 ); },
569 |
570 | fix: function(a, b) {
571 | a = a- b* (Math.floor(a/ b));
572 | return (a < 0) ? a+ b : a;
573 | }
574 | }
575 |
576 |
577 | //---------------------- Init Object -----------------------
578 |
579 |
580 | var prayTimes = new PrayTimes();
581 |
--------------------------------------------------------------------------------