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;
},
},
});