//============================================================================= // #region ACTIONS //============================================================================= const NotificationActionTypes = { ADD_NOTIFICATION: 'ADD_NOTIFICATION', REMOVE_NOTIFICATION: 'REMOVE_NOTIFICATION', CLEAR_NOTIFICATIONS: 'CLEAR_NOTIFICATIONS', UPDATE_NOTIFICATION: 'UPDATE_NOTIFICATION' }; const notificationActions = { addNotification: (notification) => ({ type: NotificationActionTypes.ADD_NOTIFICATION, payload: { id: Date.now() + Math.random(), timestamp: Date.now(), type: 'info', title: 'Notification', message: 'Default message', duration: 0, status: 'showing', ...notification } }), removeNotification: (id) => ({ type: NotificationActionTypes.REMOVE_NOTIFICATION, payload: { id } }), clearNotifications: () => ({ type: NotificationActionTypes.CLEAR_NOTIFICATIONS }), updateNotification: (id, updates) => ({ type: NotificationActionTypes.UPDATE_NOTIFICATION, payload: { id, updates } }) }; //============================================================================= // #region REDUCER //============================================================================= const notificationInitialState = { notifications: [], maxNotifications: 5 }; function notificationReducer(state = notificationInitialState, action = {}) { switch (action.type) { case NotificationActionTypes.ADD_NOTIFICATION: { if (!action.payload) return state; let newNotifications = [...state.notifications]; if (newNotifications.length >= state.maxNotifications) { newNotifications = newNotifications.slice(1); } return { ...state, notifications: [...newNotifications, action.payload] }; } case NotificationActionTypes.REMOVE_NOTIFICATION: { if (!action.payload || !action.payload.id) return state; return { ...state, notifications: state.notifications.filter(n => n.id !== action.payload.id) }; } case NotificationActionTypes.CLEAR_NOTIFICATIONS: return { ...state, notifications: [] }; case NotificationActionTypes.UPDATE_NOTIFICATION: { if (!action.payload || !action.payload.id || !action.payload.updates) return state; return { ...state, notifications: state.notifications.map(n => n.id === action.payload.id ? { ...n, ...action.payload.updates } : n ) }; } default: return state; } } //============================================================================= // #region STORE //============================================================================= class Store { constructor(reducer, initialState) { this.reducer = reducer; this.state = initialState; this.listeners = []; } getState() { return this.state; } dispatch(action) { this.state = this.reducer(this.state, action); this.listeners.forEach(listener => listener(this.state)); return action; } subscribe(listener) { this.listeners.push(listener); return () => { this.listeners = this.listeners.filter(l => l !== listener); }; } } const notificationStore = new Store(notificationReducer, notificationInitialState); //============================================================================= // #region SELECTORS //============================================================================= const notificationSelectors = { getNotifications: (state) => state.notifications, getMaxNotifications: (state) => state.maxNotifications }; //============================================================================= // #region UI COMPONENT //============================================================================= class NotificationUI { constructor(store) { this.store = store; this.unsubscribe = null; this.container = document.getElementById('notification-container'); this.renderedNotifications = new Map(); } init() { if (!this.container) { console.error('Notification container not found'); return; } this.unsubscribe = this.store.subscribe((state) => this.render(state)); this.render(this.store.getState()); } destroy() { if (this.unsubscribe) this.unsubscribe(); this.renderedNotifications.forEach(el => { if (el.parentNode) el.parentNode.removeChild(el); }); this.renderedNotifications.clear(); } render(state) { const notifications = notificationSelectors.getNotifications(state); // Remove notifications no longer present const currentIds = new Set(notifications.map(n => n.id)); for (const [id, el] of this.renderedNotifications.entries()) { if (!currentIds.has(id)) { if (el.parentNode) el.parentNode.removeChild(el); this.renderedNotifications.delete(id); } } // Add or update notifications notifications.forEach(notification => { if (!notification || !notification.id) return; if (!this.renderedNotifications.has(notification.id)) { this.createNotificationElement(notification); } else { this.updateNotificationElement(notification); } }); } createNotificationElement(notification) { const el = document.createElement('div'); el.className = `notification ${notification.type || 'info'}`; el.dataset.id = notification.id; el.innerHTML = `
${notification.title || 'Notification'}
${notification.message || 'No message'}
${notification.duration > 0 ? '
' : ''} `; this.container.appendChild(el); this.renderedNotifications.set(notification.id, el); setTimeout(() => el.classList.add('show'), 10); // Set progress bar animation duration if (notification.duration > 0) { const progressBar = el.querySelector('.notification-progress-bar'); if (progressBar) { progressBar.style.transition = `width ${notification.duration}ms linear`; progressBar.style.width = '100%'; setTimeout(() => { progressBar.style.width = '0%'; }, 20); } setTimeout(() => { notificationStore.dispatch(notificationActions.updateNotification(notification.id, { status: 'hiding' })); setTimeout(() => { notificationStore.dispatch(notificationActions.removeNotification(notification.id)); }, 300); }, notification.duration); } } updateNotificationElement(notification) { const el = this.renderedNotifications.get(notification.id); if (!el) return; if (notification.status === 'hiding') el.classList.add('hide'); } } //============================================================================= // #region GLOBAL API & EVENT HANDLING //============================================================================= let notificationUI = null; let notificationUIInitialized = false; function notifyArmaNotificationReady() { if (window.parent && window.parent !== window && typeof window.parent.postMessage === "function") { window.parent.postMessage({ event: "notifications::ready" }, "*"); } if (typeof A3API !== "undefined" && typeof A3API.SendAlert === "function") { A3API.SendAlert(JSON.stringify({ event: "notifications::ready" })); } } function initializeNotifications() { if (notificationUIInitialized) { console.log('Notification system already initialized, skipping...'); return; } notificationUI = new NotificationUI(notificationStore); notificationUI.init(); notificationUIInitialized = true; console.log('Notification system is ready!'); notifyArmaNotificationReady(); } // Expose global notification API const showNotification = (type, title, message, duration) => { return notificationStore.dispatch(notificationActions.addNotification({ type, title, message, duration })); }; const clearAllNotifications = () => { return notificationStore.dispatch(notificationActions.clearNotifications()); }; // Listen for global notification events (for Arma/SQF or other scripts) window.addEventListener('forge:notify', function (e) { if (!e || !e.detail) return; const { type, title, message, duration } = e.detail; showNotification(type, title, message, duration); }); // Auto-initialize if DOM is already loaded when script executes if (document.readyState !== 'loading') { initializeNotifications(); } else { document.addEventListener('DOMContentLoaded', initializeNotifications, { once: true }); }