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