/ app / ui / menu-manager.js
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  }