diff --git a/modules/controlcenter/expanded_player.py b/modules/controlcenter/expanded_player.py index 0bd558c..4615df7 100644 --- a/modules/controlcenter/expanded_player.py +++ b/modules/controlcenter/expanded_player.py @@ -12,6 +12,7 @@ import urllib.parse import urllib.request from typing import List import threading +import weakref # For memory-efficient references from fabric.utils import bulk_connect, invoke_repeater, cooldown from fabric.widgets.image import Image @@ -24,9 +25,19 @@ import config.data as data CACHE_DIR = f"{data.CACHE_DIR}/media" +# 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 + def cleanup_old_cache_files(): - """Clean up old artwork cache files (older than 1 day).""" + """Clean up old artwork cache files (older than 6 hours for more aggressive cleanup).""" try: if not os.path.exists(CACHE_DIR): return @@ -34,14 +45,14 @@ def cleanup_old_cache_files(): import time current_time = time.time() - one_day_ago = current_time - (24 * 60 * 60) # 24 hours + six_hours_ago = current_time - (6 * 60 * 60) # 6 hours instead of 24 for filename in os.listdir(CACHE_DIR): filepath = os.path.join(CACHE_DIR, filename) try: if os.path.isfile(filepath): file_mtime = os.path.getmtime(filepath) - if file_mtime < one_day_ago: + if file_mtime < six_hours_ago: os.unlink(filepath) except Exception: pass # Ignore individual file errors @@ -61,7 +72,7 @@ class EmbeddedExpandedPlayer(Box): ) self.control_center = control_center - self.mpris_manager = MprisPlayerManager() + self.mpris_manager = get_shared_mpris_manager() # Create back button self.back_button = Button( @@ -197,10 +208,10 @@ class PlayerBoxStack(Box): from gi.repository import GdkPixbuf pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale( - fallback_cover_path, 80, 80, True + fallback_cover_path, 60, 60, True ) album_cover_image.set_from_pixbuf(pixbuf) - album_cover_image.set_size_request(80, 80) + album_cover_image.set_size_request(60, 60) except Exception as e: logger.warning(f"Failed to load fallback image in no-media box: {e}") @@ -208,7 +219,7 @@ class PlayerBoxStack(Box): name="macos-album-image", children=[album_cover_image], ) - album_cover.set_size_request(80, 80) + album_cover.set_size_request(60, 60) image_stack = Box(h_align="start", v_align="center", name="player-image-stack") image_stack.children = [album_cover] @@ -218,7 +229,7 @@ class PlayerBoxStack(Box): label="No media playing", name="player-title", justification="left", - max_chars_width=15, + max_chars_width=12, ellipsization="end", h_align="start", ) @@ -227,7 +238,7 @@ class PlayerBoxStack(Box): label="", name="player-artist", justification="left", - max_chars_width=15, + max_chars_width=12, ellipsization="end", h_align="start", visible=False, # Hide artist and album when no media @@ -237,7 +248,7 @@ class PlayerBoxStack(Box): label="", name="player-album", justification="left", - max_chars_width=15, + max_chars_width=12, ellipsization="end", h_align="start", visible=False, # Hide artist and album when no media @@ -483,7 +494,7 @@ class PlayerBox(Box): self.player_stack = player_stack self.fallback_cover_path = f"{data.HOME_DIR}/.current.wall" - self.image_size = 80 # Large album art for macOS style + self.image_size = 60 # Smaller album art for compact layout self.icon_size = 15 # State @@ -512,7 +523,7 @@ class PlayerBox(Box): label="No Title", name="macos-player-title", justification="left", - max_chars_width=30, + max_chars_width=25, ellipsization="end", h_align="start", h_expand=True, @@ -522,7 +533,7 @@ class PlayerBox(Box): label="No Artist", name="macos-player-artist", justification="left", - max_chars_width=30, + max_chars_width=25, ellipsization="end", h_align="start", h_expand=True, @@ -540,10 +551,10 @@ class PlayerBox(Box): from gi.repository import GdkPixbuf pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale( - self.fallback_cover_path, 80, 80, True + self.fallback_cover_path, 60, 60, True ) self.album_cover_image.set_from_pixbuf(pixbuf) - self.album_cover_image.set_size_request(80, 80) + self.album_cover_image.set_size_request(60, 60) except Exception as e: logger.warning(f"Failed to load fallback image: {e}") @@ -552,7 +563,7 @@ class PlayerBox(Box): name="macos-album-image", children=[self.album_cover_image], ) - self.album_cover.set_size_request(80, 80) + self.album_cover.set_size_request(60, 60) self.image_stack = Box( h_align="start", @@ -621,7 +632,7 @@ class PlayerBox(Box): name="macos-button-box", h_align="center", h_expand=True, - spacing=15, # Wider spacing for macOS look + spacing=10, # Reduced spacing for macOS look ) # Define controls_box BEFORE using it in track_info @@ -629,7 +640,7 @@ class PlayerBox(Box): name="macos-player-controls", orientation="v", h_expand=True, - spacing=8, + spacing=6, h_align="center", children=[self.button_box], ) @@ -637,7 +648,7 @@ class PlayerBox(Box): # Track info with inline controls - expands to fill available space self.track_info = Box( name="macos-track-info", - spacing=6, # Reduced spacing + spacing=4, # Reduced spacing orientation="v", v_align="start", h_align="fill", # Fill all available horizontal space @@ -718,7 +729,7 @@ class PlayerBox(Box): self.outer_box = Box( name="macos-outer-player-box", orientation="horizontal", - spacing=15, # Good spacing between elements + spacing=10, # Reduced spacing between elements h_expand=True, v_expand=True, v_align="center", @@ -746,8 +757,15 @@ class PlayerBox(Box): def destroy(self): """Clean up all resources when the widget is destroyed.""" - # Cancel any ongoing downloads + # Cancel any ongoing downloads immediately self._download_cancelled = True + + # Wait for download thread to finish (with timeout) + if self.current_download_thread and self.current_download_thread.is_alive(): + try: + self.current_download_thread.join(timeout=1.0) # 1 second timeout + except Exception: + pass # Disconnect all signal connections for connection in self._signal_connections: @@ -757,8 +775,15 @@ class PlayerBox(Box): logger.warning(f"Failed to disconnect signal: {e}") self._signal_connections.clear() - # Clean up temp files + # Clean up temp files aggressively self._cleanup_temp_files() + + # Clear image references + if hasattr(self, 'album_cover_image'): + try: + self.album_cover_image.set_from_pixbuf(None) + except Exception: + pass super().destroy() @@ -892,21 +917,25 @@ class PlayerBox(Box): if image_path and os.path.isfile(image_path): # Create scaled pixbuf to ensure proper size pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale( - image_path, 80, 80, True + image_path, 60, 60, True ) self.album_cover_image.set_from_pixbuf(pixbuf) + # Explicitly clear pixbuf reference to help GC + del pixbuf else: # Use fallback image with scaling pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale( - self.fallback_cover_path, 80, 80, True + self.fallback_cover_path, 60, 60, True ) self.album_cover_image.set_from_pixbuf(pixbuf) + # Explicitly clear pixbuf reference to help GC + del pixbuf except Exception as e: logger.warning(f"Failed to update album cover image: {e}") # Try simple file setting as last resort try: self.album_cover_image.set_from_file(self.fallback_cover_path) - self.album_cover_image.set_size_request(80, 80) + self.album_cover_image.set_size_request(60, 60) except Exception as e2: logger.error(f"Failed to set fallback image: {e2}") @@ -954,16 +983,16 @@ class PlayerBox(Box): if self._download_cancelled: return - # Clean up old temp files first (keep only last 2 to avoid excessive cleanup) - if len(self.temp_artwork_files) > 2: - old_files = self.temp_artwork_files[:-2] + # Clean up old temp files first (keep only last 1 to reduce memory) + if len(self.temp_artwork_files) > 1: + old_files = self.temp_artwork_files[:-1] for old_file in old_files: try: if os.path.exists(old_file): os.unlink(old_file) except Exception: pass - self.temp_artwork_files = self.temp_artwork_files[-2:] + self.temp_artwork_files = self.temp_artwork_files[-1:] # Check again if cancelled if self._download_cancelled: @@ -1066,7 +1095,7 @@ class Thing(Box): def __init__(self, **kwargs): super().__init__( name="thing", - size=(600, 200), + size=(480, 160), orientation="vertical", spacing=0, children=[ @@ -1088,7 +1117,7 @@ class ExpandedPlayer(Window): anchor="top right", layer="top", exclusivity="auto", - child=PlayerBoxStack(MprisPlayerManager()), + child=PlayerBoxStack(get_shared_mpris_manager()), visible=False, ) self.add_keybinding("Escape", self.set_child_visible(False)) diff --git a/modules/controlcenter/main.py b/modules/controlcenter/main.py index 669b39f..9bd5bf2 100644 --- a/modules/controlcenter/main.py +++ b/modules/controlcenter/main.py @@ -1,4 +1,5 @@ from gi.repository import Gdk, GLib +from loguru import logger from fabric.utils import idle_add from fabric.utils.helpers import ( @@ -22,6 +23,16 @@ from services.mpris import MprisPlayerManager 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 + # FIX: Icon not showing up in control center # TODO: Add Player @@ -325,6 +336,43 @@ class ModusControlCenter(Window): # 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() def _on_visibility_changed(self, widget, param): """Handle visibility changes for memory management""" @@ -332,23 +380,72 @@ class ModusControlCenter(Window): self._cleanup_when_hidden() def _cleanup_when_hidden(self): - """Clean up resources when widget is hidden""" + """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 not self._music_initialized: - # Remove from the parent container - 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 - self._music_widget_content = None + if self._music_widget_content and hasattr(self._music_widget_content, 'destroy'): + 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() + self._music_widget_content = None + self._music_initialized = False + except Exception as e: + logger.warning(f"Failed to cleanup music widget content: {e}") - # Reset lazy loading flags to allow re-initialization when shown again - # Keep initialized flags to avoid recreating heavy components unnecessarily + # Clean up per-app volume widget + if self._per_app_volume_widget and hasattr(self._per_app_volume_widget, 'destroy'): + try: + self._per_app_volume_widget.destroy() + self._per_app_volume_widget = None + self._per_app_volume_initialized = False + except Exception: + pass + + # Clean up expanded player widget + 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 + + # Reset widget containers to free memory + self.bluetooth_widgets = None + self.wifi_widgets = None + self.per_app_volume_widgets = None + self.expanded_player_widgets = None + + # Reset center boxes + self.bluetooth_center_box = None + self.wifi_center_box = None + self.per_app_volume_center_box = None + self.expanded_player_center_box = None + + # Force garbage collection + import gc + gc.collect() + + def _periodic_cleanup(self): + """Periodic cleanup to keep memory usage low""" + if not self.get_visible(): + # If not visible, do aggressive cleanup + self._cleanup_when_hidden() + return True # Continue timer + + # Light cleanup when visible + import gc + gc.collect() + return True # Continue timer def _ensure_music_widget(self): """Lazy load music widget content""" if not self._music_initialized: - self._music_widget_content = PlayerBoxStack(MprisPlayerManager(), control_center=self) + self._music_widget_content = PlayerBoxStack(get_shared_mpris_manager(), control_center=self) # Add to the music widget's children list current_children = list(self.music_widget.children) current_children.append(self._music_widget_content) diff --git a/modules/controlcenter/player.py b/modules/controlcenter/player.py index c4e1d46..423ca81 100644 --- a/modules/controlcenter/player.py +++ b/modules/controlcenter/player.py @@ -816,16 +816,16 @@ class PlayerBox(Box): if self._download_cancelled: return - # Clean up old temp files first (keep only last 2 to avoid excessive cleanup) - if len(self.temp_artwork_files) > 2: - old_files = self.temp_artwork_files[:-2] + # Clean up old temp files first (keep only last 1 to reduce memory) + if len(self.temp_artwork_files) > 1: + old_files = self.temp_artwork_files[:-1] for old_file in old_files: try: if os.path.exists(old_file): os.unlink(old_file) except Exception: pass - self.temp_artwork_files = self.temp_artwork_files[-2:] + self.temp_artwork_files = self.temp_artwork_files[-1:] # Check again if cancelled if self._download_cancelled: diff --git a/styles/player.css b/styles/player.css index 82359eb..aa49686 100644 --- a/styles/player.css +++ b/styles/player.css @@ -22,8 +22,8 @@ } #outer-player-box { - min-height: 130px; - min-width: 363px; + min-height: 100px; + min-width: 300px; border-radius: 10px; margin: 0 6px; background-color: alpha(#000, 0.3); @@ -31,16 +31,16 @@ } #outer-player-box-c { - min-height: 70px; - min-width: 263px; + min-height: 55px; + min-width: 220px; border-radius: 8px; /* margin: 0 6px; */ /* background-color: alpha(#000, 0.3); */ /* box-shadow: inset 0 0 0 1px alpha(#aaa, 0.4); */ } #box-c { - min-height: 60px; - min-width: 263px; + min-height: 50px; + min-width: 220px; border-radius: 8px; margin: 0 6px 10px; border: 0.5px solid alpha(#000, 0.4); @@ -118,8 +118,8 @@ border-radius: 5px; } .album-image { - min-width: 100px; - min-height: 90px; + min-width: 75px; + min-height: 70px; background-position: center; background-size: cover; margin-left: 12px;