Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"permissions": {
"allow": [
"Bash(git ls-tree *)",
"Bash(grep -v \"^[+-][[:space:]]*$\")",
"Bash(git checkout *)",
"Bash(python *)",
"Bash(git add *)",
"Bash(git commit -m ' *)",
"Bash(git branch *)",
"Bash(git merge *)",
"Bash(git commit *)"
]
}
}
425 changes: 425 additions & 0 deletions docs/widgets/(Widget)-Prayer-Times.md

Large diffs are not rendered by default.

91 changes: 91 additions & 0 deletions src/core/utils/widgets/prayer_times/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import json
import logging
import traceback
from typing import Callable

from PyQt6.QtCore import QObject, QTimer, QUrl, pyqtSignal
from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest

HEADER = (b"User-Agent", b"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:129.0) Gecko/20100101 Firefox/129.0")
RETRY_INTERVAL_MS = 5_000 # retry every 5 s when the network is unavailable


class PrayerTimesDataFetcher(QObject):
"""Fetches Islamic prayer times from the Aladhan API."""

finished = pyqtSignal(dict)

def __init__(self, parent: QObject, url_factory: Callable[[], str], timeout_ms: int):
"""
Args:
parent: Qt parent object.
url_factory: A callable that returns the current API URL string.
Called on every request so the date is always today.
timeout_ms: Interval between automatic re-fetches, in milliseconds.
"""
super().__init__(parent)
self.started = False
self._url_factory = url_factory
self._manager = QNetworkAccessManager(self)
self._manager.finished.connect(self._handle_response)
self._timer = QTimer(self)
self._timer.setInterval(timeout_ms)
self._timer.timeout.connect(self.make_request)
# Single-shot timer used to retry quickly after a network failure.
self._retry_timer = QTimer(self)
self._retry_timer.setSingleShot(True)
self._retry_timer.timeout.connect(self.make_request)

def start(self) -> None:
"""Begin periodic fetching. The first request fires immediately."""
self.make_request()
self._timer.start()
self.started = True

def make_request(self) -> None:
"""Make a single API request using the current URL from url_factory."""
url = QUrl(self._url_factory())
if not url.isValid():
logging.error("Prayer times: built an invalid URL — check latitude/longitude settings.")
return
request = QNetworkRequest(url)
request.setRawHeader(*HEADER)
self._manager.get(request)

def _handle_response(self, reply: QNetworkReply) -> None:
try:
error = reply.error()
status = reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute)
if error == QNetworkReply.NetworkError.NoError:
raw = reply.readAll().data().decode("utf-8", errors="replace")
data = json.loads(raw)
if data.get("code") == 200:
self._retry_timer.stop()
self.finished.emit(data)
else:
logging.error(f"Prayer times API returned non-200 code: {data.get('code')} — {data.get('status')}")
self.finished.emit({})
self._schedule_retry()
elif error == QNetworkReply.NetworkError.HostNotFoundError:
logging.warning("Prayer times: no internet connection or host not found.")
self.finished.emit({})
self._schedule_retry()
else:
logging.error(f"Prayer times API network error {status}: {error}")
self.finished.emit({})
self._schedule_retry()
except json.JSONDecodeError as e:
logging.error(f"Prayer times: invalid JSON in response: {e}")
self.finished.emit({})
self._schedule_retry()
except Exception as e:
logging.error(f"Prayer times: unexpected error: {e}\n{traceback.format_exc()}")
self.finished.emit({})
self._schedule_retry()
finally:
reply.deleteLater()

def _schedule_retry(self) -> None:
"""Schedule a quick retry if one is not already pending."""
if not self._retry_timer.isActive():
self._retry_timer.start(RETRY_INTERVAL_MS)
70 changes: 70 additions & 0 deletions src/core/validation/widgets/yasb/prayer_times.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
from pydantic import Field

from core.validation.widgets.base_model import (
CallbacksConfig,
CustomBaseModel,
KeybindingConfig,
)


class PrayerTimesCallbacksConfig(CallbacksConfig):
on_left: str = "toggle_card"
on_middle: str = "do_nothing"
on_right: str = "toggle_label"


class PrayerTimesIconsConfig(CustomBaseModel):
mosque: str = "\uf67f"
fajr: str = "\uf185"
sunrise: str = "\uf185"
dhuhr: str = "\uf185"
asr: str = "\uf185"
maghrib: str = "\uf186"
isha: str = "\uf186"
imsak: str = "\uf185"
sunset: str = "\uf185"
midnight: str = "\uf186"
default: str = "\uf017"


class PrayerTimesMenuConfig(CustomBaseModel):
blur: bool = True
round_corners: bool = True
round_corners_type: str = "normal"
border_color: str = "System"
alignment: str = "right"
direction: str = "down"
offset_top: int = 6
offset_left: int = 0


class PrayerTimesFlashConfig(CustomBaseModel):
enabled: bool = True
debug: bool = False
duration: int = Field(default=30, ge=1, le=3600)
interval: int = Field(default=500, ge=100, le=5000)
color_a: str = "#ff8c00"
color_b: str = "#1e1e2e"


class PrayerTimesConfig(CustomBaseModel):
label: str = "{icon} {next_prayer} {next_prayer_time}"
label_alt: str = "Fajr {fajr} · Dhuhr {dhuhr} · Asr {asr} · Maghrib {maghrib} · Isha {isha}"
class_name: str = ""
latitude: float = Field(default=51.5074, ge=-90.0, le=90.0)
longitude: float = Field(default=-0.1278, ge=-180.0, le=180.0)
method: int = Field(default=2, ge=0, le=99)
school: int = Field(default=0, ge=0, le=1)
midnight_mode: int = Field(default=0, ge=0, le=1)
tune: str = ""
timezone: str = ""
shafaq: str = ""
prayers_to_show: list[str] = ["Fajr", "Dhuhr", "Asr", "Maghrib", "Isha"]
grace_period: int = Field(default=15, ge=0, le=120)
update_interval: int = Field(default=3600, ge=60, le=86400)
tooltip: bool = True
icons: PrayerTimesIconsConfig = PrayerTimesIconsConfig()
menu: PrayerTimesMenuConfig = PrayerTimesMenuConfig()
flash: PrayerTimesFlashConfig = PrayerTimesFlashConfig()
callbacks: PrayerTimesCallbacksConfig = PrayerTimesCallbacksConfig()
keybindings: list[KeybindingConfig] = []
Binary file modified src/core/widgets/services/systray/hook/YASBTrayHook.dll
Binary file not shown.
Binary file modified src/core/widgets/services/systray/hook/YASBTrayHook_arm64.dll
Binary file not shown.
Loading