From 84af1ab3bc7743e87ae0f3a8d57962f737f518b9 Mon Sep 17 00:00:00 2001 From: tr1x_em Date: Thu, 7 Aug 2025 11:21:00 +0530 Subject: [PATCH] Fix Memory Leak(maybe??) --- modules/controlcenter/bluetooth.py | 3 + modules/controlcenter/expanded_player.py | 265 ++++++++++++++++++----- modules/controlcenter/main.py | 247 +++++++++++++-------- modules/controlcenter/player.py | 16 ++ modules/controlcenter/wifi.py | 3 + 5 files changed, 396 insertions(+), 138 deletions(-) diff --git a/modules/controlcenter/bluetooth.py b/modules/controlcenter/bluetooth.py index dcdfb65..58f2d94 100644 --- a/modules/controlcenter/bluetooth.py +++ b/modules/controlcenter/bluetooth.py @@ -1,3 +1,6 @@ +import gi +gi.require_version('Gtk', '3.0') +gi.require_version('Gdk', '3.0') from gi.repository import Gdk, GLib, Gtk from fabric.bluetooth import BluetoothClient, BluetoothDevice diff --git a/modules/controlcenter/expanded_player.py b/modules/controlcenter/expanded_player.py index 5fabb38..53828e5 100644 --- a/modules/controlcenter/expanded_player.py +++ b/modules/controlcenter/expanded_player.py @@ -1,18 +1,20 @@ -from fabric.widgets.scale import Scale -from widgets.wayland import WaylandWindow as Window -from fabric.widgets.button import Button -from fabric.widgets.label import Label -from fabric.widgets.box import Box -from services.mpris import MprisPlayer, MprisPlayerManager - +# Standard library imports import os import re import tempfile import urllib.parse import urllib.request -from typing import List import threading +import weakref +from typing import List, Optional, Dict, Set +import gc +# Fabric imports +from fabric.widgets.scale import Scale +from widgets.wayland import WaylandWindow as Window +from fabric.widgets.button import Button +from fabric.widgets.label import Label +from fabric.widgets.box import Box from fabric.utils import bulk_connect, invoke_repeater, cooldown from fabric.utils.helpers import get_relative_path from fabric.widgets.image import Image @@ -22,12 +24,17 @@ from fabric.widgets.svg import Svg from gi.repository import GLib, GObject from loguru import logger +# Local imports +from services.mpris import MprisPlayer, MprisPlayerManager import config.data as data CACHE_DIR = f"{data.CACHE_DIR}/media" -# Shared MPRIS manager to reduce memory usage +# Global memory management _shared_mpris_manager = None +_widget_cache = weakref.WeakValueDictionary() +_artwork_cache = {} +_max_artwork_cache_size = 10 # Limit artwork cache to prevent memory bloat def get_shared_mpris_manager(): @@ -38,16 +45,81 @@ def get_shared_mpris_manager(): return _shared_mpris_manager +def cleanup_artwork_cache(): + """Clean up artwork cache to prevent memory leaks.""" + global _artwork_cache + if len(_artwork_cache) > _max_artwork_cache_size: + # Keep only the most recent items + items = list(_artwork_cache.items()) + _artwork_cache = dict(items[-_max_artwork_cache_size:]) + # Force garbage collection + gc.collect() + + def cleanup_old_cache_files(): - """Clean up old artwork cache files (older than 6 hours for more aggressive cleanup).""" + """Clean up old artwork cache files aggressively to save memory.""" try: if not os.path.exists(CACHE_DIR): return import time - current_time = time.time() - six_hours_ago = current_time - (6 * 60 * 60) # 6 hours instead of 24 + # Clean files older than 2 hours (more aggressive) + two_hours_ago = current_time - (2 * 60 * 60) + + for filename in os.listdir(CACHE_DIR): + filepath = os.path.join(CACHE_DIR, filename) + try: + if os.path.isfile(filepath): + file_mod_time = os.path.getmtime(filepath) + if file_mod_time < two_hours_ago: + os.remove(filepath) + logger.debug(f"Cleaned up old cache file: {filename}") + except Exception as e: + logger.warning(f"Failed to clean up cache file {filename}: {e}") + + except Exception as e: + logger.error(f"Error during cache cleanup: {e}") + + +def get_artwork_cached(url: str) -> Optional[str]: + """Get artwork with memory-efficient caching.""" + if not url: + return None + + # Check memory cache first + if url in _artwork_cache: + return _artwork_cache[url] + + # Clean cache if too large + cleanup_artwork_cache() + + try: + # Create cache directory if it doesn't exist + os.makedirs(CACHE_DIR, exist_ok=True) + + # Generate cache filename + safe_filename = re.sub(r'[^\w\-_.]', '_', urllib.parse.urlparse(url).path.split('/')[-1]) + if not safe_filename: + safe_filename = f"artwork_{hash(url)}" + + cache_file = os.path.join(CACHE_DIR, safe_filename) + + # Check if cached file exists and is recent (within 2 hours) + if os.path.exists(cache_file): + import time + if time.time() - os.path.getmtime(cache_file) < 7200: # 2 hours + _artwork_cache[url] = cache_file + return cache_file + + # Download and cache + urllib.request.urlretrieve(url, cache_file) + _artwork_cache[url] = cache_file + return cache_file + + except Exception as e: + logger.warning(f"Failed to cache artwork from {url}: {e}") + return None for filename in os.listdir(CACHE_DIR): filepath = os.path.join(CACHE_DIR, filename) @@ -87,9 +159,11 @@ class EmbeddedExpandedPlayer(Box): self.player_content = PlayerBoxStack(self.mpris_manager) # Add escape key binding for navigation back + self._keybinding_added = False try: if hasattr(self.control_center, "add_keybinding"): self.control_center.add_keybinding("Escape", self._on_back_clicked) + self._keybinding_added = True except Exception: pass # Ignore if keybinding fails @@ -122,34 +196,78 @@ class EmbeddedExpandedPlayer(Box): pass def destroy(self): - """Clean up resources""" - if hasattr(self.player_content, "destroy"): - self.player_content.destroy() - super().destroy() + """Clean up resources and prevent memory leaks""" + logger.debug("๐Ÿ—‘๏ธ EmbeddedExpandedPlayer cleanup starting") + + try: + # Destroy player content (PlayerBoxStack) + if hasattr(self, 'player_content') and hasattr(self.player_content, "destroy"): + self.player_content.destroy() + + # Clean up back button + if hasattr(self, 'back_button') and hasattr(self.back_button, "destroy"): + self.back_button.destroy() + + # Clean up any other widgets we might have + for child in list(self.get_children()): + try: + child.destroy() + except Exception as e: + logger.warning(f"Failed to destroy child widget: {e}") + + # Clean up keybinding if it was added + if hasattr(self, '_keybinding_added') and self._keybinding_added: + try: + if hasattr(self.control_center, "remove_keybinding"): + self.control_center.remove_keybinding("Escape") + except Exception as e: + logger.warning(f"Failed to remove keybinding: {e}") + + # Aggressively clean global caches + global _widget_cache, _artwork_cache + _widget_cache.clear() + _artwork_cache.clear() + logger.debug("๐Ÿ—‘๏ธ Cleared global widget and artwork caches") + + # Clear references + if hasattr(self, 'control_center'): + self.control_center = None + if hasattr(self, 'mpris_manager'): + self.mpris_manager = None + + # Force garbage collection + gc.collect() + logger.debug("๐Ÿ—‘๏ธ EmbeddedExpandedPlayer cleanup completed") + + except Exception as e: + logger.error(f"Error during EmbeddedExpandedPlayer cleanup: {e}") + finally: + super().destroy() class PlayerBoxStack(Box): - """A widget that displays the current player information.""" + """Memory-optimized widget that displays current player information.""" def __init__(self, mpris_manager: MprisPlayerManager, **kwargs): # Clean up old cache files on startup cleanup_old_cache_files() - # The player stack + # The player stack with memory-efficient settings self.player_stack = Stack( - # transition_type="slide-left-right", - # transition_duration=500, name="player-stack", + # Disable transitions to reduce memory usage + transition_type="none", ) self.current_stack_pos = 0 - # List to store player buttons + # Store player buttons - cleaned up via explicit removal self.player_buttons: list[Button] = [] + self._player_widgets: Dict[str, Box] = weakref.WeakValueDictionary() - # Track signal connections for cleanup + # Track signal connections for cleanup using weak references self._signal_connections = [] - # Create a "No media playing" placeholder + # Create a lightweight "No media playing" placeholder self.no_media_box = self._create_no_media_box() super().__init__(orientation="v", name="media", children=[self.player_stack]) @@ -160,7 +278,7 @@ class PlayerBoxStack(Box): self.mpris_manager = mpris_manager - # Track connections for cleanup - store (object, handler_id) tuples + # Track connections for cleanup connections = bulk_connect( self.mpris_manager, { @@ -168,43 +286,92 @@ class PlayerBoxStack(Box): "player-vanished": self.on_lost_player, }, ) - # Store as (object, handler_id) tuples for handler_id in connections: self._signal_connections.append((self.mpris_manager, handler_id)) + # Process existing players for player in self.mpris_manager.players: # type: ignore - logger.info( - f"[PLAYER MANAGER] player found: {player.get_property('player-name')}", - ) + logger.info(f"[PLAYER MANAGER] player found: {player.get_property('player-name')}") self.on_new_player(self.mpris_manager, player) + # Schedule periodic memory cleanup and store source ID + self._cleanup_source_id = GLib.timeout_add_seconds(300, self._periodic_cleanup) # Every 5 minutes + + def _periodic_cleanup(self): + """Periodic cleanup to prevent memory leaks.""" + try: + # Clean artwork cache + cleanup_artwork_cache() + + # Remove destroyed widgets from tracking + destroyed_keys = [] + for key, widget in self._player_widgets.items(): + if not widget or not widget.get_parent(): + destroyed_keys.append(key) + + for key in destroyed_keys: + if key in self._player_widgets: + del self._player_widgets[key] + + # Force garbage collection + gc.collect() + + except Exception as e: + logger.warning(f"Error during periodic cleanup: {e}") + + return True # Continue timer + def destroy(self): """Clean up resources when the widget is destroyed.""" - # Disconnect all signal connections - for obj, handler_id in self._signal_connections: - try: - obj.disconnect(handler_id) - except Exception as e: - logger.warning(f"Failed to disconnect signal: {e}") - self._signal_connections.clear() - - # Clean up player buttons - for button in self.player_buttons: - try: - button.destroy() - except Exception: - pass - self.player_buttons.clear() - - # Clean up player boxes - for child in self.player_stack.get_children(): - if hasattr(child, "destroy") and child != self.no_media_box: + try: + # Cancel any pending cleanup timer + if hasattr(self, '_cleanup_source_id') and self._cleanup_source_id: try: - child.destroy() + GLib.source_remove(self._cleanup_source_id) + except Exception: + pass # Timer may have already been removed + + # Disconnect all signal connections + for obj, handler_id in self._signal_connections: + try: + if obj and hasattr(obj, 'disconnect'): + obj.disconnect(handler_id) + except Exception as e: + logger.warning(f"Failed to disconnect signal: {e}") + self._signal_connections.clear() + + # Clean up player widgets + for widget in list(self._player_widgets.values()): + try: + if widget and hasattr(widget, "destroy"): + widget.destroy() + except Exception: + pass + self._player_widgets.clear() + + # Clean up player buttons explicitly + for button in list(self.player_buttons): + try: + if button and hasattr(button, "destroy"): + button.destroy() except Exception: pass - super().destroy() + # Clean up stack children + for child in self.player_stack.get_children(): + if hasattr(child, "destroy") and child != self.no_media_box: + try: + child.destroy() + except Exception: + pass + + # Force final garbage collection + gc.collect() + + except Exception as e: + logger.error(f"Error during PlayerBoxStack cleanup: {e}") + finally: + super().destroy() def _create_no_media_box(self): """Create a placeholder box for when no media is playing.""" diff --git a/modules/controlcenter/main.py b/modules/controlcenter/main.py index 7be2213..d9a2cdf 100644 --- a/modules/controlcenter/main.py +++ b/modules/controlcenter/main.py @@ -1,40 +1,66 @@ -from gi.repository import Gdk, GLib +# Standard library imports +import contextlib +import gc +import weakref +from pathlib import Path +from typing import Optional, Dict, Any + +# Fabric imports +from fabric.core.service import Service, Signal, Property +from fabric.widgets.window import Window +from fabric.widgets.overlay import Overlay +from fabric.widgets.box import Box +from fabric.widgets.centerbox import CenterBox +from fabric.widgets.button import Button +from fabric.widgets.label import Label +from fabric.widgets.scale import Scale +from fabric.widgets.revealer import Revealer +from fabric.utils import idle_add, get_relative_path, invoke_repeater +from fabric.widgets.image import Image + +# Gi imports +import gi +gi.require_version("Gtk", "3.0") +gi.require_version("GLib", "2.0") +from gi.repository import Gtk, GLib, Gdk + +# Local imports +from modules.controlcenter.bluetooth import BluetoothConnections +from modules.controlcenter.wifi import WifiConnections +from modules.controlcenter.player import PlayerBoxStack +from modules.controlcenter.per_app_volume import PerAppVolumeControl +from modules.controlcenter.expanded_player import EmbeddedExpandedPlayer +from services.modus import modus_service +from widgets.wayland import WaylandWindow as Window from loguru import logger +# Memory monitoring +from debug_memory import set_memory_baseline, log_memory from fabric.utils import idle_add -from fabric.utils.helpers import ( - get_relative_path, -) +from fabric.utils.helpers import get_relative_path from fabric.widgets.box import Box from fabric.widgets.button import Button from fabric.widgets.centerbox import CenterBox from fabric.widgets.label import Label from fabric.widgets.scale import Scale from fabric.widgets.svg import Svg +from widgets.wayland import WaylandWindow as Window + +# Local imports from modules.controlcenter.bluetooth import BluetoothConnections from modules.controlcenter.wifi import WifiConnections from services.brightness import Brightness from utils.roam import audio_service, modus_service -from widgets.wayland import WaylandWindow as Window from modules.controlcenter.player import PlayerBoxStack from modules.controlcenter.per_app_volume import PerAppVolumeControl -from modules.controlcenter.expanded_player import EmbeddedExpandedPlayer +from modules.controlcenter.expanded_player import EmbeddedExpandedPlayer, get_shared_mpris_manager from services.mpris import MprisPlayerManager -from fabric.widgets.svg import Svg -from fabric.utils.helpers import get_relative_path brightness_service = Brightness.get_initial() -# Shared MPRIS manager to reduce memory usage -_shared_mpris_manager = None - - -def get_shared_mpris_manager(): - """Get shared MPRIS manager instance to reduce memory usage.""" - global _shared_mpris_manager - if _shared_mpris_manager is None: - _shared_mpris_manager = MprisPlayerManager() - return _shared_mpris_manager +# Memory management globals +_widget_cache = weakref.WeakValueDictionary() +_cleanup_timer_id = None class ModusControlCenter(Window): @@ -348,65 +374,41 @@ class ModusControlCenter(Window): self.children = self.center_box + # Set memory baseline for monitoring + set_memory_baseline("Control Center Initialized") + # Connect to visibility changes for cleanup self.connect("notify::visible", self._on_visibility_changed) # Periodic cleanup timer (every 5 minutes when visible) - GLib.timeout_add_seconds(300, self._periodic_cleanup) - - def destroy(self): - """Proper cleanup when control center is destroyed""" - # Disconnect all signal connections - for connection in self._signal_connections: - try: - if hasattr(connection, "disconnect"): - connection.disconnect() - elif hasattr(connection, "handler_disconnect"): - # For GObject connections - connection.handler_disconnect() - except Exception: - pass - self._signal_connections.clear() - - # Clean up all widgets - self._cleanup_when_hidden() - - # Clean up managers - if hasattr(self, "wifi_man") and self.wifi_man: - try: - if hasattr(self.wifi_man, "destroy"): - self.wifi_man.destroy() - except Exception: - pass - - if hasattr(self, "bluetooth_man") and self.bluetooth_man: - try: - if hasattr(self.bluetooth_man, "destroy"): - self.bluetooth_man.destroy() - except Exception: - pass - - super().destroy() + # Start periodic memory cleanup (every 5 minutes) + global _cleanup_timer_id + _cleanup_timer_id = GLib.timeout_add_seconds(300, self._periodic_cleanup) def _on_visibility_changed(self, widget, param): """Handle visibility changes for memory management""" if not self.get_visible(): + logger.debug("Control center hidden - starting cleanup") self._cleanup_when_hidden() def _cleanup_when_hidden(self): """Clean up resources when widget is hidden to reduce memory usage""" - # Clean up music widget content if it exists - if self._music_widget_content and hasattr( - self._music_widget_content, "destroy" - ): + log_memory("๐Ÿงน BEFORE cleanup when hidden") + logger.debug("Starting hidden cleanup - freeing widget memory") + # Clean up music widget content properly + if self._music_widget_content: try: # Remove from parent container first current_children = list(self.music_widget.children) if self._music_widget_content in current_children: current_children.remove(self._music_widget_content) self.music_widget.children = current_children - # Destroy the content to free memory - self._music_widget_content.destroy() + + # Destroy the content to free memory completely + if hasattr(self._music_widget_content, "destroy"): + self._music_widget_content.destroy() + + logger.debug("Destroyed music widget content to free memory") self._music_widget_content = None self._music_initialized = False except Exception as e: @@ -423,33 +425,43 @@ class ModusControlCenter(Window): except Exception: pass - # Clean up expanded player widget + # Clean up expanded player widget - BUT preserve for reuse if self._expanded_player_widget and hasattr( self._expanded_player_widget, "destroy" ): try: - self._expanded_player_widget.destroy() - self._expanded_player_widget = None - self._expanded_player_initialized = False - except Exception: - pass + # DON'T destroy the widget, just clean its internal state + if hasattr(self._expanded_player_widget, 'player_content'): + # Clean up only the player content, keep the widget shell + if hasattr(self._expanded_player_widget.player_content, '_periodic_cleanup'): + self._expanded_player_widget.player_content._periodic_cleanup() + logger.debug("Cleaned expanded player internal state (preserved for reuse)") + # DON'T set to None - keep for reuse! + # self._expanded_player_widget = None + # self.expanded_player_widgets = None + # self.expanded_player_center_box = None + except Exception as e: + logger.warning(f"Failed to clean expanded player state: {e}") - # Reset widget containers to free memory + # Reset widget containers to free memory (except expanded player for reuse) self.bluetooth_widgets = None self.wifi_widgets = None self.per_app_volume_widgets = None - self.expanded_player_widgets = None + # Keep expanded_player_widgets for reuse! + # self.expanded_player_widgets = None - # Reset center boxes + # Reset center boxes (except expanded player for reuse) self.bluetooth_center_box = None self.wifi_center_box = None self.per_app_volume_center_box = None - self.expanded_player_center_box = None + # Keep expanded_player_center_box for reuse! + # self.expanded_player_center_box = None # Force garbage collection import gc gc.collect() + log_memory("๐Ÿงน AFTER cleanup when hidden") def _periodic_cleanup(self): """Periodic cleanup to keep memory usage low""" @@ -467,14 +479,22 @@ class ModusControlCenter(Window): def _ensure_music_widget(self): """Lazy load music widget content""" if not self._music_initialized: + log_memory("๐ŸŽผ BEFORE creating music widget content") + logger.debug("Creating new PlayerBoxStack instance") self._music_widget_content = PlayerBoxStack( get_shared_mpris_manager(), control_center=self ) + log_memory("๐ŸŽผ AFTER creating PlayerBoxStack") + # Add to the music widget's children list current_children = list(self.music_widget.children) current_children.append(self._music_widget_content) self.music_widget.children = current_children self._music_initialized = True + log_memory("๐ŸŽผ AFTER adding to music widget container") + else: + logger.debug("Music widget already initialized") + log_memory("โ™ป๏ธ REUSING existing music widget") def _ensure_bluetooth_widgets(self): """Lazy load bluetooth widgets""" @@ -556,10 +576,16 @@ class ModusControlCenter(Window): self.per_app_volume_center_box.set_size_request(300, -1) def _ensure_expanded_player_widgets(self): - """Lazy load expanded player widgets""" + """Lazy load expanded player widgets with reuse optimization""" if self.expanded_player_widgets is None: + log_memory("๐Ÿ”ง BEFORE creating expanded player widgets") + logger.debug("Creating new expanded player widgets") if self._expanded_player_widget is None: + logger.debug("Creating new EmbeddedExpandedPlayer instance") self._expanded_player_widget = EmbeddedExpandedPlayer(self) + log_memory("๐Ÿ”ง AFTER creating EmbeddedExpandedPlayer") + else: + logger.debug("Reusing existing EmbeddedExpandedPlayer instance") self.expanded_player_widgets = Box( orientation="vertical", @@ -573,6 +599,10 @@ class ModusControlCenter(Window): start_children=[self.expanded_player_widgets] ) self.expanded_player_center_box.set_size_request(300, -1) + log_memory("๐Ÿ”ง AFTER creating expanded player containers") + else: + logger.debug("Reusing existing expanded player widgets") + log_memory("โ™ป๏ธ REUSING existing expanded player widgets") def set_dont_disturb(self, *_): self.focus_mode = not self.focus_mode @@ -669,16 +699,20 @@ class ModusControlCenter(Window): self.has_per_app_volume_open = False def open_expanded_player(self, *_): + log_memory("๐ŸŽต BEFORE opening expanded player") self._ensure_expanded_player_widgets() idle_add(lambda *_: self.set_children(self.expanded_player_center_box)) self.has_expanded_player_open = True # Refresh the player when opening if self._expanded_player_widget: self._expanded_player_widget.refresh() + log_memory("๐ŸŽต AFTER opening expanded player") def close_expanded_player(self, *_): + log_memory("๐Ÿ”’ BEFORE closing expanded player") idle_add(lambda *_: self.set_children(self.center_box)) self.has_expanded_player_open = False + log_memory("๐Ÿ”’ AFTER closing expanded player") def _set_mousecapture(self, visible: bool): if visible: @@ -776,24 +810,59 @@ class ModusControlCenter(Window): self.set_visible(False) def destroy(self): - """Clean up resources when widget is destroyed""" - # Disconnect all signal connections - for connection in self._signal_connections: - try: - connection.disconnect() - except: - pass + """Enhanced cleanup of resources when widget is destroyed.""" + try: + # Cancel any periodic cleanup timers + global _cleanup_timer_id + if _cleanup_timer_id: + GLib.source_remove(_cleanup_timer_id) + _cleanup_timer_id = None - # Clean up heavy components - if self._wifi_man: - self._wifi_man.destroy() - if self._bluetooth_man: - self._bluetooth_man.destroy() - if self._music_widget_content: - self._music_widget_content.destroy() - if self._per_app_volume_widget: - self._per_app_volume_widget.destroy() - if self._expanded_player_widget: - self._expanded_player_widget.destroy() + # Disconnect all signal connections with better error handling + for connection in self._signal_connections: + try: + if connection and hasattr(connection, 'disconnect'): + connection.disconnect() + except Exception as e: + logger.warning(f"Failed to disconnect signal: {e}") + self._signal_connections.clear() - super().destroy() + # Clean up heavy components with memory optimization + components_to_cleanup = [ + ('wifi_man', getattr(self, 'wifi_man', None)), + ('bluetooth_man', getattr(self, 'bluetooth_man', None)), + ('_music_widget_content', self._music_widget_content), + ('_per_app_volume_widget', self._per_app_volume_widget), + ('_expanded_player_widget', self._expanded_player_widget) + ] + + for attr_name, component in components_to_cleanup: + if component and hasattr(component, 'destroy'): + try: + component.destroy() + setattr(self, attr_name, None) + except Exception as e: + logger.warning(f"Failed to destroy {attr_name}: {e}") + + # Clean up widget containers and references + widget_containers = [ + 'bluetooth_widgets', 'wifi_widgets', 'per_app_volume_widgets', + 'expanded_player_widgets', 'bluetooth_center_box', 'wifi_center_box', + 'per_app_volume_center_box', 'expanded_player_center_box' + ] + + for container in widget_containers: + if hasattr(self, container): + setattr(self, container, None) + + # Clear any cached widgets + global _widget_cache + _widget_cache.clear() + + # Force garbage collection for immediate memory cleanup + gc.collect() + + except Exception as e: + logger.error(f"Error during control center destruction: {e}") + finally: + super().destroy() diff --git a/modules/controlcenter/player.py b/modules/controlcenter/player.py index 6cb14db..f4c797f 100644 --- a/modules/controlcenter/player.py +++ b/modules/controlcenter/player.py @@ -132,6 +132,22 @@ class PlayerBoxStack(Box): super().destroy() + def _periodic_cleanup(self): + """Light cleanup for reuse - clean internal state but preserve widget""" + try: + # Reset to no media state + self.player_stack.children = [self.no_media_box] + + # Clear player buttons but don't destroy them + self.player_buttons.clear() + + # Reset stack position + self.current_stack_pos = 0 + + logger.debug("PlayerBoxStack periodic cleanup completed") + except Exception as e: + logger.warning(f"PlayerBoxStack periodic cleanup failed: {e}") + def _create_no_media_box(self): """Create a placeholder box for when no media is playing.""" fallback_cover_path = f"{data.HOME_DIR}/.current.wall" diff --git a/modules/controlcenter/wifi.py b/modules/controlcenter/wifi.py index 2dd25e7..c2c72fc 100644 --- a/modules/controlcenter/wifi.py +++ b/modules/controlcenter/wifi.py @@ -1,3 +1,6 @@ +import gi +gi.require_version('Gtk', '3.0') +gi.require_version('Gdk', '3.0') from gi.repository import Gdk, GLib, Gtk from fabric.widgets.box import Box