desktop-notifications.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. /**
  2. * @fileoverview Desktop notification utilities for cross-platform notifications
  3. * @author GrabZilla Development Team
  4. * @version 2.1.0
  5. * @since 2024-01-01
  6. */
  7. /**
  8. * DESKTOP NOTIFICATION UTILITIES
  9. *
  10. * Provides cross-platform desktop notifications with fallbacks
  11. * for different operating systems and notification systems.
  12. */
  13. /**
  14. * Notification types with default configurations
  15. */
  16. const NOTIFICATION_TYPES = {
  17. SUCCESS: {
  18. type: 'success',
  19. icon: 'assets/icons/download.svg',
  20. sound: true,
  21. timeout: 5000,
  22. color: '#00a63e'
  23. },
  24. ERROR: {
  25. type: 'error',
  26. icon: 'assets/icons/close.svg',
  27. sound: true,
  28. timeout: 8000,
  29. color: '#e7000b'
  30. },
  31. WARNING: {
  32. type: 'warning',
  33. icon: 'assets/icons/clock.svg',
  34. sound: false,
  35. timeout: 6000,
  36. color: '#ff9500'
  37. },
  38. INFO: {
  39. type: 'info',
  40. icon: 'assets/icons/logo.svg',
  41. sound: false,
  42. timeout: 4000,
  43. color: '#155dfc'
  44. },
  45. PROGRESS: {
  46. type: 'progress',
  47. icon: 'assets/icons/download.svg',
  48. sound: false,
  49. timeout: 0, // Persistent until updated
  50. color: '#155dfc'
  51. }
  52. };
  53. /**
  54. * Desktop Notification Manager
  55. *
  56. * Handles desktop notifications with cross-platform support
  57. * and intelligent fallbacks for different environments.
  58. */
  59. class DesktopNotificationManager {
  60. constructor() {
  61. this.activeNotifications = new Map();
  62. this.notificationQueue = [];
  63. this.isElectronAvailable = typeof window !== 'undefined' &&
  64. window.electronAPI &&
  65. typeof window.electronAPI === 'object';
  66. this.isBrowserNotificationSupported = typeof Notification !== 'undefined';
  67. this.maxActiveNotifications = 5;
  68. // Initialize notification permissions
  69. this.initializePermissions();
  70. }
  71. /**
  72. * Initialize notification permissions
  73. */
  74. async initializePermissions() {
  75. if (this.isBrowserNotificationSupported && !this.isElectronAvailable) {
  76. try {
  77. if (Notification.permission === 'default') {
  78. await Notification.requestPermission();
  79. }
  80. } catch (error) {
  81. console.warn('Failed to request notification permission:', error);
  82. }
  83. }
  84. }
  85. /**
  86. * Show desktop notification with automatic fallback
  87. * @param {Object} options - Notification options
  88. * @returns {Promise<Object>} Notification result
  89. */
  90. async showNotification(options = {}) {
  91. const config = this.prepareNotificationConfig(options);
  92. try {
  93. // Try Electron native notifications first
  94. if (this.isElectronAvailable) {
  95. return await this.showElectronNotification(config);
  96. }
  97. // Fallback to browser notifications
  98. if (this.isBrowserNotificationSupported) {
  99. return await this.showBrowserNotification(config);
  100. }
  101. // Final fallback to in-app notification
  102. return this.showInAppNotification(config);
  103. } catch (error) {
  104. console.error('Failed to show notification:', error);
  105. // Always fallback to in-app notification
  106. return this.showInAppNotification(config);
  107. }
  108. }
  109. /**
  110. * Show success notification for completed downloads
  111. * @param {string} filename - Downloaded filename
  112. * @param {Object} options - Additional options
  113. */
  114. async showDownloadSuccess(filename, options = {}) {
  115. const config = {
  116. type: NOTIFICATION_TYPES.SUCCESS,
  117. title: 'Download Complete',
  118. message: `Successfully downloaded: ${filename}`,
  119. ...options
  120. };
  121. return this.showNotification(config);
  122. }
  123. /**
  124. * Show error notification for failed downloads
  125. * @param {string} filename - Failed filename or URL
  126. * @param {string} error - Error message
  127. * @param {Object} options - Additional options
  128. */
  129. async showDownloadError(filename, error, options = {}) {
  130. const config = {
  131. type: NOTIFICATION_TYPES.ERROR,
  132. title: 'Download Failed',
  133. message: `Failed to download ${filename}: ${error}`,
  134. ...options
  135. };
  136. return this.showNotification(config);
  137. }
  138. /**
  139. * Show progress notification for ongoing downloads
  140. * @param {string} filename - Downloading filename
  141. * @param {number} progress - Progress percentage (0-100)
  142. * @param {Object} options - Additional options
  143. */
  144. async showDownloadProgress(filename, progress, options = {}) {
  145. const config = {
  146. type: NOTIFICATION_TYPES.PROGRESS,
  147. title: 'Downloading...',
  148. message: `${filename} - ${progress}% complete`,
  149. id: `progress_${filename}`,
  150. persistent: true,
  151. ...options
  152. };
  153. return this.showNotification(config);
  154. }
  155. /**
  156. * Show conversion progress notification
  157. * @param {string} filename - Converting filename
  158. * @param {number} progress - Progress percentage (0-100)
  159. * @param {Object} options - Additional options
  160. */
  161. async showConversionProgress(filename, progress, options = {}) {
  162. const config = {
  163. type: NOTIFICATION_TYPES.PROGRESS,
  164. title: 'Converting...',
  165. message: `${filename} - ${progress}% converted`,
  166. id: `conversion_${filename}`,
  167. persistent: true,
  168. ...options
  169. };
  170. return this.showNotification(config);
  171. }
  172. /**
  173. * Show dependency missing notification
  174. * @param {string} dependency - Missing dependency name
  175. * @param {Object} options - Additional options
  176. */
  177. async showDependencyMissing(dependency, options = {}) {
  178. const config = {
  179. type: NOTIFICATION_TYPES.ERROR,
  180. title: 'Missing Dependency',
  181. message: `${dependency} is required but not found. Please check the application setup.`,
  182. timeout: 10000, // Show longer for critical errors
  183. ...options
  184. };
  185. return this.showNotification(config);
  186. }
  187. /**
  188. * Prepare notification configuration with defaults
  189. * @param {Object} options - User options
  190. * @returns {Object} Complete configuration
  191. */
  192. prepareNotificationConfig(options) {
  193. const typeConfig = options.type || NOTIFICATION_TYPES.INFO;
  194. return {
  195. id: options.id || this.generateNotificationId(),
  196. title: options.title || 'GrabZilla',
  197. message: options.message || '',
  198. icon: options.icon || typeConfig.icon,
  199. sound: options.sound !== undefined ? options.sound : typeConfig.sound,
  200. timeout: options.timeout !== undefined ? options.timeout : typeConfig.timeout,
  201. persistent: options.persistent || false,
  202. onClick: options.onClick || null,
  203. onClose: options.onClose || null,
  204. type: typeConfig,
  205. timestamp: new Date()
  206. };
  207. }
  208. /**
  209. * Show notification using Electron's native system
  210. * @param {Object} config - Notification configuration
  211. * @returns {Promise<Object>} Result
  212. */
  213. async showElectronNotification(config) {
  214. try {
  215. const result = await window.electronAPI.showNotification({
  216. title: config.title,
  217. message: config.message,
  218. icon: config.icon,
  219. sound: config.sound,
  220. timeout: config.timeout / 1000, // Convert to seconds
  221. wait: config.persistent
  222. });
  223. if (result.success) {
  224. this.trackNotification(config);
  225. }
  226. return {
  227. success: result.success,
  228. method: 'electron',
  229. id: config.id,
  230. error: result.error
  231. };
  232. } catch (error) {
  233. console.error('Electron notification failed:', error);
  234. throw error;
  235. }
  236. }
  237. /**
  238. * Show notification using browser's Notification API
  239. * @param {Object} config - Notification configuration
  240. * @returns {Promise<Object>} Result
  241. */
  242. async showBrowserNotification(config) {
  243. try {
  244. if (Notification.permission !== 'granted') {
  245. throw new Error('Notification permission not granted');
  246. }
  247. const notification = new Notification(config.title, {
  248. body: config.message,
  249. icon: config.icon,
  250. silent: !config.sound,
  251. tag: config.id // Prevents duplicate notifications
  252. });
  253. // Handle events
  254. if (config.onClick) {
  255. notification.onclick = config.onClick;
  256. }
  257. if (config.onClose) {
  258. notification.onclose = config.onClose;
  259. }
  260. // Auto-close if not persistent
  261. if (!config.persistent && config.timeout > 0) {
  262. setTimeout(() => {
  263. notification.close();
  264. }, config.timeout);
  265. }
  266. this.trackNotification(config, notification);
  267. return {
  268. success: true,
  269. method: 'browser',
  270. id: config.id,
  271. notification
  272. };
  273. } catch (error) {
  274. console.error('Browser notification failed:', error);
  275. throw error;
  276. }
  277. }
  278. /**
  279. * Show in-app notification as final fallback
  280. * @param {Object} config - Notification configuration
  281. * @returns {Object} Result
  282. */
  283. showInAppNotification(config) {
  284. try {
  285. // Dispatch custom event for UI components to handle
  286. const notificationEvent = new CustomEvent('app-notification', {
  287. detail: {
  288. id: config.id,
  289. title: config.title,
  290. message: config.message,
  291. type: config.type.type,
  292. icon: config.icon,
  293. timeout: config.timeout,
  294. persistent: config.persistent,
  295. timestamp: config.timestamp
  296. }
  297. });
  298. document.dispatchEvent(notificationEvent);
  299. this.trackNotification(config);
  300. return {
  301. success: true,
  302. method: 'in-app',
  303. id: config.id
  304. };
  305. } catch (error) {
  306. console.error('In-app notification failed:', error);
  307. return {
  308. success: false,
  309. method: 'in-app',
  310. id: config.id,
  311. error: error.message
  312. };
  313. }
  314. }
  315. /**
  316. * Update existing notification (for progress updates)
  317. * @param {string} id - Notification ID
  318. * @param {Object} updates - Updates to apply
  319. */
  320. async updateNotification(id, updates) {
  321. const existing = this.activeNotifications.get(id);
  322. if (!existing) {
  323. // Create new notification if doesn't exist
  324. return this.showNotification({ id, ...updates });
  325. }
  326. const updatedConfig = { ...existing.config, ...updates };
  327. // Close existing and show updated
  328. this.closeNotification(id);
  329. return this.showNotification(updatedConfig);
  330. }
  331. /**
  332. * Close specific notification
  333. * @param {string} id - Notification ID
  334. */
  335. closeNotification(id) {
  336. const notification = this.activeNotifications.get(id);
  337. if (notification) {
  338. if (notification.instance && notification.instance.close) {
  339. notification.instance.close();
  340. }
  341. this.activeNotifications.delete(id);
  342. }
  343. }
  344. /**
  345. * Close all active notifications
  346. */
  347. closeAllNotifications() {
  348. for (const [id] of this.activeNotifications) {
  349. this.closeNotification(id);
  350. }
  351. }
  352. /**
  353. * Track active notification
  354. * @param {Object} config - Notification configuration
  355. * @param {Object} instance - Notification instance (if available)
  356. */
  357. trackNotification(config, instance = null) {
  358. this.activeNotifications.set(config.id, {
  359. config,
  360. instance,
  361. timestamp: config.timestamp
  362. });
  363. // Clean up old notifications
  364. this.cleanupOldNotifications();
  365. }
  366. /**
  367. * Clean up old notifications to prevent memory leaks
  368. */
  369. cleanupOldNotifications() {
  370. const maxAge = 5 * 60 * 1000; // 5 minutes
  371. const now = new Date();
  372. for (const [id, notification] of this.activeNotifications) {
  373. if (now - notification.timestamp > maxAge) {
  374. this.closeNotification(id);
  375. }
  376. }
  377. // Limit total active notifications
  378. if (this.activeNotifications.size > this.maxActiveNotifications) {
  379. const oldest = Array.from(this.activeNotifications.entries())
  380. .sort((a, b) => a[1].timestamp - b[1].timestamp)
  381. .slice(0, this.activeNotifications.size - this.maxActiveNotifications);
  382. oldest.forEach(([id]) => this.closeNotification(id));
  383. }
  384. }
  385. /**
  386. * Generate unique notification ID
  387. * @returns {string} Unique ID
  388. */
  389. generateNotificationId() {
  390. return `notification_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  391. }
  392. /**
  393. * Get notification statistics
  394. * @returns {Object} Statistics
  395. */
  396. getStats() {
  397. return {
  398. active: this.activeNotifications.size,
  399. electronAvailable: this.isElectronAvailable,
  400. browserSupported: this.isBrowserNotificationSupported,
  401. permission: this.isBrowserNotificationSupported ? Notification.permission : 'unknown'
  402. };
  403. }
  404. /**
  405. * Test notification system
  406. * @returns {Promise<Object>} Test results
  407. */
  408. async testNotifications() {
  409. const results = {
  410. electron: false,
  411. browser: false,
  412. inApp: false,
  413. errors: []
  414. };
  415. // Test Electron notifications
  416. if (this.isElectronAvailable) {
  417. try {
  418. const result = await this.showElectronNotification({
  419. id: 'test_electron',
  420. title: 'Test Notification',
  421. message: 'Electron notifications are working',
  422. timeout: 2000
  423. });
  424. results.electron = result.success;
  425. } catch (error) {
  426. results.errors.push(`Electron: ${error.message}`);
  427. }
  428. }
  429. // Test browser notifications
  430. if (this.isBrowserNotificationSupported) {
  431. try {
  432. const result = await this.showBrowserNotification({
  433. id: 'test_browser',
  434. title: 'Test Notification',
  435. message: 'Browser notifications are working',
  436. timeout: 2000
  437. });
  438. results.browser = result.success;
  439. } catch (error) {
  440. results.errors.push(`Browser: ${error.message}`);
  441. }
  442. }
  443. // Test in-app notifications
  444. try {
  445. const result = this.showInAppNotification({
  446. id: 'test_inapp',
  447. title: 'Test Notification',
  448. message: 'In-app notifications are working',
  449. timeout: 2000
  450. });
  451. results.inApp = result.success;
  452. } catch (error) {
  453. results.errors.push(`In-app: ${error.message}`);
  454. }
  455. return results;
  456. }
  457. }
  458. // Create global notification manager instance
  459. const notificationManager = new DesktopNotificationManager();
  460. // Export for use in other modules
  461. if (typeof module !== 'undefined' && module.exports) {
  462. module.exports = {
  463. DesktopNotificationManager,
  464. notificationManager,
  465. NOTIFICATION_TYPES
  466. };
  467. } else {
  468. // Browser environment - attach to window
  469. window.DesktopNotificationManager = DesktopNotificationManager;
  470. window.notificationManager = notificationManager;
  471. window.NOTIFICATION_TYPES = NOTIFICATION_TYPES;
  472. }