app.component('navbar', { created() { this.load_def(); this.initSeenNotifications(); this.startPolling(); }, beforeUnmount() { this.stopPolling(); }, template: `
`, data() { return { poller: null, pollingDelay: 10000, isnotify: false, request: [], notify: [], seenNotifyIds: new Set(), logo: "/assets/img/logo3.png", audioPrimed: false, audioCtx: null, }; }, methods: { // --- INIT ---------------------------------------------------------- async load_def() { try { const res = await api.get('settings/requests_config.php'); this.isnotify = res.data.notify || false; } catch(err) { console.error("load_def failed:", err.message); } }, async initSeenNotifications() { try { const res = await api.get('settings/requests.php'); this.notify = res.data.notify || []; this.request = res.data.json || []; // Mark all current notifications as already seen so they // don't trigger a sound on first load. this.notify.forEach(n => { if(n.id) this.seenNotifyIds.add(n.id); }); } catch(err) { console.error("initSeenNotifications failed:", err.message); } }, // --- POLLING ------------------------------------------------------- startPolling() { const poll = async () => { // Only fetch when the tab is visible. if(!document.hidden) { await this.get(); } // Always reschedule so polling continues after a network error. this.poller = setTimeout(poll, this.pollingDelay); }; poll(); }, stopPolling() { clearTimeout(this.poller); this.poller = null; }, // --- FETCH --------------------------------------------------------- async get() { try { const res = await api.get('settings/requests.php'); const incoming = res.data.notify || []; // Only notifications not seen on init or in a previous poll. const newOnes = incoming.filter(n => !this.seenNotifyIds.has(n.id)); if(newOnes.length) { this.onNewNotifications(newOnes); } incoming.forEach(n => { if(n.id) this.seenNotifyIds.add(n.id); }); this.notify = incoming; this.request = res.data.json || []; } catch(err) { console.error("Notification fetch failed:", err.message); } }, // --- NEW NOTIFICATION HANDLER -------------------------------------- onNewNotifications(newItems) { this.playSound(); }, primeAudio() { if(this.audioPrimed) return; try { this.audioCtx = new (window.AudioContext || window.webkitAudioContext)(); if(this.audioCtx.state === "suspended") { this.audioCtx.resume(); } this.audioPrimed = true; } catch(err) { console.warn("Web Audio API not available:", err.message); } }, playSound() { try { const ctx = this.audioCtx || new (window.AudioContext || window.webkitAudioContext)(); if(ctx.state === "suspended") { ctx.resume(); } // Oscillator generates the tone. const oscillator = ctx.createOscillator(); oscillator.type = "sine"; oscillator.frequency.value = 880; // Hz // Gain node controls volume and fade-out. const gain = ctx.createGain(); gain.gain.setValueAtTime(0.3, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.4); oscillator.connect(gain); gain.connect(ctx.destination); oscillator.start(ctx.currentTime); oscillator.stop(ctx.currentTime + 0.4); } catch(err) { console.warn("playSound failed:", err.message); } }, // --- NOTIFICATIONS ------------------------------------------------- readnotify(obj, k) { api.post('settings/read.php', { index: obj.id }) .catch(err => console.error("readnotify failed:", err.message)); this.notify.splice(k, 1); }, // --- AUTH ---------------------------------------------------------- logout() { api.get('/logout/logout.php') .then(() => { this.stopPolling(); window.location.reload(); }) .catch(err => { console.error("Logout failed:", err.message); }); }, }, computed: { notifyCount() { return this.notify.length || 0; }, hasNotifications() { return this.notify.length > 0; }, hasRequests() { return this.request.length > 0; }, }, });