menu-manager.js
1 import AppConstants from '../config/constants.js'; 2 import { createEventEmitter } from '../utils/event/event-emitter.js'; 3 import { MenuEventData, MenuEvents } from '../services/menu-events.js'; 4 import { showIconAnimation } from '../utils/animation/icon-animation.js'; 5 import { queryElement } from '../utils/dom/element-query.js'; 6 import attachMenuEventListenersDefault from '../utils/event/attach-menu-event-listeners.js'; 7 import bindEventHandlersDefault from '../utils/event/bind-event-handlers.js'; 8 import detachMenuEventListenersDefault from '../utils/event/detach-menu-event-listeners.js'; 9 import { createLocalEventSubscriptionManager } from '../utils/event/local-events.js'; 10 import { 11 checkMenuVisibility, 12 handleMenuTransition, 13 toggleMenuVisibility, 14 getMenuElements as getMenuElementsUtil, 15 } from '../utils/ui/menu-transitions.js'; 16 import { showMessage, showTemporaryMessage } from '../utils/ui/message.js'; 17 18 import { PlaylistUIManager } from './playlist-ui-manager.js'; 19 export class MenuManager { 20 constructor(uiManager) { 21 this.events = createEventEmitter(); 22 23 this.subscriptions = createLocalEventSubscriptionManager(); 24 25 this.uiManager = uiManager; 26 this.CssClasses = AppConstants.UI.CSS_CLASSES; 27 28 this.eventHandlers = {}; 29 this._languageButtonListenersAttached = false; 30 31 this._statsButtonListenerAttached = false; 32 33 this._eventSubscriptions = []; 34 35 this._menuOperationInProgress = false; 36 this._pendingMenuOperations = []; 37 38 this._menuOperationLocks = { 39 left: false, 40 right: false, 41 }; 42 43 this._menuState = { 44 left: false, 45 right: false, 46 }; 47 48 this.uiManagerEvents = uiManager?.getEventEmitter?.(); 49 50 this._initialize(); 51 } 52 53 getEventEmitter() { 54 return this.events; 55 } 56 57 async _initialize() { 58 try { 59 await this._initializeEventHandlers(); 60 61 this._subscribeToMenuEvents(); 62 63 this._initialized = true; 64 } catch (error) { 65 console.error('Failed to initialize menu manager:', error); 66 } 67 } 68 69 _subscribeToMenuEvents() { 70 try { 71 const handleOperationComplete = data => { 72 if (data?.side && data.operation) { 73 this._handleMenuOperationComplete(data.side, data.operation); 74 } 75 }; 76 77 let closeRequestDebounceTimer = null; 78 const handleCloseRequested = data => { 79 if (data?.side && this.isMenuOpen(data.side)) { 80 if (closeRequestDebounceTimer) { 81 clearTimeout(closeRequestDebounceTimer); 82 } 83 closeRequestDebounceTimer = setTimeout(() => { 84 this.closeMenu(data.side); 85 closeRequestDebounceTimer = null; 86 }, 100); 87 } 88 }; 89 90 const readyComponents = new Set(); 91 const handleCloseReady = data => { 92 if (data && data.side === 'right' && data.component) { 93 readyComponents.add(data.component); 94 95 if (readyComponents.has('playlist-ui') && readyComponents.has('playlist-data')) { 96 readyComponents.clear(); 97 this._executeMenuClose(data.side); 98 } 99 } 100 }; 101 102 this.subscriptions.subscribe( 103 this.events, 104 MenuEvents.MENU_OPERATION_COMPLETE, 105 handleOperationComplete, 106 this, 107 ); 108 109 this.subscriptions.subscribe( 110 this.events, 111 MenuEvents.MENU_CLOSE_REQUESTED, 112 handleCloseRequested, 113 this, 114 ); 115 116 this.subscriptions.subscribe( 117 this.events, 118 MenuEvents.MENU_CLOSE_READY, 119 handleCloseReady, 120 this, 121 ); 122 123 if (this.uiManagerEvents) { 124 this.subscriptions.subscribe( 125 this.uiManagerEvents, 126 MenuEvents.MENU_OPERATION_COMPLETE, 127 handleOperationComplete, 128 this, 129 ); 130 131 this.subscriptions.subscribe( 132 this.uiManagerEvents, 133 MenuEvents.MENU_CLOSE_REQUESTED, 134 handleCloseRequested, 135 this, 136 ); 137 138 this.subscriptions.subscribe( 139 this.uiManagerEvents, 140 MenuEvents.MENU_CLOSE_READY, 141 handleCloseReady, 142 this, 143 ); 144 } else { 145 console.warn('UIManager event emitter not available, menu events may not work properly'); 146 } 147 } catch (error) { 148 console.error('Failed to subscribe to menu events:', error); 149 } 150 } 151 152 closeLeftMenu() { 153 return this.closeMenu('left'); 154 } 155 156 closeRightMenu() { 157 return this.closeMenu('right'); 158 } 159 160 toggleLeftMenu() { 161 return this.toggleMenu('left'); 162 } 163 164 toggleRightMenu() { 165 return this.toggleMenu('right'); 166 } 167 168 async _initializeEventHandlers() { 169 if (!this.uiManager) { 170 console.error('UI Manager not available, cannot initialize menu event handlers'); 171 return; 172 } 173 174 try { 175 const handlers = { 176 closeLeftMenu: this.closeLeftMenu, 177 closeRightMenu: this.closeRightMenu, 178 toggleLeftMenu: this.toggleLeftMenu, 179 toggleRightMenu: this.toggleRightMenu, 180 }; 181 182 this.eventHandlers = bindEventHandlersDefault(this, handlers); 183 184 if (!this.eventHandlers || Object.keys(this.eventHandlers).length === 0) { 185 throw new Error('Failed to bind event handlers'); 186 } 187 188 await this.attachMenuEventListeners(); 189 } catch (error) { 190 console.error('Failed to initialize menu event handlers:', error); 191 } 192 } 193 194 async attachMenuEventListeners() { 195 if (!this.uiManager) { 196 console.error('UI Manager not available, cannot attach menu event listeners'); 197 return; 198 } 199 200 if (!this.eventHandlers || Object.keys(this.eventHandlers).length === 0) { 201 console.error('Event handlers not initialized, cannot attach menu event listeners'); 202 return; 203 } 204 205 try { 206 const elements = { 207 leftMenuCloseButton: this.uiManager.get('leftMenuCloseButton'), 208 leftMenuToggleButton: this.uiManager.get('leftMenuToggleButton'), 209 rightMenuCloseButton: this.uiManager.get('rightMenuCloseButton'), 210 rightMenuToggleButton: this.uiManager.get('rightMenuToggleButton'), 211 }; 212 const hasButtons = Object.values(elements).some( 213 element => element !== null && element !== undefined, 214 ); 215 if (!hasButtons) { 216 console.warn('No menu buttons found, skipping event listener attachment'); 217 return; 218 } 219 220 attachMenuEventListenersDefault(elements, this.eventHandlers); 221 } catch (error) { 222 console.error('Failed to attach menu event listeners:', error); 223 } 224 } 225 isMenuOpen(side) { 226 if (side !== 'left' && side !== 'right') { 227 console.error(`Invalid menu side: ${side}. Must be 'left' or 'right'.`); 228 return false; 229 } 230 231 if (!this.uiManager) { 232 console.error('UI Manager not available, cannot check menu state'); 233 return false; 234 } 235 236 try { 237 const { menu } = this._getMenuElements(side); 238 239 if (!menu) { 240 console.warn(`Menu element for side '${side}' not found`); 241 this._menuState[side] = false; 242 return false; 243 } 244 245 const isVisible = checkMenuVisibility(menu, this.CssClasses.VISIBLE); 246 this._menuState[side] = isVisible; 247 return isVisible; 248 } catch (error) { 249 console.error('Failed to check menu state:', error); 250 return this._menuState[side]; 251 } 252 } 253 254 scheduleMenuOperation(operation, side) { 255 return new Promise((resolve, reject) => { 256 const menuOperation = { 257 operation, 258 reject, 259 resolve, 260 side, 261 timestamp: Date.now(), 262 }; 263 this._pendingMenuOperations.push(menuOperation); 264 265 if (!this._menuOperationInProgress) { 266 this._processNextMenuOperation(); 267 } 268 }); 269 } 270 271 async _processNextMenuOperation() { 272 if (this._pendingMenuOperations.length === 0) { 273 this._menuOperationInProgress = false; 274 return; 275 } 276 277 this._menuOperationInProgress = true; 278 279 const { operation, reject, resolve, side, timestamp } = this._pendingMenuOperations.shift(); 280 const waitTime = Date.now() - timestamp; 281 282 try { 283 const isOpen = this.isMenuOpen(side); 284 285 if ((operation === 'open' && isOpen) || (operation === 'close' && !isOpen)) { 286 resolve(); 287 288 this._processNextMenuOperation(); 289 return; 290 } 291 292 if (operation === 'open') { 293 await this._executeOpenMenu(side); 294 resolve(); 295 } else if (operation === 'close') { 296 this._executeCloseMenu(side); 297 resolve(); 298 } 299 } catch (error) { 300 console.error(`Failed to execute menu ${operation} operation:`, error); 301 reject(error); 302 303 this._processNextMenuOperation(); 304 } finally { 305 const eventData = { operation, side }; 306 307 this.events.publish(MenuEvents.MENU_OPERATION_COMPLETE, eventData); 308 309 this._processNextMenuOperation(); 310 } 311 } 312 313 _handleMenuOperationComplete(side, operation) { 314 this._processNextMenuOperation(); 315 } 316 317 toggleMenu(side) { 318 if (this._menuOperationLocks[side]) { 319 return Promise.resolve(); 320 } 321 322 this._menuOperationLocks[side] = true; 323 324 const isOpen = this.isMenuOpen(side); 325 this._menuState[side] = isOpen; 326 327 const oppositeSide = side === 'left' ? 'right' : 'left'; 328 const isOppositeOpen = this.isMenuOpen(oppositeSide); 329 330 let promise; 331 332 if (isOpen) { 333 promise = this.closeMenu(side); 334 } else if (isOppositeOpen) { 335 this._menuOperationLocks[oppositeSide] = true; 336 337 promise = this.closeMenu(oppositeSide) 338 .then(() => this.openMenu(side)) 339 .finally(() => { 340 this._menuOperationLocks[oppositeSide] = false; 341 }); 342 } else { 343 promise = this.openMenu(side); 344 } 345 346 promise 347 .catch(error => console.error(`Error during toggle operation for ${side} menu:`, error)) 348 .finally(() => { 349 this._menuOperationLocks[side] = false; 350 }); 351 352 return promise; 353 } 354 355 async _executeOpenMenu(side) { 356 try { 357 await this._doOpenMenu(side); 358 359 const eventData = { 360 operation: 'open', 361 side, 362 }; 363 364 this.events.publish(MenuEvents.MENU_OPERATION_COMPLETE, eventData); 365 } catch (error) { 366 console.error(`Failed to execute open menu for ${side}:`, error); 367 368 const eventData = { 369 operation: 'open', 370 side, 371 }; 372 373 this.events.publish(MenuEvents.MENU_OPERATION_COMPLETE, eventData); 374 } 375 } 376 377 _executeCloseMenu(side) { 378 try { 379 this._doCloseMenu(side); 380 381 const eventData = { 382 operation: 'close', 383 side, 384 }; 385 386 this.events.publish(MenuEvents.MENU_OPERATION_COMPLETE, eventData); 387 } catch (error) { 388 console.error(`Failed to execute close menu for ${side}:`, error); 389 390 const eventData = { 391 operation: 'close', 392 side, 393 }; 394 395 this.events.publish(MenuEvents.MENU_OPERATION_COMPLETE, eventData); 396 } 397 } 398 399 _executeMenuClose(side) { 400 try { 401 this._executeCloseMenu(side); 402 } catch (error) { 403 console.error(`Failed to execute coordinated menu close for ${side}:`, error); 404 405 try { 406 this._doCloseMenu(side); 407 } catch (fallbackError) { 408 console.error(`Even fallback close failed for ${side} menu:`, fallbackError); 409 } 410 } 411 } 412 413 _requestMenuOperation(operation, side) { 414 const hasPendingOperation = this._pendingMenuOperations.some( 415 op => op.side === side && op.operation === operation, 416 ); 417 418 if (hasPendingOperation) { 419 return Promise.resolve(); 420 } 421 422 const isOpen = this.isMenuOpen(side); 423 if ((operation === 'open' && isOpen) || (operation === 'close' && !isOpen)) { 424 return Promise.resolve(); 425 } 426 427 return this.scheduleMenuOperation(operation, side); 428 } 429 430 openMenu(side) { 431 return this._requestMenuOperation('open', side); 432 } 433 434 _getMenuElements(side) { 435 return getMenuElementsUtil(this.uiManager, side); 436 } 437 438 async _handleOppositeMenu(side) { 439 const oppositeSide = side === 'left' ? 'right' : 'left'; 440 const { menu: oppositeMenu } = this._getMenuElements(oppositeSide); 441 442 const isOppositeOpen = checkMenuVisibility(oppositeMenu, this.CssClasses.VISIBLE); 443 this._menuState[oppositeSide] = isOppositeOpen; 444 445 if (isOppositeOpen) { 446 if (!this._menuOperationLocks[oppositeSide]) { 447 await this.scheduleMenuOperation('close', oppositeSide); 448 } else { 449 console.warn(`Cannot close ${oppositeSide} menu: operation locked`); 450 } 451 } 452 } 453 454 async _loadMenuContent(side, menu) { 455 menu.classList.add('content-loading'); 456 457 if (side === 'left') { 458 await this._attachLanguageButtonListeners(); 459 await this._attachStatsButtonListener(); 460 await this._attachLivestreamButtonListener(); 461 } 462 463 menu.classList.remove('content-loading'); 464 } 465 466 _updateMenuState(side, menu, btn) { 467 const onTransitionComplete = () => { 468 this._menuState[side] = true; 469 this.uiManager?.onMenuOpened(side); 470 471 const eventData = MenuEventData.createMenuSideData(side); 472 473 this.events.publish(MenuEvents.MENU_OPENED, eventData); 474 }; 475 476 handleMenuTransition(menu, this.CssClasses.VISIBLE, true, onTransitionComplete, btn); 477 } 478 479 async _doOpenMenu(side) { 480 try { 481 const { menu, btn } = this._getMenuElements(side); 482 483 if (!menu || !btn) { 484 console.warn(`Menu elements for side '${side}' not found`); 485 return; 486 } 487 488 const isOpen = menu.classList.contains(this.CssClasses.VISIBLE); 489 if (isOpen) { 490 this._menuState[side] = true; 491 return; 492 } 493 494 const oppositeSide = side === 'left' ? 'right' : 'left'; 495 const isOppositeMenuLocked = this._menuOperationLocks[oppositeSide]; 496 497 if (!isOppositeMenuLocked) { 498 await this._handleOppositeMenu(side); 499 } 500 501 await this._loadMenuContent(side, menu); 502 503 this._updateMenuState(side, menu, btn); 504 } catch (error) { 505 console.error(`Failed to open ${side} menu:`, error); 506 507 try { 508 const { menu, btn } = this._getMenuElements(side); 509 510 if (menu && btn) { 511 menu.classList.remove('content-loading'); 512 const onTransitionComplete = () => { 513 this._menuState[side] = true; 514 this.uiManager?.onMenuOpened(side); 515 516 const eventData = MenuEventData.createMenuSideData(side); 517 518 this.events.publish(MenuEvents.MENU_OPENED, eventData); 519 }; 520 handleMenuTransition(menu, this.CssClasses.VISIBLE, true, onTransitionComplete, btn); 521 } 522 } catch (recoveryError) { 523 console.error(`Failed to recover from ${side} menu open error:`, recoveryError); 524 } 525 } 526 } 527 528 closeMenu(side) { 529 return this._requestMenuOperation('close', side); 530 } 531 532 _doCloseMenu(side) { 533 try { 534 const { menu, btn } = this._getMenuElements(side); 535 536 if (!menu || !btn) { 537 console.warn(`Menu elements for side '${side}' not found`); 538 return; 539 } 540 541 const isOpen = checkMenuVisibility(menu, this.CssClasses.VISIBLE); 542 if (!isOpen) { 543 this._menuState[side] = false; 544 return; 545 } 546 547 const onTransitionComplete = () => { 548 this._menuState[side] = false; 549 this.uiManager?.onMenuClosed(side); 550 551 const eventData = MenuEventData.createMenuSideData(side); 552 553 this.events.publish(MenuEvents.MENU_CLOSED, eventData); 554 }; 555 556 handleMenuTransition(menu, this.CssClasses.VISIBLE, false, onTransitionComplete, btn); 557 } catch (error) { 558 console.error(`Failed to close ${side} menu:`, error); 559 560 try { 561 const { menu, btn } = this._getMenuElements(side); 562 563 if (menu && btn) { 564 menu.classList.remove('content-loading'); 565 const onTransitionComplete = () => { 566 this._menuState[side] = false; 567 this.uiManager?.onMenuClosed(side); 568 569 const eventData = MenuEventData.createMenuSideData(side); 570 571 this.events.publish(MenuEvents.MENU_CLOSED, eventData); 572 }; 573 handleMenuTransition(menu, this.CssClasses.VISIBLE, false, onTransitionComplete, btn); 574 } 575 } catch (recoveryError) { 576 console.error(`Failed to recover from ${side} menu close error:`, recoveryError); 577 } 578 } 579 } 580 581 async _ensureLanguageManagerLoaded() { 582 if (!this.uiManager) { 583 console.error('UI Manager not available, cannot load LanguageManager'); 584 return null; 585 } 586 try { 587 return await this.uiManager._ensureLanguageManagerLoaded(); 588 } catch (error) { 589 console.error('Failed to load LanguageManager:', error); 590 return null; 591 } 592 } 593 594 async _ensureStatsUIManagerLoaded() { 595 if (!this.uiManager) { 596 console.error('UI Manager not available, cannot load StatsUIManager'); 597 return null; 598 } 599 try { 600 return await this.uiManager._ensureStatsUIManagerLoaded(); 601 } catch (error) { 602 console.error('Failed to load StatsUIManager:', error); 603 return null; 604 } 605 } 606 607 async _ensurePlaylistUIManagerLoaded() { 608 if (!this.uiManager) { 609 console.error('UI Manager not available, cannot load PlaylistUIManager'); 610 return null; 611 } 612 try { 613 if (!this.uiManager._playlistUIManager) { 614 this.uiManager._playlistUIManager = new PlaylistUIManager( 615 this.uiManager, 616 this.uiManager.videoController, 617 this.uiManager.videoSwitcher, 618 ); 619 } 620 return this.uiManager._playlistUIManager; 621 } catch (error) { 622 console.error('Failed to load PlaylistUIManager:', error); 623 return null; 624 } 625 } 626 627 _attachLanguageButtonListeners() { 628 if (!this.uiManager) { 629 console.error('UI Manager not available, cannot attach language button listeners'); 630 return; 631 } 632 const languageSelector = this.uiManager.get('languageSelector'); 633 if (!languageSelector) { 634 console.warn('Language selector element not found, cannot attach language button listeners'); 635 return; 636 } 637 const japaneseButton = queryElement(languageSelector, '[data-lang="ja"]'); 638 const chineseButton = queryElement(languageSelector, '[data-lang="zh"]'); 639 640 if (this._languageButtonListenersAttached) { 641 return; 642 } 643 644 if (japaneseButton) { 645 japaneseButton.addEventListener('click', async () => { 646 try { 647 const languageManager = await this._ensureLanguageManagerLoaded(); 648 if (languageManager) { 649 await languageManager.handleLanguageSelection('ja'); 650 } 651 } catch (error) { 652 console.error('Failed to handle Japanese language selection:', error); 653 showIconAnimation({ 654 container: this.uiManager.get('container'), 655 durationMs: AppConstants.UI.ANIMATION_DURATION_MS, 656 getIcon: url => Promise.resolve(url), 657 imageUrl: 'https://img.icons8.com/neon/256/sakura.png', 658 position: AppConstants.UI.ANIMATION_POSITIONS.MID, 659 }); 660 showTemporaryMessage('😽🍣'); 661 } 662 }); 663 } 664 if (chineseButton) { 665 chineseButton.addEventListener('click', async () => { 666 try { 667 const languageManager = await this._ensureLanguageManagerLoaded(); 668 if (languageManager) { 669 await languageManager.handleLanguageSelection('zh'); 670 } 671 } catch (error) { 672 console.error('Failed to handle Chinese language selection:', error); 673 showIconAnimation({ 674 container: this.uiManager.get('container'), 675 durationMs: AppConstants.UI.ANIMATION_DURATION_MS, 676 getIcon: url => Promise.resolve(url), 677 imageUrl: 'https://img.icons8.com/neon/256/lantern.png', 678 position: AppConstants.UI.ANIMATION_POSITIONS.MID, 679 }); 680 showTemporaryMessage('🍜🐼'); 681 } 682 }); 683 } 684 this._languageButtonListenersAttached = true; 685 } 686 687 _attachStatsButtonListener() { 688 if (!this.uiManager) { 689 console.error('UI Manager not available, cannot attach stats button listener'); 690 return; 691 } 692 const statsToggleButton = this.uiManager.get('statsToggle'); 693 if (!statsToggleButton) { 694 console.warn('Stats toggle button not found, cannot attach stats button listener'); 695 return; 696 } 697 if (this._statsButtonListenerAttached) { 698 return; 699 } 700 701 statsToggleButton.addEventListener('click', async () => { 702 try { 703 const statsUIManager = await this._ensureStatsUIManagerLoaded(); 704 if (!statsUIManager) { 705 return; 706 } 707 708 statsUIManager.toggleStatsVisibility(); 709 if (statsUIManager.statsVisible) { 710 statsUIManager.startUpdating(); 711 } else { 712 statsUIManager.stopUpdating(); 713 } 714 } catch (error) { 715 console.error('Failed to handle stats button click:', error); 716 } 717 }); 718 this._statsButtonListenerAttached = true; 719 } 720 721 _livestreamButtonListenerAttached = false; 722 723 async _attachLivestreamButtonListener() { 724 if (!this.uiManager) { 725 console.error('UI Manager not available, cannot attach livestream button listener'); 726 return; 727 } 728 729 const livestreamToggleButton = this.uiManager.get('livestreamToggle'); 730 if (!livestreamToggleButton) { 731 console.warn('Livestream toggle button not found, cannot attach livestream button listener'); 732 return; 733 } 734 735 if (this._livestreamButtonListenerAttached) { 736 return; 737 } 738 739 await this._setLivestreamButtonState(); 740 741 livestreamToggleButton.addEventListener('click', async () => { 742 try { 743 const playlistUIManager = await this._ensurePlaylistUIManagerLoaded(); 744 if (!playlistUIManager) { 745 console.error('PlaylistUIManager not available'); 746 return; 747 } 748 749 const { loadSourcesFromJson, isLivestreamMode } = await import( 750 '../services/video/sources.js' 751 ); 752 753 const currentlyInLivestreamMode = isLivestreamMode(); 754 755 const sourceFile = currentlyInLivestreamMode ? '/app/videos.json' : '/app/lives.json'; 756 757 showIconAnimation({ 758 container: this.uiManager.get('container'), 759 durationMs: AppConstants.UI.ANIMATION_DURATION_MS, 760 getIcon: url => Promise.resolve(url), 761 imageUrl: currentlyInLivestreamMode 762 ? 'https://img.icons8.com/neon/256/music-heart.png' 763 : 'https://img.icons8.com/neon/256/rfid-signal.png', 764 position: AppConstants.UI.ANIMATION_POSITIONS.MID, 765 }); 766 767 await this.closeMenu('left'); 768 769 try { 770 await loadSourcesFromJson(sourceFile); 771 772 await this.openMenu('right'); 773 774 await playlistUIManager.populatePlaylist(); 775 776 await new Promise(resolve => setTimeout(resolve, 50)); 777 await this._setLivestreamButtonState(); 778 779 showTemporaryMessage( 780 currentlyInLivestreamMode ? 'Music Videos Loaded' : 'Livestream Archives Loaded', 781 ); 782 } catch (error) { 783 console.error( 784 `Failed to load ${currentlyInLivestreamMode ? 'music videos' : 'livestream data'}:`, 785 error, 786 ); 787 showTemporaryMessage( 788 `Failed to load ${currentlyInLivestreamMode ? 'music videos' : 'livestream archives'}`, 789 ); 790 } 791 } catch (error) { 792 console.error('Failed to handle livestream button click:', error); 793 } 794 }); 795 796 this._livestreamButtonListenerAttached = true; 797 } 798 799 async _setLivestreamButtonState() { 800 try { 801 const { isLivestreamMode } = await import('../services/video/sources.js'); 802 const currentlyInLivestreamMode = isLivestreamMode(); 803 804 const livestreamToggleButton = this.uiManager.get('livestreamToggle'); 805 if (!livestreamToggleButton) { 806 return; 807 } 808 809 const buttonText = livestreamToggleButton.querySelector('.stats-text'); 810 const leftIcon = livestreamToggleButton.querySelector('.stats-icon-left'); 811 const rightIcon = livestreamToggleButton.querySelector('.stats-icon-right'); 812 813 if (currentlyInLivestreamMode) { 814 buttonText.textContent = 'Music Videos'; 815 leftIcon.setAttribute('alt', 'music-heart'); 816 leftIcon.src = 'https://img.icons8.com/neon/96/music-heart.png'; 817 818 rightIcon.setAttribute('alt', 'retro-tv'); 819 rightIcon.src = 'https://img.icons8.com/neon/96/retro-tv.png'; 820 } else { 821 buttonText.textContent = 'Livestream Archives'; 822 leftIcon.setAttribute('alt', 'rfid-signal'); 823 leftIcon.src = 'https://img.icons8.com/neon/96/rfid-signal.png'; 824 825 rightIcon.setAttribute('alt', 'retro-tv'); 826 rightIcon.src = 'https://img.icons8.com/neon/96/retro-tv.png'; 827 } 828 829 leftIcon.onerror = () => console.error(`Failed to load left icon: ${leftIcon.src}`); 830 rightIcon.onerror = () => console.error(`Failed to load right icon: ${rightIcon.src}`); 831 832 return currentlyInLivestreamMode; 833 } catch (error) { 834 console.error('Failed to set livestream button state:', error); 835 return false; 836 } 837 } 838 839 cleanup() { 840 for (const unsubscribe of this._eventSubscriptions) { 841 if (typeof unsubscribe === 'function') { 842 unsubscribe(); 843 } 844 } 845 this._eventSubscriptions = []; 846 847 this.subscriptions.unsubscribeAll(); 848 849 this.events.clearAllEvents(); 850 851 this._pendingMenuOperations = []; 852 this._menuOperationInProgress = false; 853 } 854 }