Per App Volume Control Enhanced
This commit is contained in:
1 parent
f1993ef473
commit
2575814554
4 files changed
+226
-182
No files matched your search
@@ -74,7 +74,7 @@ class EmbeddedExpandedPlayer(Box):
|
||||
self.control_center = control_center
|
||||
self.mpris_manager = get_shared_mpris_manager()
|
||||
|
||||
# Create back button
|
||||
# Create back button (hidden in header)
|
||||
self.back_button = Button(
|
||||
name="back-button",
|
||||
child=Label(label="← Back"),
|
||||
@@ -84,11 +84,19 @@ class EmbeddedExpandedPlayer(Box):
|
||||
# Create expanded player content
|
||||
self.player_content = PlayerBoxStack(self.mpris_manager)
|
||||
|
||||
# Add escape key binding for navigation back
|
||||
try:
|
||||
if hasattr(self.control_center, "add_keybinding"):
|
||||
self.control_center.add_keybinding("Escape", self._on_back_clicked)
|
||||
except Exception:
|
||||
pass # Ignore if keybinding fails
|
||||
|
||||
self.children = [
|
||||
Box(
|
||||
orientation="horizontal",
|
||||
h_expand=True,
|
||||
style_classes="menu",
|
||||
visible=False, # Hide header to remove title and back button
|
||||
children=[
|
||||
self.back_button,
|
||||
Box(h_expand=True), # Spacer
|
||||
|
||||
@@ -20,12 +20,15 @@ from modules.controlcenter.player import PlayerBoxStack
|
||||
from modules.controlcenter.per_app_volume import PerAppVolumeControl
|
||||
from modules.controlcenter.expanded_player import EmbeddedExpandedPlayer
|
||||
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
|
||||
@@ -33,9 +36,6 @@ def get_shared_mpris_manager():
|
||||
_shared_mpris_manager = MprisPlayerManager()
|
||||
return _shared_mpris_manager
|
||||
|
||||
# FIX: Icon not showing up in control center
|
||||
# TODO: Add Player
|
||||
|
||||
|
||||
class ModusControlCenter(Window):
|
||||
def __init__(self, **kwargs):
|
||||
@@ -53,12 +53,12 @@ class ModusControlCenter(Window):
|
||||
self.focus_mode = modus_service.dont_disturb
|
||||
self._updating_brightness = False
|
||||
self._updating_volume = False
|
||||
|
||||
|
||||
# Lazy loading flags
|
||||
self._music_initialized = False
|
||||
self._per_app_volume_initialized = False
|
||||
self._expanded_player_initialized = False
|
||||
|
||||
|
||||
# Store references for cleanup
|
||||
self._signal_connections = []
|
||||
self._music_widget_content = None
|
||||
@@ -73,11 +73,13 @@ class ModusControlCenter(Window):
|
||||
music = modus_service.sc("music-changed", self.audio_changed)
|
||||
|
||||
# Store signal connections for cleanup
|
||||
self._signal_connections.extend([
|
||||
audio_service.connect("changed", self.audio_changed),
|
||||
audio_service.connect("changed", self.volume_changed),
|
||||
modus_service.connect("dont-disturb-changed", self.dnd_changed)
|
||||
])
|
||||
self._signal_connections.extend(
|
||||
[
|
||||
audio_service.connect("changed", self.audio_changed),
|
||||
audio_service.connect("changed", self.volume_changed),
|
||||
modus_service.connect("dont-disturb-changed", self.dnd_changed),
|
||||
]
|
||||
)
|
||||
|
||||
self.wlan_label = Label(wlan, name="wifi-widget-label", h_align="start")
|
||||
if bluetooth != "disabled":
|
||||
@@ -134,9 +136,7 @@ class ModusControlCenter(Window):
|
||||
|
||||
# Create placeholder music widget - lazy load content when needed
|
||||
self.music_widget = Box(
|
||||
name="music-widget",
|
||||
h_align="start",
|
||||
children=[] # Empty initially
|
||||
name="music-widget", h_align="start", children=[] # Empty initially
|
||||
)
|
||||
|
||||
self.has_bluetooth_open = False
|
||||
@@ -288,17 +288,23 @@ class ModusControlCenter(Window):
|
||||
Box(
|
||||
orientation="horizontal",
|
||||
spacing=8,
|
||||
v_expand=False,
|
||||
children=[
|
||||
self.volume_scale,
|
||||
Button(
|
||||
name="per-app-volume-button",
|
||||
child=Label(label="⚙", name="per-app-volume-icon"),
|
||||
child=Svg(
|
||||
svg_file=get_relative_path(
|
||||
"../../config/assets/icons/player/audio-switcher.svg"
|
||||
),
|
||||
name="per-app-volume-icon",
|
||||
),
|
||||
on_clicked=self.open_per_app_volume,
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
Label(
|
||||
label=" ", name="brightness-widget-icon", h_align="start"
|
||||
label=" ", name="brightness-widget-icon", h_align="start"
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -333,10 +339,10 @@ class ModusControlCenter(Window):
|
||||
self.widgets.set_size_request(300, -1)
|
||||
|
||||
self.children = self.center_box
|
||||
|
||||
|
||||
# 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)
|
||||
|
||||
@@ -345,33 +351,33 @@ class ModusControlCenter(Window):
|
||||
# Disconnect all signal connections
|
||||
for connection in self._signal_connections:
|
||||
try:
|
||||
if hasattr(connection, 'disconnect'):
|
||||
if hasattr(connection, "disconnect"):
|
||||
connection.disconnect()
|
||||
elif hasattr(connection, 'handler_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:
|
||||
if hasattr(self, "wifi_man") and self.wifi_man:
|
||||
try:
|
||||
if hasattr(self.wifi_man, 'destroy'):
|
||||
if hasattr(self.wifi_man, "destroy"):
|
||||
self.wifi_man.destroy()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if hasattr(self, 'bluetooth_man') and self.bluetooth_man:
|
||||
|
||||
if hasattr(self, "bluetooth_man") and self.bluetooth_man:
|
||||
try:
|
||||
if hasattr(self.bluetooth_man, 'destroy'):
|
||||
if hasattr(self.bluetooth_man, "destroy"):
|
||||
self.bluetooth_man.destroy()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
super().destroy()
|
||||
|
||||
def _on_visibility_changed(self, widget, param):
|
||||
@@ -382,7 +388,9 @@ class ModusControlCenter(Window):
|
||||
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'):
|
||||
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)
|
||||
@@ -395,57 +403,65 @@ class ModusControlCenter(Window):
|
||||
self._music_initialized = False
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to cleanup music widget content: {e}")
|
||||
|
||||
|
||||
# Clean up per-app volume widget
|
||||
if self._per_app_volume_widget and hasattr(self._per_app_volume_widget, 'destroy'):
|
||||
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'):
|
||||
|
||||
# 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.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(get_shared_mpris_manager(), 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)
|
||||
@@ -478,7 +494,9 @@ class ModusControlCenter(Window):
|
||||
),
|
||||
],
|
||||
)
|
||||
self.bluetooth_center_box = CenterBox(start_children=[self.bluetooth_widgets])
|
||||
self.bluetooth_center_box = CenterBox(
|
||||
start_children=[self.bluetooth_widgets]
|
||||
)
|
||||
self.bluetooth_center_box.set_size_request(300, -1)
|
||||
|
||||
def _ensure_wifi_widgets(self):
|
||||
@@ -515,7 +533,7 @@ class ModusControlCenter(Window):
|
||||
if self.per_app_volume_widgets is None:
|
||||
if self._per_app_volume_widget is None:
|
||||
self._per_app_volume_widget = PerAppVolumeControl(self)
|
||||
|
||||
|
||||
self.per_app_volume_widgets = Box(
|
||||
orientation="vertical",
|
||||
h_expand=True,
|
||||
@@ -524,7 +542,9 @@ class ModusControlCenter(Window):
|
||||
self._per_app_volume_widget,
|
||||
],
|
||||
)
|
||||
self.per_app_volume_center_box = CenterBox(start_children=[self.per_app_volume_widgets])
|
||||
self.per_app_volume_center_box = CenterBox(
|
||||
start_children=[self.per_app_volume_widgets]
|
||||
)
|
||||
self.per_app_volume_center_box.set_size_request(300, -1)
|
||||
|
||||
def _ensure_expanded_player_widgets(self):
|
||||
@@ -532,7 +552,7 @@ class ModusControlCenter(Window):
|
||||
if self.expanded_player_widgets is None:
|
||||
if self._expanded_player_widget is None:
|
||||
self._expanded_player_widget = EmbeddedExpandedPlayer(self)
|
||||
|
||||
|
||||
self.expanded_player_widgets = Box(
|
||||
orientation="vertical",
|
||||
h_expand=True,
|
||||
@@ -541,7 +561,9 @@ class ModusControlCenter(Window):
|
||||
self._expanded_player_widget,
|
||||
],
|
||||
)
|
||||
self.expanded_player_center_box = CenterBox(start_children=[self.expanded_player_widgets])
|
||||
self.expanded_player_center_box = CenterBox(
|
||||
start_children=[self.expanded_player_widgets]
|
||||
)
|
||||
self.expanded_player_center_box.set_size_request(300, -1)
|
||||
|
||||
def set_dont_disturb(self, *_):
|
||||
@@ -654,7 +676,7 @@ class ModusControlCenter(Window):
|
||||
if visible:
|
||||
# Lazy load music widget when becoming visible
|
||||
self._ensure_music_widget()
|
||||
|
||||
|
||||
self.set_visible(visible)
|
||||
if not visible:
|
||||
self.close_bluetooth()
|
||||
@@ -744,7 +766,7 @@ class ModusControlCenter(Window):
|
||||
def hide_controlcenter(self, *_):
|
||||
self._mousecapture_parent.toggle_mousecapture()
|
||||
self.set_visible(False)
|
||||
|
||||
|
||||
def destroy(self):
|
||||
"""Clean up resources when widget is destroyed"""
|
||||
# Disconnect all signal connections
|
||||
@@ -753,7 +775,7 @@ class ModusControlCenter(Window):
|
||||
connection.disconnect()
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
# Clean up heavy components
|
||||
if self._wifi_man:
|
||||
self._wifi_man.destroy()
|
||||
@@ -765,5 +787,5 @@ class ModusControlCenter(Window):
|
||||
self._per_app_volume_widget.destroy()
|
||||
if self._expanded_player_widget:
|
||||
self._expanded_player_widget.destroy()
|
||||
|
||||
|
||||
super().destroy()
|
||||
@@ -15,54 +15,45 @@ from utils.roam import audio_service
|
||||
|
||||
class PerAppVolumeControl(Box):
|
||||
"""Per-application volume control widget"""
|
||||
|
||||
|
||||
def __init__(self, control_center, **kwargs):
|
||||
super().__init__(
|
||||
orientation="vertical",
|
||||
name="per-app-volume-control",
|
||||
style_classes="menu",
|
||||
spacing=5,
|
||||
**kwargs
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
self.control_center = control_center
|
||||
self._updating_volumes = set()
|
||||
self._app_widgets = {}
|
||||
self._signal_connections = []
|
||||
|
||||
# Header with back button
|
||||
header = Box(
|
||||
orientation="horizontal",
|
||||
spacing=10,
|
||||
children=[
|
||||
Button(
|
||||
label="← Back",
|
||||
name="back-button",
|
||||
on_clicked=self._go_back,
|
||||
),
|
||||
Label(
|
||||
label="App Volume",
|
||||
style_classes="title",
|
||||
h_expand=True,
|
||||
h_align="start"
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
# Header with back button (hidden for cleaner UI)
|
||||
|
||||
# Scrollable container for app volume controls
|
||||
self.apps_container = Box(
|
||||
orientation="vertical",
|
||||
name="apps-scrolled-container",
|
||||
spacing=5,
|
||||
)
|
||||
|
||||
|
||||
self.scrolled_window = ScrolledWindow(
|
||||
name="apps-scrolled-window",
|
||||
child=self.apps_container,
|
||||
min_content_size=(280, 200),
|
||||
max_content_size=(280, 300),
|
||||
size=(300, 500),
|
||||
)
|
||||
|
||||
self.children = [header, self.scrolled_window]
|
||||
|
||||
|
||||
# Add escape key binding for navigation back
|
||||
try:
|
||||
if hasattr(self.control_center, "add_keybinding"):
|
||||
self.control_center.add_keybinding("Escape", self._go_back)
|
||||
except Exception:
|
||||
pass # Ignore if keybinding fails
|
||||
|
||||
self.children = [self.scrolled_window]
|
||||
|
||||
# Connect to audio service changes
|
||||
if audio_service:
|
||||
self._signal_connections.append(
|
||||
@@ -71,10 +62,10 @@ class PerAppVolumeControl(Box):
|
||||
self._signal_connections.append(
|
||||
audio_service.connect("stream-removed", self._on_stream_changed)
|
||||
)
|
||||
|
||||
|
||||
# Initial population
|
||||
self._populate_apps()
|
||||
|
||||
|
||||
# Set up auto-refresh timer for PulseAudio sinks
|
||||
self._refresh_timer = GLib.timeout_add_seconds(2, self._auto_refresh)
|
||||
|
||||
@@ -82,7 +73,7 @@ class PerAppVolumeControl(Box):
|
||||
"""Auto-refresh the application list every 2 seconds"""
|
||||
self._populate_apps()
|
||||
return True # Continue the timer
|
||||
|
||||
|
||||
def _go_back(self, *_):
|
||||
"""Return to main control center view"""
|
||||
self.control_center.close_per_app_volume()
|
||||
@@ -90,38 +81,41 @@ class PerAppVolumeControl(Box):
|
||||
def _get_pulse_sinks(self):
|
||||
"""Get PulseAudio sink inputs (application audio streams)"""
|
||||
try:
|
||||
result = subprocess.run([
|
||||
'pactl', 'list', 'sink-inputs'
|
||||
], capture_output=True, text=True, check=True)
|
||||
|
||||
result = subprocess.run(
|
||||
["pactl", "list", "sink-inputs"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
|
||||
sinks = []
|
||||
current_sink = {}
|
||||
|
||||
for line in result.stdout.split('\n'):
|
||||
|
||||
for line in result.stdout.split("\n"):
|
||||
line = line.strip()
|
||||
|
||||
if line.startswith('Sink Input #'):
|
||||
|
||||
if line.startswith("Sink Input #"):
|
||||
if current_sink:
|
||||
sinks.append(current_sink)
|
||||
current_sink = {
|
||||
'index': line.split('#')[1],
|
||||
'name': 'Unknown',
|
||||
'volume_raw': 100
|
||||
"index": line.split("#")[1],
|
||||
"name": "Unknown",
|
||||
"volume_raw": 100,
|
||||
}
|
||||
elif 'application.name = ' in line:
|
||||
current_sink['name'] = line.split('= ')[1].strip('"')
|
||||
elif 'Volume:' in line and 'front-left:' in line:
|
||||
elif "application.name = " in line:
|
||||
current_sink["name"] = line.split("= ")[1].strip('"')
|
||||
elif "Volume:" in line and "front-left:" in line:
|
||||
parts = line.split()
|
||||
for part in parts:
|
||||
if part.endswith('%'):
|
||||
current_sink['volume_raw'] = int(part.replace('%', ''))
|
||||
if part.endswith("%"):
|
||||
current_sink["volume_raw"] = int(part.replace("%", ""))
|
||||
break
|
||||
|
||||
|
||||
if current_sink:
|
||||
sinks.append(current_sink)
|
||||
|
||||
|
||||
return sinks
|
||||
|
||||
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
return []
|
||||
|
||||
@@ -129,25 +123,25 @@ class PerAppVolumeControl(Box):
|
||||
"""Format application name with proper capitalization"""
|
||||
if not name:
|
||||
return "Unknown"
|
||||
|
||||
|
||||
# Handle common app names specially
|
||||
special_names = {
|
||||
'spotify': 'Spotify',
|
||||
'firefox': 'Firefox',
|
||||
'chromium': 'Chromium',
|
||||
'chrome': 'Chrome',
|
||||
'vlc': 'VLC',
|
||||
'discord': 'Discord',
|
||||
'steam': 'Steam',
|
||||
'zen': 'Zen Browser',
|
||||
'code': 'VS Code',
|
||||
'telegram': 'Telegram'
|
||||
"spotify": "Spotify",
|
||||
"firefox": "Firefox",
|
||||
"chromium": "Chromium",
|
||||
"chrome": "Chrome",
|
||||
"vlc": "VLC",
|
||||
"discord": "Discord",
|
||||
"steam": "Steam",
|
||||
"zen": "Zen Browser",
|
||||
"code": "VS Code",
|
||||
"telegram": "Telegram",
|
||||
}
|
||||
|
||||
|
||||
name_lower = name.lower()
|
||||
if name_lower in special_names:
|
||||
return special_names[name_lower]
|
||||
|
||||
|
||||
# Default: capitalize first letter
|
||||
return name.capitalize()
|
||||
|
||||
@@ -156,29 +150,33 @@ class PerAppVolumeControl(Box):
|
||||
# Clear existing widgets
|
||||
self.apps_container.children = []
|
||||
self._app_widgets.clear()
|
||||
|
||||
|
||||
# Try Fabric audio service first
|
||||
applications = []
|
||||
if audio_service and hasattr(audio_service, 'applications'):
|
||||
if audio_service and hasattr(audio_service, "applications"):
|
||||
applications = audio_service.applications
|
||||
|
||||
|
||||
# If no Fabric applications, try PulseAudio directly
|
||||
if not applications:
|
||||
pulse_sinks = self._get_pulse_sinks()
|
||||
|
||||
|
||||
if pulse_sinks:
|
||||
for sink in pulse_sinks:
|
||||
app_widget = self._create_pulse_app_control(sink)
|
||||
self.apps_container.children = list(self.apps_container.children) + [app_widget]
|
||||
self._app_widgets[sink['name']] = (app_widget, sink)
|
||||
self.apps_container.children = list(
|
||||
self.apps_container.children
|
||||
) + [app_widget]
|
||||
self._app_widgets[sink["name"]] = (app_widget, sink)
|
||||
return
|
||||
|
||||
|
||||
# Use Fabric applications if available
|
||||
if applications:
|
||||
for app in applications:
|
||||
if hasattr(app, 'name') and hasattr(app, 'volume'):
|
||||
if hasattr(app, "name") and hasattr(app, "volume"):
|
||||
app_widget = self._create_app_control(app)
|
||||
self.apps_container.children = list(self.apps_container.children) + [app_widget]
|
||||
self.apps_container.children = list(
|
||||
self.apps_container.children
|
||||
) + [app_widget]
|
||||
self._app_widgets[app.name] = (app_widget, app)
|
||||
else:
|
||||
self._show_no_apps_message()
|
||||
@@ -196,13 +194,13 @@ class PerAppVolumeControl(Box):
|
||||
def _create_pulse_app_control(self, sink):
|
||||
"""Create volume control for a PulseAudio sink input"""
|
||||
# Format and truncate app name
|
||||
app_name = self._format_app_name(sink['name'])
|
||||
app_name = self._format_app_name(sink["name"])
|
||||
if len(app_name) > 20:
|
||||
app_name = app_name[:17] + "..."
|
||||
|
||||
|
||||
# Volume scale with Apple-like styling
|
||||
volume_scale = Scale(
|
||||
value=sink['volume_raw'],
|
||||
value=sink["volume_raw"],
|
||||
min_value=0,
|
||||
max_value=150,
|
||||
increments=(5, 5),
|
||||
@@ -210,10 +208,15 @@ class PerAppVolumeControl(Box):
|
||||
size=28,
|
||||
h_expand=True,
|
||||
)
|
||||
|
||||
|
||||
# Connect volume change handler
|
||||
volume_scale.connect("change-value", lambda scale, scroll_type, value, sink_data=sink: self._set_pulse_volume(sink_data, value))
|
||||
|
||||
volume_scale.connect(
|
||||
"change-value",
|
||||
lambda scale, scroll_type, value, sink_data=sink: self._set_pulse_volume(
|
||||
sink_data, value
|
||||
),
|
||||
)
|
||||
|
||||
# Create the app control widget
|
||||
app_control = Box(
|
||||
orientation="vertical",
|
||||
@@ -226,9 +229,9 @@ class PerAppVolumeControl(Box):
|
||||
h_align="start",
|
||||
),
|
||||
volume_scale,
|
||||
]
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
return app_control
|
||||
|
||||
def _create_app_control(self, app):
|
||||
@@ -237,7 +240,7 @@ class PerAppVolumeControl(Box):
|
||||
app_name = self._format_app_name(app.name)
|
||||
if len(app_name) > 20:
|
||||
app_name = app_name[:17] + "..."
|
||||
|
||||
|
||||
# Handle different volume formats
|
||||
volume_value = app.volume
|
||||
if isinstance(volume_value, float):
|
||||
@@ -252,7 +255,7 @@ class PerAppVolumeControl(Box):
|
||||
volume_percent = min(max(volume_value, 0), 100)
|
||||
else:
|
||||
volume_percent = 50
|
||||
|
||||
|
||||
# Volume scale with Apple-like styling
|
||||
volume_scale = Scale(
|
||||
value=volume_percent,
|
||||
@@ -263,10 +266,13 @@ class PerAppVolumeControl(Box):
|
||||
size=28,
|
||||
h_expand=True,
|
||||
)
|
||||
|
||||
|
||||
# Connect volume change handler
|
||||
volume_scale.connect("change-value", lambda scale, scroll_type, value, app=app: self._set_app_volume(app, value))
|
||||
|
||||
volume_scale.connect(
|
||||
"change-value",
|
||||
lambda scale, scroll_type, value, app=app: self._set_app_volume(app, value),
|
||||
)
|
||||
|
||||
# Create the app control widget
|
||||
app_control = Box(
|
||||
orientation="vertical",
|
||||
@@ -279,38 +285,43 @@ class PerAppVolumeControl(Box):
|
||||
h_align="start",
|
||||
),
|
||||
volume_scale,
|
||||
]
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
return app_control
|
||||
|
||||
def _set_pulse_volume(self, sink, volume_percent):
|
||||
"""Set volume for a PulseAudio sink input"""
|
||||
if sink['name'] in self._updating_volumes:
|
||||
if sink["name"] in self._updating_volumes:
|
||||
return
|
||||
|
||||
self._updating_volumes.add(sink['name'])
|
||||
|
||||
|
||||
self._updating_volumes.add(sink["name"])
|
||||
|
||||
try:
|
||||
subprocess.run([
|
||||
'pactl', 'set-sink-input-volume',
|
||||
sink['index'], f'{int(volume_percent)}%'
|
||||
], check=True)
|
||||
subprocess.run(
|
||||
[
|
||||
"pactl",
|
||||
"set-sink-input-volume",
|
||||
sink["index"],
|
||||
f"{int(volume_percent)}%",
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
pass
|
||||
finally:
|
||||
GLib.timeout_add(100, lambda: self._updating_volumes.discard(sink['name']))
|
||||
GLib.timeout_add(100, lambda: self._updating_volumes.discard(sink["name"]))
|
||||
|
||||
def _set_app_volume(self, app, volume_percent):
|
||||
"""Set volume for a specific application (Fabric)"""
|
||||
if app.name in self._updating_volumes:
|
||||
return
|
||||
|
||||
|
||||
self._updating_volumes.add(app.name)
|
||||
|
||||
|
||||
try:
|
||||
current_volume = app.volume
|
||||
|
||||
|
||||
if isinstance(current_volume, float):
|
||||
if 0.0 <= current_volume <= 1.0:
|
||||
volume_value = volume_percent / 100.0
|
||||
@@ -323,34 +334,35 @@ class PerAppVolumeControl(Box):
|
||||
volume_value = int(volume_percent)
|
||||
else:
|
||||
volume_value = volume_percent
|
||||
|
||||
|
||||
app.volume = volume_value
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
GLib.timeout_add(100, lambda: self._updating_volumes.discard(app.name))
|
||||
|
||||
|
||||
def _on_stream_changed(self, *_):
|
||||
"""Handle when audio streams are added or removed"""
|
||||
GLib.idle_add(self._populate_apps)
|
||||
|
||||
|
||||
def refresh(self):
|
||||
"""Manually refresh the application list"""
|
||||
self._populate_apps()
|
||||
|
||||
|
||||
def destroy(self):
|
||||
"""Clean up resources"""
|
||||
if hasattr(self, '_refresh_timer'):
|
||||
if hasattr(self, "_refresh_timer"):
|
||||
GLib.source_remove(self._refresh_timer)
|
||||
|
||||
|
||||
for connection in self._signal_connections:
|
||||
try:
|
||||
connection.disconnect()
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
self._signal_connections.clear()
|
||||
self._app_widgets.clear()
|
||||
self._updating_volumes.clear()
|
||||
|
||||
super().destroy()
|
||||
|
||||
super().destroy()
|
||||
|
||||
+23
-21
@@ -321,13 +321,19 @@
|
||||
|
||||
/* Per-app volume control styles */
|
||||
#per-app-volume-control {
|
||||
margin: 5px;
|
||||
margin: 0px;
|
||||
background-color: alpha(#999, 0.1);
|
||||
min-height: 140px;
|
||||
min-width: 140px;
|
||||
border: 0.5px solid alpha(#000, 0.4);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
/* padding: 1rem; */
|
||||
}
|
||||
|
||||
#apps-scrolled-container {
|
||||
min-height: 120px;
|
||||
min-width: 120px;
|
||||
}
|
||||
#back-button {
|
||||
padding: 8px 12px;
|
||||
background-color: alpha(#fff, 0.1);
|
||||
@@ -345,7 +351,7 @@
|
||||
|
||||
/* Apple-style app volume items */
|
||||
.apple-app-volume-item {
|
||||
margin: 12px 0;
|
||||
margin: 3px 0;
|
||||
padding: 16px;
|
||||
background-color: alpha(#fff, 0.08);
|
||||
border-radius: 12px;
|
||||
@@ -361,14 +367,14 @@
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 4px;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
/* Apple-style volume slider */
|
||||
#apple-volume-slider {
|
||||
background-color: transparent;
|
||||
margin-top: 8px;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
min-height: 32px;
|
||||
}
|
||||
@@ -391,11 +397,11 @@
|
||||
|
||||
#apple-volume-slider slider {
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
|
||||
border: 2px solid alpha(#007aff, 0.8);
|
||||
/* border: 2px solid alpha(#007aff, 0.8); */
|
||||
border-radius: 50%;
|
||||
min-width: 20px;
|
||||
min-height: 20px;
|
||||
margin: -7px;
|
||||
margin: -10px;
|
||||
box-shadow:
|
||||
0 2px 8px alpha(#000, 0.15),
|
||||
0 1px 3px alpha(#000, 0.2);
|
||||
@@ -410,14 +416,14 @@
|
||||
0 2px 6px alpha(#007aff, 0.3);
|
||||
min-width: 22px;
|
||||
min-height: 22px;
|
||||
margin: -8px;
|
||||
margin: -9px;
|
||||
}
|
||||
|
||||
#apple-volume-slider slider:active {
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
min-width: 21px;
|
||||
min-height: 21px;
|
||||
margin: -7.5px;
|
||||
margin: -9px;
|
||||
box-shadow:
|
||||
0 2px 6px alpha(#000, 0.25),
|
||||
0 1px 3px alpha(#007aff, 0.4);
|
||||
@@ -441,7 +447,7 @@
|
||||
|
||||
#app-volume-slider {
|
||||
background-color: transparent;
|
||||
margin-top: 5px;
|
||||
/* margin-top: 5px; */
|
||||
}
|
||||
|
||||
#app-volume-slider trough {
|
||||
@@ -468,18 +474,14 @@
|
||||
/* Circle button for per-app volume */
|
||||
#per-app-volume-button {
|
||||
background-color: alpha(#fff, 0.15);
|
||||
border: 2px solid alpha(#007aff, 0.6);
|
||||
border: 2px solid alpha(#999, 0.6);
|
||||
border-radius: 50%;
|
||||
min-width: 32px;
|
||||
min-height: 32px;
|
||||
padding: 0;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 6px alpha(#000, 0.15);
|
||||
min-width: 30px;
|
||||
}
|
||||
|
||||
#per-app-volume-button:hover {
|
||||
background-color: alpha(#fff, 0.25);
|
||||
border-color: #007aff;
|
||||
/* border-color: #007aff; */
|
||||
box-shadow: 0 4px 12px alpha(#000, 0.2);
|
||||
}
|
||||
|
||||
@@ -489,10 +491,10 @@
|
||||
}
|
||||
|
||||
#per-app-volume-icon {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #007aff;
|
||||
text-shadow: none;
|
||||
/* font-size: 14px; */
|
||||
/* font-weight: bold; */
|
||||
/* color: #007aff; */
|
||||
/* text-shadow: none; */
|
||||
}
|
||||
|
||||
/* Apple-style seek bar for expanded player */
|
||||
|
||||
Reference in new issue
Block a user