New:Socket for Asana Is Now Available.Learn more
Get Started

Zakkir — مواقيت الصلاة وأذكار

Package Overview
Versions
1
Alerts
File Explorer
Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

zakkir@lovable.app - firefox Package Compare versions

Comparing version
1.51
to
1.52
+80
notification-scheduler.js
(function (root, factory) {
if (typeof module === "object" && module.exports) module.exports = factory();
else root.ZakkirNotifications = factory();
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
const PRAYER_ORDER = ["Fajr", "Dhuhr", "Asr", "Maghrib", "Isha"];
function clampMinutes(value, fallback) {
const n = Math.floor(Number(value));
if (!Number.isFinite(n)) return fallback;
return Math.min(60, Math.max(1, n));
}
function normalizePrayers(value) {
if (Array.isArray(value)) return PRAYER_ORDER.filter((name) => value.includes(name));
if (value && typeof value === "object") return PRAYER_ORDER.filter((name) => Boolean(value[name]));
return [...PRAYER_ORDER];
}
function normalizeSettings(input = {}) {
const source = input || {};
const reminderSound = source.reminderSound === "adhan-3" ? "adhan-2" : source.reminderSound || "adhan-1";
return {
notificationsEnabled: source.notificationsEnabled !== false,
remindersEnabled: source.remindersEnabled ?? source.reminderEnabled ?? false,
prayerAlertEnabled: source.prayerAlertEnabled ?? source.athanEnabled ?? true,
iqamaEnabled: source.iqamaEnabled === true,
reminderMinutes: clampMinutes(source.reminderMinutes, 10),
reminderMinutesByPrayer: source.reminderMinutesByPrayer || {},
iqamaMinutes: clampMinutes(source.iqamaMinutes, 10),
iqamaMinutesByPrayer: source.iqamaMinutesByPrayer || {},
reminderPrayers: normalizePrayers(source.reminderPrayers),
reminderSound,
};
}
function migrateSettings(input = {}) {
const source = input || {};
const migrated = { ...source, ...normalizeSettings(source) };
delete migrated.reminderEnabled;
delete migrated.athanEnabled;
return migrated;
}
function parseHHMM(value) {
const match = String(value || "").match(/^(\d{1,2}):(\d{2})/);
if (!match) return null;
const hour = Number(match[1]);
const minute = Number(match[2]);
if (hour > 23 || minute > 59) return null;
return hour * 60 + minute;
}
function localDateKey(date) {
const d = date instanceof Date ? date : new Date(date);
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
}
function targetDate(date, minutes) {
const result = new Date(date.getFullYear(), date.getMonth(), date.getDate());
result.setMinutes(minutes);
return result;
}
function buildEvents(times, settingsInput, date = new Date()) {
const settings = normalizeSettings(settingsInput);
if (!settings.notificationsEnabled) return [];
const events = [];
for (const prayer of settings.reminderPrayers) {
const athanMinutes = parseHHMM(times?.[prayer]);
if (athanMinutes == null) continue;
const before = clampMinutes(settings.reminderMinutesByPrayer?.[prayer], settings.reminderMinutes);
const after = clampMinutes(settings.iqamaMinutesByPrayer?.[prayer], settings.iqamaMinutes);
if (settings.remindersEnabled) events.push({ type: "pre", prayer, minutes: before, at: targetDate(date, athanMinutes - before), key: `${localDateKey(date)}|${prayer}|pre|${before}` });
if (settings.prayerAlertEnabled) events.push({ type: "athan", prayer, minutes: 0, at: targetDate(date, athanMinutes), key: `${localDateKey(date)}|${prayer}|athan` });
if (settings.iqamaEnabled) events.push({ type: "iqama", prayer, minutes: after, at: targetDate(date, athanMinutes + after), key: `${localDateKey(date)}|${prayer}|iqama|${after}` });
}
return events;
}
function dueEvents(times, settings, now = new Date(), previousTimes = times) {
const windows = { pre: 15 * 60 * 1000, athan: 15 * 60 * 1000, iqama: 30 * 60 * 1000 };
const yesterday = new Date(now);
yesterday.setDate(now.getDate() - 1);
return [...buildEvents(previousTimes, settings, yesterday), ...buildEvents(times, settings, now)]
.filter((event) => event.at <= now && now - event.at <= windows[event.type])
.sort((a, b) => a.at - b.at);
}
return { PRAYER_ORDER, normalizeSettings, migrateSettings, parseHHMM, localDateKey, buildEvents, dueEvents };
});
const test = require("node:test");
const assert = require("node:assert/strict");
const { buildEvents, dueEvents, migrateSettings } = require("./notification-scheduler");
const times = { Fajr: "05:00", Dhuhr: "12:00", Asr: "15:30", Maghrib: "18:00", Isha: "23:55" };
test("migrates Firefox legacy toggles and prayer object selections", () => {
const migrated = migrateSettings({ reminderEnabled: true, athanEnabled: false, reminderPrayers: { Fajr: true, Dhuhr: false } });
assert.equal(migrated.remindersEnabled, true);
assert.equal(migrated.prayerAlertEnabled, false);
assert.deepEqual(migrated.reminderPrayers, ["Fajr"]);
});
test("honors independent toggles, overrides, and the master toggle", () => {
const settings = { reminderPrayers: ["Fajr"], remindersEnabled: true, prayerAlertEnabled: false, iqamaEnabled: true, reminderMinutesByPrayer: { Fajr: 20 }, iqamaMinutesByPrayer: { Fajr: 15 } };
const events = buildEvents(times, settings, new Date(2026, 7, 4));
assert.deepEqual(events.map((event) => event.type), ["pre", "iqama"]);
assert.deepEqual(events.map((event) => event.minutes), [20, 15]);
assert.deepEqual(buildEvents(times, { ...settings, notificationsEnabled: false }, new Date(2026, 7, 4)), []);
});
test("recovers delayed ticks and prior-day iqama after midnight", () => {
const delayed = dueEvents(times, { reminderPrayers: ["Dhuhr"], prayerAlertEnabled: true }, new Date(2026, 7, 4, 12, 10));
assert.equal(delayed[0].key, "2026-08-04|Dhuhr|athan");
const midnight = dueEvents(times, { reminderPrayers: ["Isha"], prayerAlertEnabled: false, iqamaEnabled: true, iqamaMinutes: 10 }, new Date(2026, 7, 5, 0, 7), times);
assert.equal(midnight[0].key, "2026-08-04|Isha|iqama|10");
});

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

