feat: add mawaqit plugin (#53)

* feat: add mawaqit plugin

* Address review feedback

* Address validator errors
This commit is contained in:
ycf-anon
2026-07-20 20:45:30 -04:00
committed by GitHub
parent d253a9430c
commit 65503f2ce1
9 changed files with 1610 additions and 0 deletions
+93
View File
@@ -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.
+119
View File
@@ -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.
+289
View File
@@ -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()
+319
View File
@@ -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
+247
View File
@@ -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"
+408
View File
@@ -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

+135
View File
@@ -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."
}
}
}