├── .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 | ![alt text](/.img/azan.png) 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 | --------------------------------------------------------------------------------