+213
-59

@@ -6,2 +6,3 @@ // Zakkir background — schedules pre-athan reminders and updates the toolbar

const PRAYER_ORDER = ["Fajr", "Dhuhr", "Asr", "Maghrib", "Isha"];
const notificationScheduler = globalThis.ZakkirNotifications;

@@ -16,16 +17,20 @@ const DEFAULTS = {

prayerCache: null,
previousPrayerCache: null,
notificationsEnabled: true,
reminderEnabled: false,
remindersEnabled: false,
reminderMinutes: 10,
athanEnabled: true,
reminderPrayers: { Fajr: true, Dhuhr: true, Asr: true, Maghrib: true, Isha: true },
reminderMinutesByPrayer: {},
prayerAlertEnabled: true,
reminderPrayers: ["Fajr", "Dhuhr", "Asr", "Maghrib", "Isha"],
iqamaEnabled: false,
iqamaMinutes: 10,
iqamaMinutesByPrayer: {},
badgeEnabled: false,
_sentReminders: {},
_lastTick: null,
};
function todayKey() { return new Date().toISOString().slice(0, 10); }
function ddmmyyyy() {
const d = new Date(), p = (n) => String(n).padStart(2, "0");
function todayKey(date = new Date()) { return notificationScheduler.localDateKey(date); }
function ddmmyyyy(date = new Date()) {
const d = date, p = (n) => String(n).padStart(2, "0");
return `${p(d.getDate())}-${p(d.getMonth() + 1)}-${d.getFullYear()}`;

@@ -36,18 +41,24 @@ }

function getState() {
return new Promise((res) => chrome.storage.local.get(DEFAULTS, res));
return new Promise((res) => chrome.storage.local.get(null, (raw) => {
const state = { ...DEFAULTS, ...notificationScheduler.migrateSettings(raw) };
chrome.storage.local.set(notificationScheduler.normalizeSettings(state));
chrome.storage.local.remove(["reminderEnabled", "athanEnabled"]);
res(state);
}));
}
async function ensurePrayers(state) {
const today = todayKey();
const c = state.prayerCache;
async function ensurePrayers(state, date = new Date()) {
const dateKey = todayKey(date);
const currentDate = todayKey();
const c = state.prayerCache?.date === dateKey ? state.prayerCache : state.previousPrayerCache?.date === dateKey ? state.previousPrayerCache : null;
const sameLoc = c && (state.useCoords && state.lat != null && state.lng != null
? (c.lat === state.lat && c.lng === state.lng)
: (c.city === state.city && c.country === state.country));
if (c && c.date === today && sameLoc && c.method === state.method) return c.timings;
if (c && sameLoc && c.method === state.method) return c.timings;
let url;
if (state.useCoords && state.lat != null && state.lng != null) {
url = `https://api.aladhan.com/v1/timings/${ddmmyyyy()}?latitude=${state.lat}&longitude=${state.lng}&method=${state.method || 5}`;
url = `https://api.aladhan.com/v1/timings/${ddmmyyyy(date)}?latitude=${state.lat}&longitude=${state.lng}&method=${state.method || 5}`;
} else {
url = `https://api.aladhan.com/v1/timingsByCity/${ddmmyyyy()}?city=${encodeURIComponent(state.city || "Cairo")}&country=${encodeURIComponent(state.country || "Egypt")}&method=${state.method || 5}`;
url = `https://api.aladhan.com/v1/timingsByCity/${ddmmyyyy(date)}?city=${encodeURIComponent(state.city || "Cairo")}&country=${encodeURIComponent(state.country || "Egypt")}&method=${state.method || 5}`;
}

@@ -63,7 +74,12 @@ try {

const cache = {
date: today, city: state.city, country: state.country,
date: dateKey, city: state.city, country: state.country,
lat: state.lat, lng: state.lng, method: state.method,
timings: prayers, hijri,
};
await chrome.storage.local.set({ prayerCache: cache });
const cacheKey = dateKey === currentDate ? "prayerCache" : "previousPrayerCache";
const patch = { [cacheKey]: cache };
if (cacheKey === "prayerCache" && state.prayerCache?.date && state.prayerCache.date !== dateKey) {
patch.previousPrayerCache = state.prayerCache;
}
await chrome.storage.local.set(patch);
return prayers;

@@ -75,17 +91,78 @@ } catch (e) {

function notify(id, title, message) {
function isPrayerEnabled(reminderPrayers, prayerName) {
if (Array.isArray(reminderPrayers)) return reminderPrayers.includes(prayerName);
if (reminderPrayers && typeof reminderPrayers === "object") return Boolean(reminderPrayers[prayerName]);
return true;
}
function playNotificationSound(soundName) {
if (!soundName || soundName === "silent") return;
try {
chrome.notifications.create(id, {
type: "basic",
iconUrl: chrome.runtime.getURL("icon_48.png"),
title,
message,
priority: 2,
});
} catch (e) { /* notifications may be unavailable */ }
const file = soundName.startsWith("adhan") ? `${soundName}.mp3` : `${soundName}.mp3`;
const audio = new Audio(chrome.runtime.getURL(`sounds/${file}`));
audio.play().catch(() => {});
} catch (_) {}
}
function notify(id, title, message, soundName) {
return new Promise((resolve) => {
try {
if (soundName) playNotificationSound(soundName);
chrome.notifications.create(id, {
type: "basic",
iconUrl: chrome.runtime.getURL("icon_48.png"),
title,
message,
priority: 2,
}, (createdId) => {
const error = chrome.runtime.lastError;
if (error) resolve({ ok: false, error: error.message || String(error) });
else resolve({ ok: true, id: createdId || id });
});
} catch (e) {
resolve({ ok: false, error: e?.message || String(e) });
}
});
}
function getNextReminder(state, prayers) {
if (!prayers) return null;
const config = notificationScheduler.normalizeSettings(state);
const nowM = new Date().getHours() * 60 + new Date().getMinutes();
const enabled = state.reminderPrayers || {};
const preMins = Math.max(0, state.reminderMinutes ?? 10);
const iqMins = Math.max(0, state.iqamaMinutes ?? 10);
const upcoming = [];
for (const name of PRAYER_ORDER) {
const p = toMin(prayers[name]);
if (Number.isNaN(p)) continue;
if (config.reminderPrayers.includes(name)) {
if (config.remindersEnabled) {
const pre = Math.max(1, state.reminderMinutesByPrayer?.[name] ?? preMins);
upcoming.push({ label: `${name} before`, at: p - pre });
}
if (config.prayerAlertEnabled) upcoming.push({ label: `${name} at`, at: p });
if (config.iqamaEnabled) {
const iq = Math.max(1, state.iqamaMinutesByPrayer?.[name] ?? iqMins);
upcoming.push({ label: `${name} iqama`, at: p + iq });
}
}
}
const next = upcoming
.map((e) => ({ ...e, at: ((e.at % 1440) + 1440) % 1440, days: 0 }))
.filter((e) => e.at >= nowM)
.sort((a, b) => a.at - b.at)[0];
if (!next) return null;
const h = Math.floor(next.at / 60);
const m = next.at % 60;
return { label: next.label, time: `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}` };
}
async function tick() {
const state = await getState();
const prayers = await ensurePrayers(state);
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const previousPrayers = await ensurePrayers(state, yesterday);
try { await chrome.storage.local.set({ _lastTick: new Date().toISOString() }); } catch {}

@@ -118,43 +195,89 @@ if (!prayers) {

// Reminders
const anyReminder = state.notificationsEnabled && (state.reminderEnabled || state.iqamaEnabled || state.athanEnabled);
if (anyReminder) {
const enabled = state.reminderPrayers || {};
const anyEnabled = PRAYER_ORDER.some((n) => isPrayerEnabled(enabled, n));
if (state.notificationsEnabled && anyEnabled) {
const preMins = Math.max(0, state.reminderMinutes ?? 10);
const iqMins = Math.max(0, state.iqamaMinutes ?? 10);
const enabled = state.reminderPrayers || {};
const sound = state.reminderSound || "adhan-1";
const today = todayKey();
const sent = { ...(state._sentReminders || {}) };
for (const p of list) {
if (!enabled[p.name]) continue;
const PRAYER_MESSAGES = {
Fajr: [
{ title: "صلاة الفجر 🌅", body: "«أَقِمِ الصَّلَاةَ لِدُلُوكِ الشَّمْسِ إِلَى غَسَقِ اللَّيْلِ وَقُرْآنَ الْفَجْرِ ۖ إِنَّ قُرْآنَ الْفَجْرِ كَانَ مَشْهُودًا»" },
{ title: "صلاة الفجر 🌅", body: "«مَنْ صَلَّى الصُّبْحَ فَهُوَ فِي ذِمَّةِ اللَّهِ» — صحيح مسلم" },
{ title: "صلاة الفجر 🌅", body: "«رَكْعَتَا الْفَجْرِ خَيْرٌ مِنَ الدُّنْيَا وَمَا فِيهَا» — صحيح مسلم" },
{ title: "صلاة الفجر 🌅", body: "«بَشِّرِ الْمَشَّائِينَ فِي الظُّلَمِ إِلَى الْمَسَاجِدِ بِالنُّورِ التَّامِّ يَوْمَ الْقِيَامَةِ» — سنن أبي داود والترمذي" },
{ title: "صلاة الفجر 🌅", body: "«مَنْ صَلَّى الْبَرْدَيْنِ دَخَلَ الْجَنَّةَ» — متفق عليه" },
{ title: "صلاة الفجر 🌅", body: "«أَثْقَلُ الصَّلَاةِ عَلَى الْمُنَافِقِينَ صَلَاةُ الْعِشَاءِ وَصَلَاةُ الْفَجْرِ» — متفق عليه" },
{ title: "صلاة الفجر 🌅", body: "«لَنْ يَلِجَ النَّارَ أَحَدٌ صَلَّى قَبْلَ طُلُوعِ الشَّمْسِ وَقَبْلَ غُرُوبِهَا» — صحيح مسلم" },
{ title: "صلاة الفجر 🌅", body: "قال عمر رضي الله عنه: «لَأَنْ أَشْهَدَ صَلَاةَ الصُّبْحِ فِي الْجَمَاعَةِ أَحَبُّ إِلَيَّ مِنْ قِيَامِ لَيْلَةٍ»" },
{ title: "صلاة الفجر 🌅", body: "«مَنْ بَاتَ طَاهِرًا بَاتَ فِي شِعَارِهِ مَلَكٌ... فَيَقُولُ الْمَلَكُ: اللَّهُمَّ اغْفِرْ لِعَبْدِكَ فُلَانٍ» — صحيح ابن حبان" }
],
Dhuhr: [
{ title: "صلاة الظهر ☀️", body: "«وَمِنْ آنَاءِ اللَّيْلِ فَسَبِّحْ وَأَطْرَافَ النَّهَارِ لَعَلَّكَ تَرْضَى»" },
{ title: "صلاة الظهر ☀️", body: "«إِنَّ أَوَّلَ مَا يُحَاسَبُ بِهِ الْعَبْدُ يَوْمَ الْقِيَامَةِ مِنْ عَمَلِهِ صَلَاتُهُ» — سنن الترمذي" },
{ title: "صلاة الظهر ☀️", body: "«إِذَا زَالَتِ الشَّمْسُ فُتِحَتْ أَبْوَابُ السَّمَاءِ... فَأُحِبُّ أَنْ يَصْعَدَ لِي فِيهِنَّ عَمَلٌ صَالِحٌ» — سنن الترمذي" },
{ title: "صلاة الظهر ☀️", body: "«أَرْبَعٌ قَبْلَ الظُّهْرِ لَيْسَ فِيهِنَّ تَسْلِيمٌ تُفْتَحُ لَهُنَّ أَبْوَابُ السَّمَاءِ» — سنن أبي داود" },
{ title: "صلاة الظهر ☀️", body: "«مَنْ حَافَظَ عَلَى أَرْبَعِ رَكَعَاتٍ قَبْلَ الظُّهْرِ وَأَرْبَعٍ بَعْدَهَا حَرَّمَهُ اللَّهُ عَلَى النَّارِ» — سنن الترمذي" }
],
Asr: [
{ title: "صلاة العصر (الصلاة الوسطى) 🌤️", body: "«حَافِظُوا عَلَى الصَّلَوَاتِ وَالصَّلَاةِ الْوُسْطَى وَقُومُوا لِلَّهِ قَانِتِينَ»" },
{ title: "صلاة العصر 🌤️", body: "«مَنْ تَرَكَ صَلَاةَ الْعَصْرِ فَقَدْ حَبِطَ عَمَلُهُ» — صحيح البخاري" },
{ title: "صلاة العصر 🌤️", body: "«الَّذِي تَفُوتُهُ صَلَاةُ الْعَصْرِ كَأَنَّمَا وُتِرَ أَهْلَهُ وَمَالَهُ» — متفق عليه" },
{ title: "صلاة العصر 🌤️", body: "«مَنْ صَلَّى الْبَرْدَيْنِ دَخَلَ الْجَنَّةَ» — متفق عليه" },
{ title: "صلاة العصر 🌤️", body: "قال بريدة رضي الله عنه: «بَكِّرُوا بِصَلَاةِ الْعَصْرِ، فَإِنَّ النَّبِيَّ ﷺ قَالَ: مَنْ تَرَكَ صَلَاةَ الْعَصْرِ فَقَدْ حَبِطَ عَمَلُهُ»" }
],
Maghrib: [
{ title: "صلاة المغرب 🌅", body: "«وَسَبِّحْ بِحَمْدِ رَبِّكَ قَبْلَ طُلُوعِ الشَّمْسِ وَقَبْلَ الْغُرُوبِ»" },
{ title: "صلاة المغرب 🌅", body: "«فَسُبْحَانَ اللَّهِ حِينَ تُمْسُونَ وَحِينَ تُصْبِحُونَ»" },
{ title: "صلاة المغرب 🌅", body: "«لَا تَزَالُ أُمَّتِي بِخَيْرٍ - أَوْ عَلَى الْفِطْرَةِ - مَا لَمْ يُؤَخِّرُوا الْمَغْرِبَ حَتَّى تَشْتَبِكَ النُّجُومُ» — سنن أبي داود" },
{ title: "صلاة المغرب 🌅", body: "«إِذَا أَقْبَلَ اللَّيْلُ مِنْ هَا هُنَا، وَأَدْبَرَ النَّهَارُ مِنْ هَا هُنَا، وَغَرَبَتِ الشَّمْسُ، فَقَدْ أَفْطَرَ الصَّائِمُ» — متفق عليه" }
],
Isha: [
{ title: "صلاة العشاء 🌙", body: "«وَمِنَ اللَّيْلِ فَتَهَجَّدْ بِهِ نَافِلَةً لَكَ عَسَى أَنْ يَبْعَثَكَ رَبُّكَ مَقَامًا مَحْمُودًا»" },
{ title: "صلاة العشاء 🌙", body: "«مَنْ شَهِدَ الْعِشَاءَ فِي جَمَاعَةٍ كَانَ لَهُ قِيَامُ نِصْفِ لَيْلَةٍ» — صحيح مسلم" },
{ title: "صلاة العشاء 🌙", body: "«لَوْ يَعْلَمُونَ مَا فِي الْعَتَمَةِ وَالصُّبْحِ لَأَتَوْهُمَا وَلَوْ حَبْوًا» — متفق عليه" },
{ title: "صلاة العشاء 🌙", body: "«كَانُوا قَلِيلًا مِنَ اللَّيْلِ مَا يَهْجَعُونَ * وَبِالْأَسْحَارِ هُمْ يَسْتَغْفِرُونَ»" }
]
};
// Pre-athan reminder
if (state.reminderEnabled && preMins > 0) {
const trigger = p.m - preMins;
const key = `${today}|${p.name}|pre`;
if (nowM >= trigger && nowM < trigger + 2 && !sent[key]) {
notify(key, `${p.name} in ${preMins} min`, `Prayer time at ${prayers[p.name]}.`);
sent[key] = true;
}
}
// At-time notification (athan)
if (state.athanEnabled) {
const keyAt = `${today}|${p.name}|at`;
if (nowM >= p.m && nowM < p.m + 2 && !sent[keyAt]) {
notify(keyAt, `${p.name} now`, `It's time for ${p.name} (${prayers[p.name]}).`);
sent[keyAt] = true;
}
}
// Iqama reminder (after athan)
if (state.iqamaEnabled && iqMins > 0) {
const iq = p.m + iqMins;
const keyIq = `${today}|${p.name}|iq`;
if (nowM >= iq && nowM < iq + 2 && !sent[keyIq]) {
notify(keyIq, `${p.name} Iqama`, `Iqama time — ${iqMins} min after athan.`);
sent[keyIq] = true;
}
}
const PRE_PRAYER_MESSAGES = [
{ title: "اقتراب موعد الصلاة ⏳", body: "«مَنْ تَطَهَّرَ فِي بَيْتِهِ، ثُمَّ مَشَى إِلَى بَيْتٍ مِنْ بُيُوتِ اللَّهِ... كَانَتْ خَطْوَتَاهُ إِحْدَاهُمَا تَحُطُّ خَطِيئَةً، وَالْأُخْرَى تَرْفَعُ دَرَجَةً» — صحيح مسلم" },
{ title: "اقتراب موعد الصلاة ⏳", body: "«إِسْبَاغُ الْوُضُوءِ عَلَى الْمَكَارِهِ، وَكَثْرَةُ الْخُطَا إِلَى الْمَسَاجِدِ، وَانْتِظَارُ الصَّلَاةِ بَعْدَ الصَّلَاةِ... يَمْحُو اللَّهُ بِهِ الْخَطَايَا» — صحيح مسلم" },
{ title: "اقتراب موعد الصلاة ⏳", body: "«إِنَّ اللَّهَ يُحِبُّ التَّوَّابِينَ وَيُحِبُّ الْمُتَطَهِّرِينَ»" },
{ title: "اقتراب موعد الصلاة ⏳", body: "قال معاذ رضي الله عنه: «إِذَا صَلَّيْتَ صَلَاةً، فَصَلِّ صَلَاةَ مُوَدِّعٍ، لَا تَظُنَّ أَنَّكَ تَعُودُ إِلَيْهَا أَبَدًا»" },
{ title: "اقتراب موعد الصلاة ⏳", body: "قال وكيع بن الجراح رحمه الله: «مَنْ لَمْ يَأْخُذْ أُهْبَةَ الصَّلَاةِ قَبْلَ وَقْتِهَا لَمْ يَكُنْ وَقَّرَهَا»" },
{ title: "اقتراب موعد الصلاة ⏳", body: "«الدُّعَاءُ لَا يُرَدُّ بَيْنَ الْأَذَانِ وَالْإِقَامَةِ» — سنن الترمذي" },
{ title: "اقتراب موعد الصلاة ⏳", body: "كتب عمر رضي الله عنه لعماله: «إِنَّ أَهَمَّ أُمُورِكُمْ عِنْدِي الصَّلَاةُ، فَمَنْ حَفِظَهَا وَحَافَظَ عَلَيْهَا حَفِظَ دِينَهُ»" }
];
function getRandomNotificationMessage(prayerName, isPre = false) {
if (isPre) {
const idx = Math.floor(Math.random() * PRE_PRAYER_MESSAGES.length);
return PRE_PRAYER_MESSAGES[idx];
}
const list = PRAYER_MESSAGES[prayerName] || [];
if (!list.length) {
return { title: `صلاة ${prayerName}`, body: `حان الآن موعد صلاة ${prayerName}` };
}
const idx = Math.floor(Math.random() * list.length);
return list[idx];
}
const config = notificationScheduler.normalizeSettings(state);
const cleaned = { ...(state._sentReminders || {}) };
for (const event of notificationScheduler.dueEvents(prayers, config, new Date(), previousPrayers || prayers)) {
if (cleaned[event.key]) continue;
const msg = event.type === "pre" ? getRandomNotificationMessage(event.prayer, true) : getRandomNotificationMessage(event.prayer, false);
const title = event.type === "pre" ? `${msg.title} (بعد ${event.minutes} دقيقة)` : event.type === "iqama" ? `إقامة صلاة ${event.prayer}` : msg.title;
const body = event.type === "iqama" ? `حان وقت الإقامة — بعد ${event.minutes} دقيقة من الأذان.` : msg.body;
const result = await notify(event.key, title, body, config.reminderSound);
if (result.ok) cleaned[event.key] = true;
}
// Keep only today's entries
const cleaned = {};
for (const k of Object.keys(sent)) if (k.startsWith(today + "|")) cleaned[k] = sent[k];
const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000;
for (const key of Object.keys(cleaned)) {
const date = new Date(`${key.split("|")[0]}T00:00:00`);
if (!Number.isNaN(date.getTime()) && date.getTime() < cutoff) delete cleaned[key];
}
await chrome.storage.local.set({ _sentReminders: cleaned });

@@ -177,4 +300,35 @@ }

chrome.alarms.onAlarm.addListener((a) => { if (a.name === "tick") tick(); });
chrome.runtime.onMessage.addListener((msg) => {
chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
if (msg && msg.type === "refresh") { ensureAlarm(); tick(); }
if (msg && msg.type === "test-notification") {
(async () => {
const state = await getState();
const result = await notify(`zakkir-test-${Date.now()}`, "Zakkir notification test", "Notifications are working. You will be reminded before and after each prayer you selected.", state.reminderSound || "adhan-1");
sendResponse(result);
})();
return true;
}
if (msg && msg.type === "status") {
(async () => {
const state = await getState();
const prayers = await ensurePrayers(state);
const next = await getNextReminder(state, prayers);
sendResponse({
lastTick: state._lastTick || null,
notificationsEnabled: state.notificationsEnabled,
prayerAlertEnabled: state.prayerAlertEnabled,
remindersEnabled: state.remindersEnabled,
iqamaEnabled: state.iqamaEnabled,
reminderPrayers: state.reminderPrayers,
reminderMinutes: state.reminderMinutes,
reminderMinutesByPrayer: state.reminderMinutesByPrayer,
iqamaMinutes: state.iqamaMinutes,
iqamaMinutesByPrayer: state.iqamaMinutesByPrayer,
cacheDate: state.prayerCache?.date || null,
hasPrayers: !!prayers,
next: next,
});
})();
return true;
}
});

@@ -181,0 +335,0 @@ // Run once on script load (covers SW wake-ups)

{
"manifest_version": 3,
"name": "Zakkir",
"version": "1.51",
"version": "1.52",
"description": "Prayer times + Azkar (Hisn al-Muslim) in your toolbar.",

@@ -33,2 +33,3 @@ "permissions": [

"scripts": [
"notification-scheduler.js",
"background.js"

@@ -59,3 +60,4 @@ ]

"vendor/marker-shadow.png",
"icon_48.png"
"icon_48.png",
"sounds/*"
],

@@ -62,0 +64,0 @@ "matches": [

@@ -11,4 +11,5 @@ <!doctype html>

<div id="app"><div class="boot">Loading…</div></div>
<script src="notification-scheduler.js"></script>
<script src="popup.js"></script>
</body>
</html>
node_modules/
test-results/

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display