feat: add mawaqit plugin (#53)
* feat: add mawaqit plugin * Address review feedback * Address validator errors
This commit is contained in:
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2015-2022 The Reem Kufi Project Authors (https://github.com/aliftype/reem-kufi), with Reserved Font Name "Josefin Sans".
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://openfontlicense.org
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
# Mawaqit
|
||||||
|
|
||||||
|
Prayer times for Noctalia, with a bar widget and panel: live countdown to the next
|
||||||
|
prayer, notifications, optional azan playback, Hijri date, and per-prayer time
|
||||||
|
offsets.
|
||||||
|
|
||||||
|
## Plugin
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
| ------- | ------------------------------------------------------------ |
|
||||||
|
| ID | `ycf/mawaqit` |
|
||||||
|
| Entries | Bar widget: `bar`; panel: `panel`; service: `fetcher` |
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
Install `paplay` (PipeWire/PulseAudio) **or** `pw-cat` on `PATH` — only one is
|
||||||
|
required, used for azan playback. If neither is installed, azan is skipped (a
|
||||||
|
line is logged) and everything else — countdown, panel, notifications — works
|
||||||
|
normally.
|
||||||
|
|
||||||
|
Also requires `pkill` (part of `procps`, present on virtually every distro by
|
||||||
|
default) — used to stop azan playback, since neither player exposes its own
|
||||||
|
stop control.
|
||||||
|
|
||||||
|
Azan audio is **not bundled**. To enable it:
|
||||||
|
|
||||||
|
1. Get your own azan `.mp3` file(s) from wherever you like.
|
||||||
|
2. Copy them into this plugin's `assets/` folder, named exactly `azan1.mp3`,
|
||||||
|
`azan2.mp3`, and/or `azan3.mp3` (only the ones you want to use — you don't
|
||||||
|
need all three).
|
||||||
|
3. In Settings → Plugins → Mawaqit, turn on **Play Azan** and pick which of
|
||||||
|
the three slots to play from **Azan audio**.
|
||||||
|
|
||||||
|
If the selected file isn't present, azan is silently skipped and a line is
|
||||||
|
logged — nothing else is affected.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
- **Left click** the bar widget → open the prayer times panel.
|
||||||
|
- **Right click** the bar widget → cycle its display mode: live countdown →
|
||||||
|
static time → prayer name only.
|
||||||
|
|
||||||
|
Toggle the panel directly:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
noctalia msg panel-toggle ycf/mawaqit:panel
|
||||||
|
```
|
||||||
|
|
||||||
|
The panel shows all five daily prayers plus Sunrise and, during Ramadan, Imsak,
|
||||||
|
with a live countdown banner to whichever is next, the Gregorian and Hijri
|
||||||
|
date, and a refresh button. If azan is playing, a stop button appears next to
|
||||||
|
it.
|
||||||
|
|
||||||
|
## Settings
|
||||||
|
|
||||||
|
Plugin-level (Settings → Plugins → Mawaqit):
|
||||||
|
|
||||||
|
| Setting | Type | Default | Description |
|
||||||
|
| -------------------- | -------- | ------- | -------------------------------------------------------------------------- |
|
||||||
|
| `city` | `string` | `London` | Your city name in English. |
|
||||||
|
| `country` | `string` | `UK` | Country name or 2-letter code. |
|
||||||
|
| `method` | `select` | `3` (MWL) | Calculation authority followed in your region. |
|
||||||
|
| `school` | `select` | `0` (Shafi/Maliki/Hanbali) | Asr convention — Hanafi uses a later shadow factor. |
|
||||||
|
| `hijriDayOffset` | `select` | `0` | Shift the displayed Hijri day by −1/0/+1 if it doesn't match local moon sighting. |
|
||||||
|
| `twelveHourFormat` | `bool` | `false` | Show prayer times as 12-hour (e.g. `5:23 AM`) instead of 24-hour. |
|
||||||
|
| `showNotifications` | `bool` | `true` | Show a system notification when each prayer time begins. |
|
||||||
|
| `playAzan` | `bool` | `false` | Play an azan audio file when each prayer time begins. |
|
||||||
|
| `azanFile` | `select` | `azan1.mp3` | Which bundled azan track to play (see Requirements for setup). |
|
||||||
|
| `tune` | `bool` | `false` | Enable the per-prayer minute offsets below. |
|
||||||
|
| `tuneFajr` | `int` | `0` | Fajr offset, in minutes (−60 to 60). |
|
||||||
|
| `tuneDhuhr` | `int` | `0` | Dhuhr offset, in minutes. |
|
||||||
|
| `tuneAsr` | `int` | `0` | Asr offset, in minutes. |
|
||||||
|
| `tuneMaghrib` | `int` | `0` | Maghrib offset, in minutes. |
|
||||||
|
| `tuneIsha` | `int` | `0` | Isha offset, in minutes. |
|
||||||
|
|
||||||
|
Bar widget settings (from the widget's own settings menu):
|
||||||
|
|
||||||
|
| Setting | Type | Default | Description |
|
||||||
|
| ------------------- | -------- | ------------------- | ------------------------------------------------------------------ |
|
||||||
|
| `showCountdown` | `bool` | `true` | Show a live countdown to the next prayer instead of the static time. |
|
||||||
|
| `showElapsed` | `bool` | `false` | After a prayer begins, count up (`+`) for up to 1 hour. |
|
||||||
|
| `hidePrayerName` | `bool` | `false` | Show only the time or countdown, without the prayer name. |
|
||||||
|
| `widgetIcon` | `glyph` | `building-mosque` | Bar icon. |
|
||||||
|
| `dynamicIcon` | `bool` | `false` | Show a sun/moon icon matching the current prayer instead of the fixed icon. |
|
||||||
|
| `textColor` | `color` | `on_surface` | Bar text color. |
|
||||||
|
| `iconColor` | `color` | `on_surface` | Bar icon color. |
|
||||||
|
| `activeColor` | `color` | `primary` | Color used when a prayer is happening now or during elapsed mode. |
|
||||||
|
|
||||||
|
## IPC
|
||||||
|
|
||||||
|
Force an immediate refetch (both the service and the bar widget respond):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
noctalia msg plugin ycf/mawaqit:fetcher all refresh
|
||||||
|
```
|
||||||
|
|
||||||
|
Set the bar widget's display mode directly:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
noctalia msg plugin ycf/mawaqit:bar all mode countdown|static|name
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The background service fetches prayer times once daily from
|
||||||
|
`api.aladhan.com`, sending the configured city/country/method/school as
|
||||||
|
query parameters, plus a second request for the next day's Fajr time (used
|
||||||
|
for the countdown after Isha).
|
||||||
|
- Azan playback runs `paplay` or `pw-cat` against a file **you supply** (see
|
||||||
|
Requirements) — no audio is bundled with this plugin. Playback is stopped
|
||||||
|
by matching the exact file path being played (via `pkill -f`), not a
|
||||||
|
generic pattern — this is the only termination method available since the
|
||||||
|
plugin API doesn't currently expose a PID or stop handle for spawned
|
||||||
|
processes. Stopping happens when the plugin exits or is disabled, or
|
||||||
|
manually from the panel while azan is playing.
|
||||||
|
- The Arabic Hijri date and prayer-time banner are rendered with the bundled
|
||||||
|
Reem Kufi font (`ReemKufi.ttf`), licensed under the SIL Open Font License —
|
||||||
|
see `OFL.txt`.
|
||||||
|
- No compositor-specific behavior — works anywhere Noctalia's bar and panels do.
|
||||||
Binary file not shown.
@@ -0,0 +1,289 @@
|
|||||||
|
--!nonstrict
|
||||||
|
-- bar_widget.luau — Mawaqit bar widget
|
||||||
|
-- Left click → toggle panel
|
||||||
|
-- Right click → cycle display mode: countdown → static time → prayer name only
|
||||||
|
|
||||||
|
-- ── Config ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
local showCountdown = noctalia.getConfig("showCountdown")
|
||||||
|
local showElapsed = noctalia.getConfig("showElapsed")
|
||||||
|
local hidePrayerName = noctalia.getConfig("hidePrayerName")
|
||||||
|
local widgetIcon = noctalia.getConfig("widgetIcon") or "building-mosque"
|
||||||
|
local dynamicIcon = noctalia.getConfig("dynamicIcon")
|
||||||
|
local textColor = noctalia.getConfig("textColor") or "on_surface"
|
||||||
|
local iconColor = noctalia.getConfig("iconColor") or "on_surface"
|
||||||
|
local activeColor = noctalia.getConfig("activeColor") or "primary"
|
||||||
|
|
||||||
|
showCountdown = (showCountdown ~= false and showCountdown ~= "false")
|
||||||
|
showElapsed = (showElapsed == true or showElapsed == "true")
|
||||||
|
hidePrayerName = (hidePrayerName == true or hidePrayerName == "true")
|
||||||
|
dynamicIcon = (dynamicIcon == true or dynamicIcon == "true")
|
||||||
|
|
||||||
|
-- ── Constants ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
local PRAYER_ICONS = {
|
||||||
|
Fajr = "moon-stars",
|
||||||
|
Sunrise = "sunrise",
|
||||||
|
Dhuhr = "sun-high",
|
||||||
|
Asr = "sun-low",
|
||||||
|
Maghrib = "sunset",
|
||||||
|
Isha = "moon",
|
||||||
|
}
|
||||||
|
|
||||||
|
local MODE_COUNTDOWN = 1
|
||||||
|
local MODE_STATIC = 2
|
||||||
|
local MODE_NAME_ONLY = 3
|
||||||
|
|
||||||
|
-- ── State ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
local prayers = {}
|
||||||
|
local tomorrowFajr = nil
|
||||||
|
local displayMode = MODE_COUNTDOWN
|
||||||
|
local hasData = false
|
||||||
|
local errorMsg = ""
|
||||||
|
local isJumuah = false
|
||||||
|
|
||||||
|
-- ── Helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
local function nowSeconds()
|
||||||
|
local t = os.date("*t")
|
||||||
|
return t.hour * 3600 + t.min * 60 + t.sec
|
||||||
|
end
|
||||||
|
|
||||||
|
local function formatCountdown(secs)
|
||||||
|
if secs < 0 then secs = 0 end
|
||||||
|
local h = math.floor(secs / 3600)
|
||||||
|
local m = math.floor((secs % 3600) / 60)
|
||||||
|
local s = secs % 60
|
||||||
|
if h > 0 then return string.format("%d:%02d:%02d", h, m, s) end
|
||||||
|
return string.format("%d:%02d", m, s)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function formatCountdownShort(secs)
|
||||||
|
if secs < 0 then secs = 0 end
|
||||||
|
local h = math.floor(secs / 3600)
|
||||||
|
local m = math.floor((secs % 3600) / 60)
|
||||||
|
if h > 0 then return string.format("%dh %dm", h, m) end
|
||||||
|
if m > 0 then return string.format("%dm", m) end
|
||||||
|
return "soon"
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getIcon(prayerName)
|
||||||
|
if dynamicIcon and prayerName and PRAYER_ICONS[prayerName] then
|
||||||
|
return PRAYER_ICONS[prayerName]
|
||||||
|
end
|
||||||
|
return widgetIcon ~= "" and widgetIcon or "building-mosque"
|
||||||
|
end
|
||||||
|
|
||||||
|
local function setColors(isActive)
|
||||||
|
if isActive then
|
||||||
|
barWidget.setColor(activeColor ~= "" and activeColor or "primary")
|
||||||
|
barWidget.setGlyphColor(activeColor ~= "" and activeColor or "primary")
|
||||||
|
else
|
||||||
|
barWidget.setColor(textColor ~= "" and textColor or "on_surface")
|
||||||
|
barWidget.setGlyphColor(iconColor ~= "" and iconColor or "on_surface")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function prayerLabel(name)
|
||||||
|
if name == "Dhuhr" and isJumuah then return "Jumu'ah" end
|
||||||
|
return name
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ── Prayer logic ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
local function findCurrentPrayer()
|
||||||
|
if #prayers == 0 then return nil, nil, nil, nil end
|
||||||
|
local now = nowSeconds()
|
||||||
|
local currentIdx = nil
|
||||||
|
for i, p in ipairs(prayers) do
|
||||||
|
if now >= p.seconds then currentIdx = i end
|
||||||
|
end
|
||||||
|
|
||||||
|
local nextIdx, secondsToNext, secondsElapsed
|
||||||
|
|
||||||
|
if currentIdx == nil then
|
||||||
|
nextIdx = 1
|
||||||
|
secondsToNext = prayers[1].seconds - now
|
||||||
|
secondsElapsed = nil
|
||||||
|
elseif currentIdx == #prayers then
|
||||||
|
nextIdx = nil
|
||||||
|
secondsElapsed = now - prayers[currentIdx].seconds
|
||||||
|
secondsToNext = tomorrowFajr and (tomorrowFajr + 86400 - now) or nil
|
||||||
|
else
|
||||||
|
nextIdx = currentIdx + 1
|
||||||
|
secondsToNext = prayers[nextIdx].seconds - now
|
||||||
|
secondsElapsed = now - prayers[currentIdx].seconds
|
||||||
|
end
|
||||||
|
|
||||||
|
return currentIdx, nextIdx, secondsToNext, secondsElapsed
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ── Display ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
local function updateDisplay()
|
||||||
|
if not hasData then
|
||||||
|
barWidget.setGlyph(widgetIcon ~= "" and widgetIcon or "building-mosque")
|
||||||
|
barWidget.setText(errorMsg ~= "" and "!" or "...")
|
||||||
|
barWidget.setColor("on_surface")
|
||||||
|
barWidget.setGlyphColor("on_surface")
|
||||||
|
barWidget.setTooltip(errorMsg ~= "" and errorMsg or noctalia.tr("panel.tooltip.no_data"))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local currentIdx, nextIdx, secondsToNext, secondsElapsed = findCurrentPrayer()
|
||||||
|
local displayPrayer, isActive, timeStr = nil, false, ""
|
||||||
|
|
||||||
|
local showingElapsed = false
|
||||||
|
if showElapsed and currentIdx ~= nil and secondsElapsed ~= nil then
|
||||||
|
local cap = math.min(3600, secondsToNext or 3600)
|
||||||
|
if secondsElapsed >= 0 and secondsElapsed <= cap then
|
||||||
|
showingElapsed = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if nextIdx ~= nil then
|
||||||
|
displayPrayer = prayers[nextIdx].name
|
||||||
|
if showingElapsed and currentIdx ~= nil then
|
||||||
|
displayPrayer = prayers[currentIdx].name
|
||||||
|
isActive = true
|
||||||
|
if displayMode == MODE_COUNTDOWN then
|
||||||
|
timeStr = "+" .. formatCountdown(secondsElapsed)
|
||||||
|
elseif displayMode == MODE_STATIC then
|
||||||
|
timeStr = prayers[currentIdx].time
|
||||||
|
end
|
||||||
|
elseif secondsToNext ~= nil and secondsToNext <= 60 then
|
||||||
|
isActive = true
|
||||||
|
if displayMode ~= MODE_NAME_ONLY then timeStr = "now" end
|
||||||
|
else
|
||||||
|
if displayMode == MODE_COUNTDOWN and showCountdown and secondsToNext then
|
||||||
|
timeStr = formatCountdown(secondsToNext)
|
||||||
|
elseif displayMode == MODE_STATIC then
|
||||||
|
timeStr = prayers[nextIdx].time
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
displayPrayer = "Fajr"
|
||||||
|
if showingElapsed and currentIdx ~= nil then
|
||||||
|
isActive = true
|
||||||
|
displayPrayer = prayers[currentIdx].name
|
||||||
|
if displayMode == MODE_COUNTDOWN then
|
||||||
|
timeStr = "+" .. formatCountdown(secondsElapsed)
|
||||||
|
elseif displayMode == MODE_STATIC then
|
||||||
|
timeStr = prayers[currentIdx].time
|
||||||
|
end
|
||||||
|
elseif secondsToNext ~= nil then
|
||||||
|
if displayMode == MODE_COUNTDOWN and showCountdown then
|
||||||
|
timeStr = formatCountdown(secondsToNext)
|
||||||
|
elseif displayMode == MODE_STATIC then
|
||||||
|
timeStr = tomorrowFajr and prayers[1] and prayers[1].time or "?"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local nameLabel = prayerLabel(displayPrayer or "")
|
||||||
|
local label
|
||||||
|
if displayMode == MODE_NAME_ONLY then
|
||||||
|
label = nameLabel
|
||||||
|
elseif hidePrayerName then
|
||||||
|
label = timeStr
|
||||||
|
else
|
||||||
|
label = timeStr ~= "" and (nameLabel .. " " .. timeStr) or nameLabel
|
||||||
|
end
|
||||||
|
|
||||||
|
barWidget.setGlyph(getIcon(displayPrayer))
|
||||||
|
barWidget.setText(label)
|
||||||
|
setColors(isActive)
|
||||||
|
|
||||||
|
if nextIdx ~= nil and secondsToNext ~= nil then
|
||||||
|
barWidget.setTooltip({
|
||||||
|
{ key = noctalia.tr("panel.tooltip.next"), value = prayerLabel(prayers[nextIdx].name) },
|
||||||
|
{ key = noctalia.tr("panel.tooltip.time"), value = prayers[nextIdx].time },
|
||||||
|
{ key = noctalia.tr("panel.tooltip.in"), value = formatCountdownShort(secondsToNext) },
|
||||||
|
})
|
||||||
|
elseif tomorrowFajr ~= nil and secondsToNext ~= nil then
|
||||||
|
barWidget.setTooltip({
|
||||||
|
{ key = noctalia.tr("panel.tooltip.next"), value = noctalia.tr("panel.tooltip.tomorrow_fajr") },
|
||||||
|
{ key = noctalia.tr("panel.tooltip.in"), value = formatCountdownShort(secondsToNext) },
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ── State watchers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
noctalia.state.watch("prayers", function(val)
|
||||||
|
if type(val) == "table" and #val > 0 then
|
||||||
|
prayers = val
|
||||||
|
hasData = true
|
||||||
|
errorMsg = ""
|
||||||
|
end
|
||||||
|
updateDisplay()
|
||||||
|
end)
|
||||||
|
|
||||||
|
noctalia.state.watch("tomorrowFajr", function(val)
|
||||||
|
tomorrowFajr = tonumber(val)
|
||||||
|
end)
|
||||||
|
|
||||||
|
noctalia.state.watch("isJumuah", function(val)
|
||||||
|
isJumuah = (val == true)
|
||||||
|
end)
|
||||||
|
|
||||||
|
noctalia.state.watch("error", function(val)
|
||||||
|
if type(val) == "string" and val ~= "" then
|
||||||
|
errorMsg = val
|
||||||
|
if not hasData then updateDisplay() end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- ── Lifecycle ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
noctalia.setUpdateInterval(1000)
|
||||||
|
|
||||||
|
function update() updateDisplay() end
|
||||||
|
|
||||||
|
function onClick()
|
||||||
|
noctalia.togglePanel("ycf/mawaqit:panel")
|
||||||
|
end
|
||||||
|
|
||||||
|
function onRightClick()
|
||||||
|
if displayMode == MODE_COUNTDOWN then
|
||||||
|
displayMode = MODE_STATIC
|
||||||
|
elseif displayMode == MODE_STATIC then
|
||||||
|
displayMode = MODE_NAME_ONLY
|
||||||
|
else
|
||||||
|
displayMode = MODE_COUNTDOWN
|
||||||
|
end
|
||||||
|
updateDisplay()
|
||||||
|
end
|
||||||
|
|
||||||
|
function onIpc(event, payload)
|
||||||
|
if event == "refresh" then
|
||||||
|
noctalia.state.set("command", { action = "refresh" })
|
||||||
|
elseif event == "mode" then
|
||||||
|
if payload == "countdown" then displayMode = MODE_COUNTDOWN
|
||||||
|
elseif payload == "static" then displayMode = MODE_STATIC
|
||||||
|
elseif payload == "name" then displayMode = MODE_NAME_ONLY
|
||||||
|
end
|
||||||
|
updateDisplay()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ── Init ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
barWidget.setGlyph(widgetIcon ~= "" and widgetIcon or "building-mosque")
|
||||||
|
barWidget.setText("...")
|
||||||
|
barWidget.setColor("on_surface")
|
||||||
|
barWidget.setGlyphColor("on_surface")
|
||||||
|
barWidget.setTooltip(noctalia.tr("plugin_name"))
|
||||||
|
|
||||||
|
local existing = noctalia.state.get("prayers")
|
||||||
|
if type(existing) == "table" and #existing > 0 then
|
||||||
|
prayers = existing
|
||||||
|
hasData = true
|
||||||
|
end
|
||||||
|
local existingTomorrow = noctalia.state.get("tomorrowFajr")
|
||||||
|
if existingTomorrow then tomorrowFajr = tonumber(existingTomorrow) end
|
||||||
|
local existingJumuah = noctalia.state.get("isJumuah")
|
||||||
|
if existingJumuah ~= nil then isJumuah = (existingJumuah == true) end
|
||||||
|
|
||||||
|
updateDisplay()
|
||||||
@@ -0,0 +1,319 @@
|
|||||||
|
--!nonstrict
|
||||||
|
-- panel.luau — Mawaqit prayer times panel (clean, no azan controls beyond stop)
|
||||||
|
|
||||||
|
-- ── State ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
local prayers = {}
|
||||||
|
local tomorrowFajr = nil
|
||||||
|
local hijriDate = ""
|
||||||
|
local hijriDateAr = ""
|
||||||
|
local gregorianDate = ""
|
||||||
|
local errorMsg = ""
|
||||||
|
local hasData = false
|
||||||
|
local hijriMonth = 0
|
||||||
|
local isJumuah = false
|
||||||
|
local sunriseTime = ""
|
||||||
|
local imsakTime = ""
|
||||||
|
local azanPlaying = false
|
||||||
|
|
||||||
|
local decoFont = noctalia.loadFont("ReemKufi.ttf")
|
||||||
|
|
||||||
|
local PRAYER_ICONS = {
|
||||||
|
Fajr = "moon-stars",
|
||||||
|
Sunrise = "sunrise",
|
||||||
|
Dhuhr = "sun-high",
|
||||||
|
Asr = "sun-low",
|
||||||
|
Maghrib = "sunset",
|
||||||
|
Isha = "moon",
|
||||||
|
Imsak = "moon-off",
|
||||||
|
}
|
||||||
|
|
||||||
|
local ARABIC_NAMES = {
|
||||||
|
Fajr = "الفجر",
|
||||||
|
Dhuhr = "الظهر",
|
||||||
|
Asr = "العصر",
|
||||||
|
Maghrib = "المغرب",
|
||||||
|
Isha = "العشاء",
|
||||||
|
Imsak = "الإمساك",
|
||||||
|
}
|
||||||
|
|
||||||
|
-- ── Helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
local function nowSeconds()
|
||||||
|
local t = os.date("*t")
|
||||||
|
return t.hour * 3600 + t.min * 60 + t.sec
|
||||||
|
end
|
||||||
|
|
||||||
|
local function formatCountdown(secs)
|
||||||
|
if secs < 0 then secs = 0 end
|
||||||
|
local h = math.floor(secs / 3600)
|
||||||
|
local m = math.floor((secs % 3600) / 60)
|
||||||
|
local s = secs % 60
|
||||||
|
if h > 0 then return string.format("%dh %02dm %02ds", h, m, s) end
|
||||||
|
if m > 0 then return string.format("%dm %02ds", m, s) end
|
||||||
|
return string.format("%ds", s)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function findNext()
|
||||||
|
if #prayers == 0 then return nil, nil end
|
||||||
|
local now = nowSeconds()
|
||||||
|
for _, p in ipairs(prayers) do
|
||||||
|
if p.seconds > now then return p, p.seconds - now end
|
||||||
|
end
|
||||||
|
if tomorrowFajr then
|
||||||
|
return { name = "Fajr", time = prayers[1] and prayers[1].time or "?", seconds = tomorrowFajr },
|
||||||
|
(86400 - now) + tomorrowFajr
|
||||||
|
end
|
||||||
|
return prayers[1], nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function prayerLabel(name)
|
||||||
|
if name == "Dhuhr" and isJumuah then return "Jumu'ah" end
|
||||||
|
return name
|
||||||
|
end
|
||||||
|
|
||||||
|
local function accentFor(name)
|
||||||
|
if hijriMonth == 9 then
|
||||||
|
if name == "Imsak" then return "secondary" end
|
||||||
|
if name == "Maghrib" then return "tertiary" end
|
||||||
|
end
|
||||||
|
return "primary"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ── Render ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
local function render()
|
||||||
|
local nextPrayer, secondsToNext = findNext()
|
||||||
|
|
||||||
|
-- ── Header ────────────────────────────────────────────────────────────────
|
||||||
|
local header = ui.row({ align = "center", justify = "space_between" }, {
|
||||||
|
ui.row({ align = "center", gap = 8 }, {
|
||||||
|
ui.glyph({ name = "building-mosque", size = 18, color = "primary" }),
|
||||||
|
ui.label({ text = noctalia.tr("panel.title"), fontSize = 15, fontWeight = "semibold", color = "on_surface" }),
|
||||||
|
}),
|
||||||
|
ui.row({ align = "center", gap = 4 }, {
|
||||||
|
azanPlaying and ui.button({
|
||||||
|
glyph = "player-stop-filled",
|
||||||
|
variant = "destructive",
|
||||||
|
onClick = "onStopAzan",
|
||||||
|
}) or ui.spacer({ width = 0 }),
|
||||||
|
ui.button({ glyph = "refresh", variant = "ghost", onClick = "onRefresh" }),
|
||||||
|
ui.button({ glyph = "x", variant = "ghost", onClick = "onClose" }),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
-- ── Date row ──────────────────────────────────────────────────────────────
|
||||||
|
local dateRow = nil
|
||||||
|
if gregorianDate ~= "" or hijriDate ~= "" then
|
||||||
|
local hijriLabel
|
||||||
|
if decoFont and decoFont ~= "" and hijriDateAr ~= "" then
|
||||||
|
hijriLabel = ui.label({
|
||||||
|
text = hijriDateAr,
|
||||||
|
fontSize = 26,
|
||||||
|
fontFamily = decoFont,
|
||||||
|
color = hijriMonth == 9 and "primary" or "secondary",
|
||||||
|
})
|
||||||
|
else
|
||||||
|
hijriLabel = ui.label({
|
||||||
|
text = hijriDate,
|
||||||
|
fontSize = 13,
|
||||||
|
fontWeight = "semibold",
|
||||||
|
color = hijriMonth == 9 and "primary" or "secondary",
|
||||||
|
})
|
||||||
|
end
|
||||||
|
dateRow = ui.row({ align = "center", justify = "space_between" }, {
|
||||||
|
ui.label({ text = gregorianDate, fontSize = 12, color = "on_surface_variant" }),
|
||||||
|
hijriLabel,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ── Countdown banner — plain text, no card ────────────────────────────────
|
||||||
|
local banner = nil
|
||||||
|
if nextPrayer and secondsToNext then
|
||||||
|
local isNow = secondsToNext <= 60
|
||||||
|
local aColor = accentFor(nextPrayer.name)
|
||||||
|
local label = prayerLabel(nextPrayer.name)
|
||||||
|
|
||||||
|
if isNow then
|
||||||
|
local arName = ARABIC_NAMES[nextPrayer.name] or nextPrayer.name
|
||||||
|
local bannerChildren = {}
|
||||||
|
if decoFont and decoFont ~= "" then
|
||||||
|
bannerChildren[#bannerChildren+1] = ui.label({
|
||||||
|
text = "حان الآن موعد صلاة " .. arName,
|
||||||
|
fontSize = 20,
|
||||||
|
fontFamily = decoFont,
|
||||||
|
color = aColor,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
bannerChildren[#bannerChildren+1] = ui.label({
|
||||||
|
text = label .. " — " .. noctalia.tr("panel.now"),
|
||||||
|
fontSize = 12,
|
||||||
|
color = "on_surface_variant",
|
||||||
|
})
|
||||||
|
banner = ui.column({ align = "center", gap = 4, paddingV = 6 }, bannerChildren)
|
||||||
|
else
|
||||||
|
banner = ui.column({ align = "center", gap = 2, paddingV = 6 }, {
|
||||||
|
ui.label({ text = label .. " " .. noctalia.tr("panel.in"), fontSize = 12, color = "on_surface_variant" }),
|
||||||
|
ui.label({ text = formatCountdown(secondsToNext), fontSize = 26,
|
||||||
|
fontWeight = "bold", color = aColor }),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
elseif not hasData then
|
||||||
|
banner = ui.column({ align = "center", paddingV = 6 }, {
|
||||||
|
ui.label({
|
||||||
|
text = errorMsg ~= "" and errorMsg or noctalia.tr("panel.loading"),
|
||||||
|
fontSize = 13,
|
||||||
|
color = errorMsg ~= "" and "error" or "on_surface_variant",
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ── Prayer rows ───────────────────────────────────────────────────────────
|
||||||
|
local rows = {}
|
||||||
|
|
||||||
|
-- Imsak (Ramadan only)
|
||||||
|
if hijriMonth == 9 and imsakTime ~= "" then
|
||||||
|
rows[#rows+1] = ui.row({
|
||||||
|
key = "Imsak", align = "center", justify = "space_between",
|
||||||
|
paddingH = 12, paddingV = 8,
|
||||||
|
fill = "secondary/0.15", radius = 8,
|
||||||
|
}, {
|
||||||
|
ui.row({ align = "center", gap = 10 }, {
|
||||||
|
ui.glyph({ name = "moon-off", size = 14, color = "secondary" }),
|
||||||
|
ui.label({ text = "Imsak", fontSize = 14, color = "secondary" }),
|
||||||
|
}),
|
||||||
|
ui.label({ text = imsakTime, fontSize = 14, color = "secondary" }),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, p in ipairs(prayers) do
|
||||||
|
local isNext = nextPrayer and p.name == nextPrayer.name
|
||||||
|
local aColor = accentFor(p.name)
|
||||||
|
local nameClr = isNext and aColor or "on_surface"
|
||||||
|
local timeClr = isNext and aColor or "on_surface_variant"
|
||||||
|
local weight = isNext and "semibold" or "normal"
|
||||||
|
local icon = PRAYER_ICONS[p.name] or "sun"
|
||||||
|
local label = prayerLabel(p.name)
|
||||||
|
|
||||||
|
rows[#rows+1] = ui.row({
|
||||||
|
key = p.name,
|
||||||
|
align = "center", justify = "space_between",
|
||||||
|
paddingH = 12,
|
||||||
|
paddingV = isNext and 10 or 8,
|
||||||
|
fill = isNext and "surface_variant" or nil,
|
||||||
|
radius = isNext and 10 or nil,
|
||||||
|
}, {
|
||||||
|
ui.row({ align = "center", gap = 10 }, {
|
||||||
|
ui.glyph({ name = icon, size = 14, color = nameClr }),
|
||||||
|
ui.label({ text = label, fontSize = 14, fontWeight = weight, color = nameClr }),
|
||||||
|
}),
|
||||||
|
ui.label({ text = p.time, fontSize = 14, fontWeight = weight, color = timeClr }),
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Sunrise between Fajr and Dhuhr
|
||||||
|
if p.name == "Fajr" and sunriseTime ~= "" then
|
||||||
|
rows[#rows+1] = ui.row({
|
||||||
|
key = "Sunrise", align = "center", justify = "space_between",
|
||||||
|
paddingH = 12, paddingV = 8,
|
||||||
|
}, {
|
||||||
|
ui.row({ align = "center", gap = 10 }, {
|
||||||
|
ui.glyph({ name = "sunrise", size = 14, color = "on_surface_variant" }),
|
||||||
|
ui.label({ text = noctalia.tr("panel.sunrise"), fontSize = 14, color = "on_surface_variant" }),
|
||||||
|
}),
|
||||||
|
ui.label({ text = sunriseTime, fontSize = 14, color = "on_surface_variant" }),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ── Assemble ──────────────────────────────────────────────────────────────
|
||||||
|
local children = { header }
|
||||||
|
if dateRow then children[#children+1] = dateRow end
|
||||||
|
children[#children+1] = ui.separator({ spacing = 4, color = "outline", opacity = 0.2 })
|
||||||
|
if banner then children[#children+1] = banner end
|
||||||
|
children[#children+1] = ui.separator({ spacing = 4, color = "outline", opacity = 0.15 })
|
||||||
|
if #rows > 0 then children[#children+1] = ui.column({ gap = 1 }, rows) end
|
||||||
|
|
||||||
|
panel.render(ui.column({ flexGrow = 1, gap = 8 }, children))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ── State watchers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
noctalia.state.watch("prayers", function(val)
|
||||||
|
if type(val) == "table" then prayers = val; hasData = #val > 0 end
|
||||||
|
render()
|
||||||
|
end)
|
||||||
|
noctalia.state.watch("tomorrowFajr", function(val)
|
||||||
|
tomorrowFajr = tonumber(val); render()
|
||||||
|
end)
|
||||||
|
noctalia.state.watch("hijriDate", function(val)
|
||||||
|
if type(val) == "string" then hijriDate = val end; render()
|
||||||
|
end)
|
||||||
|
noctalia.state.watch("hijriDateAr", function(val)
|
||||||
|
if type(val) == "string" then hijriDateAr = val end; render()
|
||||||
|
end)
|
||||||
|
noctalia.state.watch("gregorianDate", function(val)
|
||||||
|
if type(val) == "string" then gregorianDate = val end; render()
|
||||||
|
end)
|
||||||
|
noctalia.state.watch("hijriMonth", function(val)
|
||||||
|
hijriMonth = tonumber(val) or 0; render()
|
||||||
|
end)
|
||||||
|
noctalia.state.watch("isJumuah", function(val)
|
||||||
|
isJumuah = (val == true); render()
|
||||||
|
end)
|
||||||
|
noctalia.state.watch("sunriseTime", function(val)
|
||||||
|
if type(val) == "string" then sunriseTime = val end; render()
|
||||||
|
end)
|
||||||
|
noctalia.state.watch("imsakTime", function(val)
|
||||||
|
if type(val) == "string" then imsakTime = val end; render()
|
||||||
|
end)
|
||||||
|
noctalia.state.watch("azanPlaying", function(val)
|
||||||
|
azanPlaying = (val == true); render()
|
||||||
|
end)
|
||||||
|
noctalia.state.watch("error", function(val)
|
||||||
|
if type(val) == "string" then errorMsg = val end
|
||||||
|
if not hasData then render() end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- ── Callbacks ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function onOpen(_context)
|
||||||
|
panel.setWantsSecondTicks(true)
|
||||||
|
local p = noctalia.state.get("prayers")
|
||||||
|
if type(p) == "table" then prayers = p; hasData = #p > 0 end
|
||||||
|
local tf = noctalia.state.get("tomorrowFajr")
|
||||||
|
if tf then tomorrowFajr = tonumber(tf) end
|
||||||
|
local hd = noctalia.state.get("hijriDate")
|
||||||
|
if type(hd) == "string" then hijriDate = hd end
|
||||||
|
local hdar = noctalia.state.get("hijriDateAr")
|
||||||
|
if type(hdar) == "string" then hijriDateAr = hdar end
|
||||||
|
local gd = noctalia.state.get("gregorianDate")
|
||||||
|
if type(gd) == "string" then gregorianDate = gd end
|
||||||
|
local hm = noctalia.state.get("hijriMonth")
|
||||||
|
if hm then hijriMonth = tonumber(hm) or 0 end
|
||||||
|
local jm = noctalia.state.get("isJumuah")
|
||||||
|
if jm ~= nil then isJumuah = (jm == true) end
|
||||||
|
local sr = noctalia.state.get("sunriseTime")
|
||||||
|
if type(sr) == "string" then sunriseTime = sr end
|
||||||
|
local im = noctalia.state.get("imsakTime")
|
||||||
|
if type(im) == "string" then imsakTime = im end
|
||||||
|
local ap = noctalia.state.get("azanPlaying")
|
||||||
|
if ap ~= nil then azanPlaying = (ap == true) end
|
||||||
|
render()
|
||||||
|
end
|
||||||
|
|
||||||
|
function onClose() panel.close() end
|
||||||
|
function onRefresh()
|
||||||
|
noctalia.state.set("command", { action = "refresh" })
|
||||||
|
end
|
||||||
|
|
||||||
|
function onStopAzan()
|
||||||
|
noctalia.state.set("command", { action = "stopAzan" })
|
||||||
|
azanPlaying = false
|
||||||
|
noctalia.state.set("azanPlaying", false)
|
||||||
|
render()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- update() is called every second when panel is open (via panel.setWantsSecondTicks)
|
||||||
|
function update()
|
||||||
|
if hasData then render() end
|
||||||
|
end
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
id = "ycf/mawaqit"
|
||||||
|
name = "Mawaqit"
|
||||||
|
version = "1.0.0"
|
||||||
|
plugin_api = 3
|
||||||
|
author = "ycf-anon"
|
||||||
|
license = "MIT" # Applies to the plugin code. Bundled font (ReemKufi.ttf) is
|
||||||
|
# SIL Open Font License — see OFL.txt.
|
||||||
|
tags = ["bar", "panel", "service", "time", "utility"]
|
||||||
|
icon = "building-mosque"
|
||||||
|
description = "Prayer times with live countdown, notifications, and azan."
|
||||||
|
dependencies = ["paplay", "pw-cat", "pkill"]
|
||||||
|
|
||||||
|
# ── Plugin-level settings ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[[setting]]
|
||||||
|
key = "city"
|
||||||
|
type = "string"
|
||||||
|
label_key = "settings.city.label"
|
||||||
|
description_key = "settings.city.description"
|
||||||
|
default = "London"
|
||||||
|
|
||||||
|
[[setting]]
|
||||||
|
key = "country"
|
||||||
|
type = "string"
|
||||||
|
label_key = "settings.country.label"
|
||||||
|
description_key = "settings.country.description"
|
||||||
|
default = "UK"
|
||||||
|
|
||||||
|
[[setting]]
|
||||||
|
key = "method"
|
||||||
|
type = "select"
|
||||||
|
label_key = "settings.method.label"
|
||||||
|
description_key = "settings.method.description"
|
||||||
|
default = "3"
|
||||||
|
options = [
|
||||||
|
{ value = "1", label_key = "settings.method.karachi" },
|
||||||
|
{ value = "2", label_key = "settings.method.isna" },
|
||||||
|
{ value = "3", label_key = "settings.method.mwl" },
|
||||||
|
{ value = "4", label_key = "settings.method.makkah" },
|
||||||
|
{ value = "5", label_key = "settings.method.egypt" },
|
||||||
|
{ value = "7", label_key = "settings.method.tehran" },
|
||||||
|
{ value = "8", label_key = "settings.method.gulf" },
|
||||||
|
{ value = "9", label_key = "settings.method.kuwait" },
|
||||||
|
{ value = "10", label_key = "settings.method.qatar" },
|
||||||
|
{ value = "11", label_key = "settings.method.singapore" },
|
||||||
|
{ value = "12", label_key = "settings.method.france" },
|
||||||
|
{ value = "13", label_key = "settings.method.turkey" },
|
||||||
|
{ value = "14", label_key = "settings.method.russia" },
|
||||||
|
{ value = "15", label_key = "settings.method.moonsighting" },
|
||||||
|
{ value = "16", label_key = "settings.method.dubai" },
|
||||||
|
{ value = "17", label_key = "settings.method.malaysia" },
|
||||||
|
{ value = "18", label_key = "settings.method.tunisia" },
|
||||||
|
{ value = "19", label_key = "settings.method.algeria" },
|
||||||
|
{ value = "20", label_key = "settings.method.indonesia" },
|
||||||
|
{ value = "21", label_key = "settings.method.morocco" },
|
||||||
|
{ value = "22", label_key = "settings.method.portugal" },
|
||||||
|
{ value = "23", label_key = "settings.method.jordan" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[setting]]
|
||||||
|
key = "school"
|
||||||
|
type = "select"
|
||||||
|
label_key = "settings.school.label"
|
||||||
|
description_key = "settings.school.description"
|
||||||
|
default = "0"
|
||||||
|
options = [
|
||||||
|
{ value = "0", label_key = "settings.school.shafi" },
|
||||||
|
{ value = "1", label_key = "settings.school.hanafi" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[setting]]
|
||||||
|
key = "hijriDayOffset"
|
||||||
|
type = "select"
|
||||||
|
label_key = "settings.hijri_day_offset.label"
|
||||||
|
description_key = "settings.hijri_day_offset.description"
|
||||||
|
default = "0"
|
||||||
|
options = [
|
||||||
|
{ value = "-1", label_key = "settings.hijri_day_offset.minus1" },
|
||||||
|
{ value = "0", label_key = "settings.hijri_day_offset.default" },
|
||||||
|
{ value = "1", label_key = "settings.hijri_day_offset.plus1" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[setting]]
|
||||||
|
key = "twelveHourFormat"
|
||||||
|
type = "bool"
|
||||||
|
label_key = "settings.twelve_hour_format.label"
|
||||||
|
description_key = "settings.twelve_hour_format.description"
|
||||||
|
default = false
|
||||||
|
|
||||||
|
[[setting]]
|
||||||
|
key = "showNotifications"
|
||||||
|
type = "bool"
|
||||||
|
label_key = "settings.show_notifications.label"
|
||||||
|
description_key = "settings.show_notifications.description"
|
||||||
|
default = true
|
||||||
|
|
||||||
|
[[setting]]
|
||||||
|
key = "playAzan"
|
||||||
|
type = "bool"
|
||||||
|
label_key = "settings.play_azan.label"
|
||||||
|
description_key = "settings.play_azan.description"
|
||||||
|
default = false
|
||||||
|
|
||||||
|
[[setting]]
|
||||||
|
key = "azanFile"
|
||||||
|
type = "select"
|
||||||
|
label_key = "settings.azan_file.label"
|
||||||
|
description_key = "settings.azan_file.description"
|
||||||
|
default = "azan1.mp3"
|
||||||
|
options = [
|
||||||
|
{ value = "azan1.mp3", label_key = "settings.azan_file.azan1" },
|
||||||
|
{ value = "azan2.mp3", label_key = "settings.azan_file.azan2" },
|
||||||
|
{ value = "azan3.mp3", label_key = "settings.azan_file.azan3" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[setting]]
|
||||||
|
key = "tune"
|
||||||
|
type = "bool"
|
||||||
|
label_key = "settings.tune.label"
|
||||||
|
description_key = "settings.tune.description"
|
||||||
|
default = false
|
||||||
|
|
||||||
|
[[setting]]
|
||||||
|
key = "tuneFajr"
|
||||||
|
type = "int"
|
||||||
|
label_key = "settings.tune_fajr.label"
|
||||||
|
default = 0
|
||||||
|
min = -60
|
||||||
|
max = 60
|
||||||
|
advanced = true
|
||||||
|
visible_when = { key = "tune", values = ["true"] }
|
||||||
|
|
||||||
|
[[setting]]
|
||||||
|
key = "tuneDhuhr"
|
||||||
|
type = "int"
|
||||||
|
label_key = "settings.tune_dhuhr.label"
|
||||||
|
default = 0
|
||||||
|
min = -60
|
||||||
|
max = 60
|
||||||
|
advanced = true
|
||||||
|
visible_when = { key = "tune", values = ["true"] }
|
||||||
|
|
||||||
|
[[setting]]
|
||||||
|
key = "tuneAsr"
|
||||||
|
type = "int"
|
||||||
|
label_key = "settings.tune_asr.label"
|
||||||
|
default = 0
|
||||||
|
min = -60
|
||||||
|
max = 60
|
||||||
|
advanced = true
|
||||||
|
visible_when = { key = "tune", values = ["true"] }
|
||||||
|
|
||||||
|
[[setting]]
|
||||||
|
key = "tuneMaghrib"
|
||||||
|
type = "int"
|
||||||
|
label_key = "settings.tune_maghrib.label"
|
||||||
|
default = 0
|
||||||
|
min = -60
|
||||||
|
max = 60
|
||||||
|
advanced = true
|
||||||
|
visible_when = { key = "tune", values = ["true"] }
|
||||||
|
|
||||||
|
[[setting]]
|
||||||
|
key = "tuneIsha"
|
||||||
|
type = "int"
|
||||||
|
label_key = "settings.tune_isha.label"
|
||||||
|
default = 0
|
||||||
|
min = -60
|
||||||
|
max = 60
|
||||||
|
advanced = true
|
||||||
|
visible_when = { key = "tune", values = ["true"] }
|
||||||
|
|
||||||
|
# ── Background service ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[[service]]
|
||||||
|
id = "fetcher"
|
||||||
|
entry = "service.luau"
|
||||||
|
|
||||||
|
# ── Panel ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[[panel]]
|
||||||
|
id = "panel"
|
||||||
|
entry = "panel.luau"
|
||||||
|
width = 380
|
||||||
|
height = 510
|
||||||
|
placement = "floating"
|
||||||
|
position = "auto"
|
||||||
|
open_near_click = true
|
||||||
|
|
||||||
|
# ── Bar widget ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[[widget]]
|
||||||
|
id = "bar"
|
||||||
|
entry = "bar_widget.luau"
|
||||||
|
|
||||||
|
[[widget.setting]]
|
||||||
|
key = "showCountdown"
|
||||||
|
type = "bool"
|
||||||
|
label_key = "settings.show_countdown.label"
|
||||||
|
description_key = "settings.show_countdown.description"
|
||||||
|
default = true
|
||||||
|
|
||||||
|
[[widget.setting]]
|
||||||
|
key = "showElapsed"
|
||||||
|
type = "bool"
|
||||||
|
label_key = "settings.show_elapsed.label"
|
||||||
|
description_key = "settings.show_elapsed.description"
|
||||||
|
default = false
|
||||||
|
|
||||||
|
[[widget.setting]]
|
||||||
|
key = "hidePrayerName"
|
||||||
|
type = "bool"
|
||||||
|
label_key = "settings.hide_prayer_name.label"
|
||||||
|
description_key = "settings.hide_prayer_name.description"
|
||||||
|
default = false
|
||||||
|
|
||||||
|
[[widget.setting]]
|
||||||
|
key = "widgetIcon"
|
||||||
|
type = "glyph"
|
||||||
|
label_key = "settings.widget_icon.label"
|
||||||
|
default = "building-mosque"
|
||||||
|
|
||||||
|
[[widget.setting]]
|
||||||
|
key = "dynamicIcon"
|
||||||
|
type = "bool"
|
||||||
|
label_key = "settings.dynamic_icon.label"
|
||||||
|
description_key = "settings.dynamic_icon.description"
|
||||||
|
default = false
|
||||||
|
|
||||||
|
[[widget.setting]]
|
||||||
|
key = "textColor"
|
||||||
|
type = "color"
|
||||||
|
label_key = "settings.text_color.label"
|
||||||
|
default = "on_surface"
|
||||||
|
|
||||||
|
[[widget.setting]]
|
||||||
|
key = "iconColor"
|
||||||
|
type = "color"
|
||||||
|
label_key = "settings.icon_color.label"
|
||||||
|
default = "on_surface"
|
||||||
|
|
||||||
|
[[widget.setting]]
|
||||||
|
key = "activeColor"
|
||||||
|
type = "color"
|
||||||
|
label_key = "settings.active_color.label"
|
||||||
|
description_key = "settings.active_color.description"
|
||||||
|
default = "primary"
|
||||||
@@ -0,0 +1,408 @@
|
|||||||
|
--!nonstrict
|
||||||
|
-- service.luau — Mawaqit background fetcher
|
||||||
|
|
||||||
|
noctalia.setUpdateInterval(5000)
|
||||||
|
|
||||||
|
-- ── Config ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
local city, country, method, school, hijriDayOffset
|
||||||
|
local tune, tuneFajr, tuneDhuhr, tuneAsr, tuneMaghrib, tuneIsha
|
||||||
|
local showNotifications, playAzan, azanFile, twelveHourFormat
|
||||||
|
|
||||||
|
-- Safely quotes strings for shell execution to prevent injection.
|
||||||
|
local function shellQuote(s)
|
||||||
|
return "'" .. tostring(s):gsub("'", "'\\''") .. "'"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Escapes POSIX ERE metacharacters so a path can be used as a `pkill -f`
|
||||||
|
-- pattern without accidentally behaving as a regex.
|
||||||
|
local function ereEscape(s)
|
||||||
|
return (s:gsub("([%.%*%+%?%(%)%[%]%^%$|\\{}])", "\\%1"))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function reloadConfig()
|
||||||
|
local c_city = noctalia.getConfig("city")
|
||||||
|
local c_country = noctalia.getConfig("country")
|
||||||
|
local c_method = noctalia.getConfig("method")
|
||||||
|
local c_school = noctalia.getConfig("school")
|
||||||
|
local c_hijriDayOffset = noctalia.getConfig("hijriDayOffset")
|
||||||
|
local c_tune = noctalia.getConfig("tune")
|
||||||
|
local c_tuneFajr = noctalia.getConfig("tuneFajr")
|
||||||
|
local c_tuneDhuhr = noctalia.getConfig("tuneDhuhr")
|
||||||
|
local c_tuneAsr = noctalia.getConfig("tuneAsr")
|
||||||
|
local c_tuneMaghrib = noctalia.getConfig("tuneMaghrib")
|
||||||
|
local c_tuneIsha = noctalia.getConfig("tuneIsha")
|
||||||
|
local c_showNotifications = noctalia.getConfig("showNotifications")
|
||||||
|
local c_playAzan = noctalia.getConfig("playAzan")
|
||||||
|
local c_azanFile = noctalia.getConfig("azanFile")
|
||||||
|
local c_twelveHourFormat = noctalia.getConfig("twelveHourFormat")
|
||||||
|
|
||||||
|
city = (type(c_city) == "string" and c_city ~= "") and c_city or "London"
|
||||||
|
country = (type(c_country) == "string" and c_country ~= "") and c_country or "UK"
|
||||||
|
method = (type(c_method) == "string" and c_method ~= "") and c_method or "3"
|
||||||
|
school = (type(c_school) == "string" and c_school ~= "") and c_school or "0"
|
||||||
|
hijriDayOffset = tonumber(c_hijriDayOffset) or 0
|
||||||
|
tune = (c_tune == true or c_tune == "true")
|
||||||
|
tuneFajr = tonumber(c_tuneFajr) or 0
|
||||||
|
tuneDhuhr = tonumber(c_tuneDhuhr) or 0
|
||||||
|
tuneAsr = tonumber(c_tuneAsr) or 0
|
||||||
|
tuneMaghrib = tonumber(c_tuneMaghrib) or 0
|
||||||
|
tuneIsha = tonumber(c_tuneIsha) or 0
|
||||||
|
showNotifications = (c_showNotifications ~= false and c_showNotifications ~= "false")
|
||||||
|
playAzan = (c_playAzan == true or c_playAzan == "true")
|
||||||
|
azanFile = (type(c_azanFile) == "string" and c_azanFile ~= "") and c_azanFile or "azan1.mp3"
|
||||||
|
twelveHourFormat = (c_twelveHourFormat == true or c_twelveHourFormat == "true")
|
||||||
|
end
|
||||||
|
|
||||||
|
reloadConfig()
|
||||||
|
noctalia.log("Mawaqit: city=" .. city .. " country=" .. country .. " method=" .. method)
|
||||||
|
|
||||||
|
-- Check azan player availability at startup
|
||||||
|
local paplayAvailable = noctalia.commandExists("paplay")
|
||||||
|
local pwcatAvailable = noctalia.commandExists("pw-cat")
|
||||||
|
if playAzan and not paplayAvailable and not pwcatAvailable then
|
||||||
|
noctalia.log("Mawaqit: azan enabled but neither paplay nor pw-cat found")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ── State ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
-- Prayers for countdown (no Sunrise — not a salah)
|
||||||
|
local PRAYER_NAMES = { "Fajr", "Dhuhr", "Asr", "Maghrib", "Isha" }
|
||||||
|
local AZAN_PRAYERS = { true, true, true, true, true }
|
||||||
|
|
||||||
|
local loadedDate = ""
|
||||||
|
local fetchPending = false
|
||||||
|
local retryCount = 0
|
||||||
|
local MAX_RETRIES = 5
|
||||||
|
local retryTicks = 0
|
||||||
|
local tomorrowFetched = false
|
||||||
|
local lastNotified = ""
|
||||||
|
local lastAzanPlayed = ""
|
||||||
|
local playingAzanPath = "" -- absolute path of the azan file currently playing, if any
|
||||||
|
|
||||||
|
-- ── Helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
local function parseTime(str)
|
||||||
|
if not str then return nil end
|
||||||
|
local h, m = str:match("^(%d+):(%d+)")
|
||||||
|
if not h then return nil end
|
||||||
|
return tonumber(h) * 3600 + tonumber(m) * 60
|
||||||
|
end
|
||||||
|
|
||||||
|
local function todayStr()
|
||||||
|
return os.date("%Y-%m-%d")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Converts a 24-hour "HH:MM" string to 12-hour display, when the setting is on.
|
||||||
|
-- Returns the input unchanged (24-hour) otherwise, or if it doesn't parse.
|
||||||
|
local function formatDisplayTime(clean)
|
||||||
|
if not twelveHourFormat then return clean end
|
||||||
|
local h, m = clean:match("^(%d+):(%d+)")
|
||||||
|
if not h then return clean end
|
||||||
|
local hourNum = tonumber(h)
|
||||||
|
local period = hourNum < 12 and "AM" or "PM"
|
||||||
|
local displayHour = hourNum % 12
|
||||||
|
if displayHour == 0 then displayHour = 12 end
|
||||||
|
return string.format("%d:%s %s", displayHour, m, period)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function nowSeconds()
|
||||||
|
local t = os.date("*t")
|
||||||
|
return t.hour * 3600 + t.min * 60 + t.sec
|
||||||
|
end
|
||||||
|
|
||||||
|
local function isJumuah()
|
||||||
|
return os.date("*t").wday == 6
|
||||||
|
end
|
||||||
|
|
||||||
|
local function dateParam(offsetDays)
|
||||||
|
local t = os.time() + (offsetDays * 86400)
|
||||||
|
local d = os.date("*t", t)
|
||||||
|
return string.format("%02d-%02d-%04d", d.day, d.month, d.year)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function applyTune(name, secs)
|
||||||
|
if not tune then return secs end
|
||||||
|
local offset = 0
|
||||||
|
if name == "Fajr" then offset = tuneFajr
|
||||||
|
elseif name == "Dhuhr" then offset = tuneDhuhr
|
||||||
|
elseif name == "Asr" then offset = tuneAsr
|
||||||
|
elseif name == "Maghrib" then offset = tuneMaghrib
|
||||||
|
elseif name == "Isha" then offset = tuneIsha
|
||||||
|
end
|
||||||
|
return secs + offset * 60
|
||||||
|
end
|
||||||
|
|
||||||
|
local function buildUrl(offsetDays)
|
||||||
|
local dateStr = dateParam(offsetDays or 0)
|
||||||
|
return string.format(
|
||||||
|
"https://api.aladhan.com/v1/timingsByCity/%s?city=%s&country=%s&method=%s&school=%s",
|
||||||
|
dateStr,
|
||||||
|
noctalia.string.urlEncode(city),
|
||||||
|
noctalia.string.urlEncode(country),
|
||||||
|
method, school
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Resolves the bundled azan file's absolute path from this plugin's own
|
||||||
|
-- install directory (works for git-source and path-source installs alike).
|
||||||
|
local function azanPath()
|
||||||
|
local pluginDir = noctalia.pluginDir()
|
||||||
|
if type(pluginDir) ~= "string" or pluginDir == "" then
|
||||||
|
noctalia.log("Mawaqit: could not resolve plugin directory for azan playback")
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
local safe = azanFile:match("^[%w%.%-_]+$") and azanFile or "azan1.mp3"
|
||||||
|
return pluginDir .. "/assets/" .. safe
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ── Fetch ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
local function publishError(msg)
|
||||||
|
noctalia.state.set("error", msg)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function processPrayerData(data)
|
||||||
|
if not data or data.code ~= 200 then
|
||||||
|
publishError("API error: " .. tostring(data and data.status or "unknown"))
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
local timings = data.data and data.data.timings
|
||||||
|
if not timings then
|
||||||
|
publishError("Parse error: no timings in response")
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local prayers = {}
|
||||||
|
for i, name in ipairs(PRAYER_NAMES) do
|
||||||
|
local raw = timings[name]
|
||||||
|
if raw then
|
||||||
|
local clean = raw:match("^(%d+:%d+)") or raw
|
||||||
|
local secs = parseTime(clean)
|
||||||
|
if secs then
|
||||||
|
secs = applyTune(name, secs)
|
||||||
|
prayers[#prayers+1] = {
|
||||||
|
name = name,
|
||||||
|
time = formatDisplayTime(clean),
|
||||||
|
seconds = secs,
|
||||||
|
azanPrayer = AZAN_PRAYERS[i],
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Imsak (Ramadan only)
|
||||||
|
local imsakRaw = timings["Imsak"]
|
||||||
|
local imsakTime = imsakRaw and formatDisplayTime(imsakRaw:match("^(%d+:%d+)") or imsakRaw) or ""
|
||||||
|
|
||||||
|
-- Sunrise (display only, not a prayer)
|
||||||
|
local sunriseRaw = timings["Sunrise"]
|
||||||
|
local sunriseTime = sunriseRaw and formatDisplayTime(sunriseRaw:match("^(%d+:%d+)") or sunriseRaw) or ""
|
||||||
|
|
||||||
|
-- Hijri date
|
||||||
|
local hijri = data.data.date and data.data.date.hijri
|
||||||
|
local greg = (data.data.date and data.data.date.readable) or ""
|
||||||
|
local hijriDay = 0
|
||||||
|
local hijriMonth = 0
|
||||||
|
local hijriYear = 0
|
||||||
|
local hijriDateStr = ""
|
||||||
|
local hijriDateAr = ""
|
||||||
|
|
||||||
|
if hijri then
|
||||||
|
hijriDay = math.max(1, math.min(30, (tonumber(hijri.day) or 0) + hijriDayOffset))
|
||||||
|
hijriMonth = (hijri.month and hijri.month.number) or 0
|
||||||
|
hijriYear = tonumber(hijri.year) or 0
|
||||||
|
local monthEn = (hijri.month and hijri.month.en) or ""
|
||||||
|
local monthAr = (hijri.month and hijri.month.ar) or ""
|
||||||
|
hijriDateStr = string.format("%d %s %d AH", hijriDay, monthEn, hijriYear)
|
||||||
|
|
||||||
|
local function toArabicNumerals(n)
|
||||||
|
local arabic = {"٠","١","٢","٣","٤","٥","٦","٧","٨","٩"}
|
||||||
|
return tostring(n):gsub("%d", function(d)
|
||||||
|
return arabic[tonumber(d) + 1]
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
hijriDateAr = toArabicNumerals(hijriDay) .. " " .. monthAr .. " " .. toArabicNumerals(hijriYear)
|
||||||
|
end
|
||||||
|
|
||||||
|
noctalia.state.set("prayers", prayers)
|
||||||
|
noctalia.state.set("sunriseTime", sunriseTime)
|
||||||
|
noctalia.state.set("imsakTime", imsakTime)
|
||||||
|
noctalia.state.set("hijriDate", hijriDateStr)
|
||||||
|
noctalia.state.set("hijriDateAr", hijriDateAr)
|
||||||
|
noctalia.state.set("gregorianDate", greg)
|
||||||
|
noctalia.state.set("hijriDay", hijriDay)
|
||||||
|
noctalia.state.set("hijriMonth", hijriMonth)
|
||||||
|
noctalia.state.set("hijriYear", hijriYear)
|
||||||
|
noctalia.state.set("isJumuah", isJumuah())
|
||||||
|
noctalia.state.set("error", "")
|
||||||
|
|
||||||
|
loadedDate = todayStr()
|
||||||
|
lastNotified = ""
|
||||||
|
lastAzanPlayed = ""
|
||||||
|
retryCount = 0
|
||||||
|
|
||||||
|
noctalia.log("Mawaqit: loaded " .. #prayers .. " prayers for " .. city .. ", " .. country
|
||||||
|
.. " hijriMonth=" .. hijriMonth)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function fetchTimes()
|
||||||
|
if fetchPending then return end
|
||||||
|
reloadConfig()
|
||||||
|
fetchPending = true
|
||||||
|
local url = buildUrl(0)
|
||||||
|
noctalia.log("Mawaqit: fetching " .. url)
|
||||||
|
noctalia.http({ url = url, follow_redirects = true }, function(res)
|
||||||
|
fetchPending = false
|
||||||
|
if not res.ok or res.body == "" then
|
||||||
|
retryCount += 1
|
||||||
|
publishError("Network error (status=" .. tostring(res.status) .. ")")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local ok, data = pcall(function() return noctalia.json.decode(res.body) end)
|
||||||
|
if not ok or not data then
|
||||||
|
retryCount += 1
|
||||||
|
publishError("Parse error")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
processPrayerData(data)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function fetchTomorrowFajr()
|
||||||
|
local url = buildUrl(1)
|
||||||
|
noctalia.http({ url = url, follow_redirects = true }, function(res)
|
||||||
|
if not res.ok or res.body == "" then return end
|
||||||
|
local ok, data = pcall(function() return noctalia.json.decode(res.body) end)
|
||||||
|
if not ok or not data or data.code ~= 200 then return end
|
||||||
|
local timings = data.data and data.data.timings
|
||||||
|
if not timings then return end
|
||||||
|
local raw = timings["Fajr"]
|
||||||
|
if raw then
|
||||||
|
local clean = raw:match("^(%d+:%d+)") or raw
|
||||||
|
local secs = parseTime(clean)
|
||||||
|
if secs then
|
||||||
|
secs = applyTune("Fajr", secs)
|
||||||
|
noctalia.state.set("tomorrowFajr", secs)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ── Azan playback ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
-- Kills azan playback in progress. Matches on the exact absolute path this
|
||||||
|
-- plugin passed to paplay/pw-cat (tracked in playingAzanPath), not a generic
|
||||||
|
-- substring — so it can't touch an unrelated process that happens to share
|
||||||
|
-- part of that path. This is an interim measure: runAsync doesn't return a
|
||||||
|
-- PID, so command-line matching is the only way to terminate a detached
|
||||||
|
-- process with the current API.
|
||||||
|
local function stopAzan()
|
||||||
|
if playingAzanPath ~= "" then
|
||||||
|
local pattern = shellQuote(ereEscape(playingAzanPath))
|
||||||
|
noctalia.runAsync("pkill -f -- " .. pattern .. " 2>/dev/null || true")
|
||||||
|
playingAzanPath = ""
|
||||||
|
end
|
||||||
|
noctalia.state.set("azanPlaying", false)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ── Prayer events ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
local cachedPrayers = {}
|
||||||
|
|
||||||
|
local function checkPrayerEvents()
|
||||||
|
if #cachedPrayers == 0 then return end
|
||||||
|
local now = nowSeconds()
|
||||||
|
for _, p in ipairs(cachedPrayers) do
|
||||||
|
if now >= p.seconds and now < p.seconds + 30 and p.azanPrayer then
|
||||||
|
local key = loadedDate .. "_" .. p.name
|
||||||
|
local title = (p.name == "Dhuhr" and isJumuah()) and "Jumu'ah" or p.name
|
||||||
|
|
||||||
|
if showNotifications and lastNotified ~= key then
|
||||||
|
lastNotified = key
|
||||||
|
noctalia.notify(title .. " prayer time", "It is time for " .. title)
|
||||||
|
end
|
||||||
|
|
||||||
|
if playAzan and lastAzanPlayed ~= key then
|
||||||
|
lastAzanPlayed = key
|
||||||
|
local path = azanPath()
|
||||||
|
if path ~= "" and noctalia.fileExists(path) then
|
||||||
|
noctalia.state.set("azanPlaying", true)
|
||||||
|
playingAzanPath = path
|
||||||
|
local cmd
|
||||||
|
if paplayAvailable then
|
||||||
|
cmd = "paplay " .. shellQuote(path) .. " 2>/dev/null"
|
||||||
|
elseif pwcatAvailable then
|
||||||
|
cmd = "pw-cat -p " .. shellQuote(path) .. " 2>/dev/null"
|
||||||
|
else
|
||||||
|
noctalia.log("Mawaqit: no azan player available (install paplay or pw-cat)")
|
||||||
|
noctalia.state.set("azanPlaying", false)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
noctalia.runAsync(cmd, function(_res)
|
||||||
|
playingAzanPath = ""
|
||||||
|
noctalia.state.set("azanPlaying", false)
|
||||||
|
end)
|
||||||
|
elseif path ~= "" then
|
||||||
|
noctalia.log("Mawaqit: azan file not found: " .. path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
noctalia.state.watch("prayers", function(val)
|
||||||
|
if type(val) == "table" then cachedPrayers = val end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Listen to panel commands via state (cleaner than shell IPC)
|
||||||
|
noctalia.state.watch("command", function(cmd)
|
||||||
|
if type(cmd) ~= "table" then return end
|
||||||
|
if cmd.action == "refresh" then onConfigChanged()
|
||||||
|
elseif cmd.action == "stopAzan" then stopAzan()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- ── Main tick ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function update()
|
||||||
|
local today = todayStr()
|
||||||
|
if loadedDate ~= today then
|
||||||
|
tomorrowFetched = false
|
||||||
|
fetchTimes()
|
||||||
|
end
|
||||||
|
if loadedDate == today and not tomorrowFetched then
|
||||||
|
tomorrowFetched = true
|
||||||
|
fetchTomorrowFajr()
|
||||||
|
end
|
||||||
|
if retryCount > 0 and retryCount <= MAX_RETRIES then
|
||||||
|
retryTicks += 1
|
||||||
|
if retryTicks >= 6 then
|
||||||
|
retryTicks = 0
|
||||||
|
fetchTimes()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
checkPrayerEvents()
|
||||||
|
end
|
||||||
|
|
||||||
|
function onConfigChanged()
|
||||||
|
noctalia.log("Mawaqit: settings changed, reloading config and refreshing")
|
||||||
|
reloadConfig()
|
||||||
|
loadedDate = ""
|
||||||
|
retryCount = 0
|
||||||
|
cachedPrayers = {}
|
||||||
|
noctalia.state.set("prayers", {})
|
||||||
|
fetchTimes()
|
||||||
|
end
|
||||||
|
|
||||||
|
function onIpc(event, _payload)
|
||||||
|
if event == "refresh" then
|
||||||
|
onConfigChanged()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function onExit()
|
||||||
|
-- Clean up: stop any playing azan when plugin is disabled or noctalia exits
|
||||||
|
stopAzan()
|
||||||
|
end
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
@@ -0,0 +1,135 @@
|
|||||||
|
{
|
||||||
|
"plugin_name": "Mawaqit",
|
||||||
|
"plugin_description": "Prayer times with live countdown, notifications, and azan.",
|
||||||
|
"panel": {
|
||||||
|
"title": "Prayer Times",
|
||||||
|
"sunrise": "Sunrise",
|
||||||
|
"now": "Now",
|
||||||
|
"in": "in",
|
||||||
|
"jumuah": "Jumu'ah",
|
||||||
|
"loading": "Loading prayer times…",
|
||||||
|
"tooltip": {
|
||||||
|
"next": "Next",
|
||||||
|
"time": "Time",
|
||||||
|
"in": "In",
|
||||||
|
"tomorrow_fajr": "Fajr (tomorrow)",
|
||||||
|
"no_data": "Fetching prayer times…"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"city": {
|
||||||
|
"label": "City",
|
||||||
|
"description": "Your city name in English (e.g. London)."
|
||||||
|
},
|
||||||
|
"country": {
|
||||||
|
"label": "Country",
|
||||||
|
"description": "Country name or 2-letter code (e.g. UK, DZ, FR)."
|
||||||
|
},
|
||||||
|
"method": {
|
||||||
|
"label": "Calculation Method",
|
||||||
|
"description": "Choose the calculation authority followed in your region.",
|
||||||
|
"karachi": "University of Islamic Sciences, Karachi",
|
||||||
|
"isna": "Islamic Society of North America (ISNA)",
|
||||||
|
"mwl": "Muslim World League (MWL)",
|
||||||
|
"makkah": "Umm Al-Qura University, Makkah",
|
||||||
|
"egypt": "Egyptian General Authority of Survey",
|
||||||
|
"tehran": "Institute of Geophysics, Tehran",
|
||||||
|
"gulf": "Gulf Region",
|
||||||
|
"kuwait": "Kuwait",
|
||||||
|
"qatar": "Qatar",
|
||||||
|
"singapore": "Majlis Ugama Islam Singapura (MUIS)",
|
||||||
|
"france": "Union Organization Islamic de France",
|
||||||
|
"turkey": "Diyanet İşleri Başkanlığı, Turkey",
|
||||||
|
"russia": "Spiritual Administration of Muslims of Russia",
|
||||||
|
"moonsighting": "Moonsighting Committee Worldwide",
|
||||||
|
"dubai": "Dubai (experimental)",
|
||||||
|
"malaysia": "JAKIM, Malaysia",
|
||||||
|
"tunisia": "Tunisia",
|
||||||
|
"algeria": "Algeria / MESRS",
|
||||||
|
"indonesia": "KEMENAG, Indonesia",
|
||||||
|
"morocco": "Morocco",
|
||||||
|
"portugal": "Comunidade Islamica de Lisboa",
|
||||||
|
"jordan": "Ministry of Awqaf, Jordan"
|
||||||
|
},
|
||||||
|
"school": {
|
||||||
|
"label": "Asr Calculation School",
|
||||||
|
"description": "Shafi/Maliki/Hanbali uses shadow factor 1. Hanafi uses factor 2, giving a later Asr.",
|
||||||
|
"shafi": "Shafi / Maliki / Hanbali (default)",
|
||||||
|
"hanafi": "Hanafi"
|
||||||
|
},
|
||||||
|
"hijri_day_offset": {
|
||||||
|
"label": "Hijri Day Adjustment",
|
||||||
|
"description": "Shift the displayed Hijri day if the API date doesn't match your local moon sighting.",
|
||||||
|
"minus1": "−1 day",
|
||||||
|
"default": "Default (from API)",
|
||||||
|
"plus1": "+1 day"
|
||||||
|
},
|
||||||
|
"twelve_hour_format": {
|
||||||
|
"label": "12-Hour Format",
|
||||||
|
"description": "Show prayer times in 12-hour format (e.g. 5:23 AM) instead of 24-hour."
|
||||||
|
},
|
||||||
|
"show_notifications": {
|
||||||
|
"label": "Prayer notifications",
|
||||||
|
"description": "Show a system notification when each prayer time begins."
|
||||||
|
},
|
||||||
|
"play_azan": {
|
||||||
|
"label": "Play Azan",
|
||||||
|
"description": "Play an azan audio file when each prayer time begins."
|
||||||
|
},
|
||||||
|
"azan_file": {
|
||||||
|
"label": "Azan audio",
|
||||||
|
"description": "Which azan to play. Name your files azan1.mp3 / azan2.mp3 / azan3.mp3 and place them in the plugin's assets/ folder (see README).",
|
||||||
|
"azan1": "Azan 1",
|
||||||
|
"azan2": "Azan 2",
|
||||||
|
"azan3": "Azan 3"
|
||||||
|
},
|
||||||
|
"tune": {
|
||||||
|
"label": "Enable per-prayer time offsets",
|
||||||
|
"description": "Apply per-prayer minute adjustments to fine-tune times."
|
||||||
|
},
|
||||||
|
"tune_fajr": {
|
||||||
|
"label": "Fajr offset (minutes)"
|
||||||
|
},
|
||||||
|
"tune_dhuhr": {
|
||||||
|
"label": "Dhuhr offset (minutes)"
|
||||||
|
},
|
||||||
|
"tune_asr": {
|
||||||
|
"label": "Asr offset (minutes)"
|
||||||
|
},
|
||||||
|
"tune_maghrib": {
|
||||||
|
"label": "Maghrib offset (minutes)"
|
||||||
|
},
|
||||||
|
"tune_isha": {
|
||||||
|
"label": "Isha offset (minutes)"
|
||||||
|
},
|
||||||
|
"show_countdown": {
|
||||||
|
"label": "Show countdown",
|
||||||
|
"description": "Show a live countdown to the next prayer instead of the static time."
|
||||||
|
},
|
||||||
|
"show_elapsed": {
|
||||||
|
"label": "Show elapsed time after prayer",
|
||||||
|
"description": "After a prayer begins, count up (+) for up to 1 hour."
|
||||||
|
},
|
||||||
|
"hide_prayer_name": {
|
||||||
|
"label": "Hide prayer name in bar",
|
||||||
|
"description": "Show only the time or countdown without the prayer name."
|
||||||
|
},
|
||||||
|
"widget_icon": {
|
||||||
|
"label": "Bar icon"
|
||||||
|
},
|
||||||
|
"dynamic_icon": {
|
||||||
|
"label": "Dynamic prayer icon",
|
||||||
|
"description": "Show a sun/moon icon matching the current prayer instead of the fixed icon."
|
||||||
|
},
|
||||||
|
"text_color": {
|
||||||
|
"label": "Bar text color"
|
||||||
|
},
|
||||||
|
"icon_color": {
|
||||||
|
"label": "Bar icon color"
|
||||||
|
},
|
||||||
|
"active_color": {
|
||||||
|
"label": "Active color",
|
||||||
|
"description": "Color used when a prayer is happening now or in elapsed mode."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user