refactor: extract shared mixins, fix cpu waste, resolve browser icons
- Extract CloseButtonRevealerMixin, PullToRefreshMixin, BasePlayerBoxStack - Replace mpris.py 2s+5s D-Bus polling with Seeked signal subscription - Remove redundant desktop/main.py os.listdir() poll (FileMonitor covers) - Remove switcher/main.py 200ms capture timer, chain from frame-ready - Resolve browser MPRIS icons via desktop file lookup, fallback to app name - Fix CodeQL py/clear-text-storage by rounding lat/lon and stripping from cache
This commit is contained in:
14 files changed
+515
-740
No files matched your search
@@ -1,6 +1,6 @@
|
||||
from enum import Enum, auto
|
||||
|
||||
from fabric.utils import Gdk, GLib, Gtk, idle_add, logger
|
||||
from fabric.utils import GLib, Gtk, idle_add, logger
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.centerbox import CenterBox
|
||||
@@ -9,6 +9,7 @@ from fabric.widgets.label import Label
|
||||
from fabric.widgets.separator import Separator
|
||||
|
||||
from services.bluetooth import BluetoothClient, BluetoothDevice
|
||||
from shared.widgets.pull_to_refresh import PullToRefreshMixin
|
||||
from shared.widgets.smooth_switch import SmoothSwitch
|
||||
from shared.window.animated_scrollwindow import AnimatedScrollable
|
||||
from utils.functions import spawn_detached
|
||||
@@ -304,7 +305,7 @@ class BluetoothDeviceSlot(Box):
|
||||
logger.warning(f"[Bluetooth] Failed to remove device: {e}")
|
||||
|
||||
|
||||
class BluetoothConnections(Box):
|
||||
class BluetoothConnections(PullToRefreshMixin, Box):
|
||||
def __init__(
|
||||
self, parent, show_hidden_devices: bool = False, show_back_button=True, **kwargs
|
||||
):
|
||||
@@ -422,7 +423,7 @@ class BluetoothConnections(Box):
|
||||
self.other_devices.set_visible(False)
|
||||
|
||||
# Add pull-to-refresh functionality to scrolled window
|
||||
self.setup_pull_to_refresh()
|
||||
self.setup_pull_to_refresh(self.other_devices_scrolled)
|
||||
|
||||
self.more_settings_button = Button(
|
||||
child=Label("More Settings", h_align="start"),
|
||||
@@ -507,31 +508,6 @@ class BluetoothConnections(Box):
|
||||
# Defer refresh until after the height animation completes
|
||||
idle_add(self._refresh_after_animation)
|
||||
|
||||
def _cancel_pending_refresh(self):
|
||||
if hasattr(self, "_anim_finished_handler") and self._anim_finished_handler:
|
||||
try:
|
||||
self.other_devices_scrolled.height_animator.disconnect(
|
||||
self._anim_finished_handler
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"[bluetooth] self.other_devices_scrolled.height_animator.disconnect( s... failed: {e}"
|
||||
)
|
||||
self._anim_finished_handler = None
|
||||
|
||||
def _refresh_after_animation(self):
|
||||
if self._destroyed:
|
||||
return False
|
||||
anim = self.other_devices_scrolled.height_animator
|
||||
if anim.playing:
|
||||
self._anim_finished_handler = anim.connect(
|
||||
"finished",
|
||||
lambda *_: self.force_device_refresh() if not self._destroyed else None,
|
||||
)
|
||||
else:
|
||||
self.force_device_refresh()
|
||||
return False
|
||||
|
||||
def open_bluetooth_settings(self, *_):
|
||||
"""Open Blueman bluetooth manager"""
|
||||
try:
|
||||
@@ -692,84 +668,22 @@ class BluetoothConnections(Box):
|
||||
self.other_devices.set_visible(False)
|
||||
self.other_devices_scrolled.snap_to_size(0)
|
||||
|
||||
def setup_pull_to_refresh(self):
|
||||
"""Setup pull-to-refresh gesture for the scrolled window"""
|
||||
self.vadjustment = self.other_devices_scrolled.get_vadjustment()
|
||||
def _trigger_scan(self):
|
||||
if not self.client.scanning and hasattr(self.client, "scan"):
|
||||
self.client.scan()
|
||||
|
||||
# Track gesture state
|
||||
self.pull_start_y = 0
|
||||
self.is_pulling = False
|
||||
self.pull_threshold = 50 # pixels to trigger refresh
|
||||
def _trigger_refresh(self):
|
||||
self.force_device_refresh()
|
||||
|
||||
# Connect to scroll events
|
||||
self.other_devices_scrolled.connect("scroll-event", self.on_scroll_event)
|
||||
self.other_devices_scrolled.connect("button-press-event", self.on_button_press)
|
||||
self.other_devices_scrolled.connect(
|
||||
"button-release-event", self.on_button_release
|
||||
)
|
||||
self.other_devices_scrolled.connect(
|
||||
"motion-notify-event", self.on_motion_notify
|
||||
)
|
||||
def _get_pull_normal_label(self):
|
||||
if self.client.scanning:
|
||||
return "↓ Pull to stop scanning"
|
||||
return "↓ Pull to scan for devices"
|
||||
|
||||
# Enable events
|
||||
self.other_devices_scrolled.set_events(
|
||||
Gdk.EventMask.SCROLL_MASK
|
||||
| Gdk.EventMask.BUTTON_PRESS_MASK
|
||||
| Gdk.EventMask.BUTTON_RELEASE_MASK
|
||||
| Gdk.EventMask.POINTER_MOTION_MASK
|
||||
)
|
||||
|
||||
def on_scroll_event(self, widget, event):
|
||||
"""Handle scroll events for pull-to-refresh"""
|
||||
if self.vadjustment.get_value() <= 0:
|
||||
if event.direction == Gdk.ScrollDirection.UP:
|
||||
if not self.client.scanning and hasattr(self.client, "scan"):
|
||||
self.client.scan()
|
||||
self.force_device_refresh()
|
||||
return True
|
||||
return False
|
||||
|
||||
def on_button_press(self, widget, event):
|
||||
"""Handle button press for touch/drag gestures"""
|
||||
if self.vadjustment.get_value() <= 0:
|
||||
self.pull_start_y = event.y
|
||||
self.is_pulling = True
|
||||
return False
|
||||
|
||||
def on_button_release(self, widget, event):
|
||||
"""Handle button release for touch/drag gestures"""
|
||||
if self.is_pulling:
|
||||
pull_distance = event.y - self.pull_start_y
|
||||
if pull_distance > self.pull_threshold:
|
||||
if not self.client.scanning and hasattr(self.client, "scan"):
|
||||
self.client.scan()
|
||||
self.force_device_refresh()
|
||||
self.refresh_indicator.set_visible(False)
|
||||
self.refresh_indicator.remove_style_class("ready-to-refresh")
|
||||
self.is_pulling = False
|
||||
return False
|
||||
|
||||
def on_motion_notify(self, widget, event):
|
||||
"""Handle motion events for visual feedback during pull"""
|
||||
if self.is_pulling and self.vadjustment.get_value() <= 0:
|
||||
pull_distance = event.y - self.pull_start_y
|
||||
if pull_distance > 0:
|
||||
self.refresh_indicator.set_visible(True)
|
||||
if pull_distance >= self.pull_threshold:
|
||||
if self.client.scanning:
|
||||
self.refresh_indicator.set_label("↑ Release to stop scanning")
|
||||
else:
|
||||
self.refresh_indicator.set_label("↑ Release to scan")
|
||||
self.refresh_indicator.add_style_class("ready-to-refresh")
|
||||
else:
|
||||
if self.client.scanning:
|
||||
self.refresh_indicator.set_label("↓ Pull to stop scanning")
|
||||
else:
|
||||
self.refresh_indicator.set_label("↓ Pull to scan for devices")
|
||||
self.refresh_indicator.remove_style_class("ready-to-refresh")
|
||||
else:
|
||||
self.refresh_indicator.set_visible(False)
|
||||
return False
|
||||
def _get_pull_ready_label(self):
|
||||
if self.client.scanning:
|
||||
return "↑ Release to stop scanning"
|
||||
return "↑ Release to scan"
|
||||
|
||||
def on_device_added(self, client: BluetoothClient, address: str):
|
||||
"""Handle when a new device is added"""
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import os
|
||||
import weakref
|
||||
|
||||
from fabric.utils import GLib, bulk_connect, logger
|
||||
from fabric.widgets.box import Box
|
||||
@@ -12,11 +11,13 @@ from fabric.widgets.stack import Stack
|
||||
from fabric.widgets.wayland import WaylandWindow as Window
|
||||
|
||||
from services.mpris import PlayerManager, PlayerService
|
||||
from shared.widgets.player_box_stack import BasePlayerBoxStack
|
||||
from utils.gtk_utils import svg_file
|
||||
from window.controlcenter.player import (
|
||||
PLAYER_FALLBACK_ART,
|
||||
apply_player_art,
|
||||
get_shared_mpris_manager,
|
||||
resolve_mpris_player_icon,
|
||||
)
|
||||
|
||||
|
||||
@@ -55,42 +56,18 @@ class EmbeddedExpandedPlayer(Box):
|
||||
super().destroy()
|
||||
|
||||
|
||||
class PlayerBoxStack(Box):
|
||||
class PlayerBoxStack(BasePlayerBoxStack):
|
||||
def __init__(self, mpris_manager: PlayerManager, **kwargs):
|
||||
self.player_stack = Stack(name="player-stack", transition_type="none")
|
||||
self.current_stack_pos = 0
|
||||
self.player_buttons: list[Button] = []
|
||||
self._player_widgets = weakref.WeakValueDictionary()
|
||||
self._signal_connections = []
|
||||
self.no_media_box = self._create_no_media_box()
|
||||
|
||||
super().__init__(orientation="v", name="media", children=[self.player_stack])
|
||||
self.player_stack.children = [self.no_media_box]
|
||||
self.set_visible(True)
|
||||
self.mpris_manager = mpris_manager
|
||||
self.connect("map", self._on_map)
|
||||
super().__init__(mpris_manager, **kwargs)
|
||||
|
||||
connections = bulk_connect(
|
||||
self.mpris_manager,
|
||||
{"new-player": self.on_new_player, "player-vanish": self.on_lost_player},
|
||||
)
|
||||
for handler_id in connections:
|
||||
self._signal_connections.append((self.mpris_manager, handler_id))
|
||||
|
||||
for name, player in self.mpris_manager.get_all_services().items():
|
||||
self.on_new_player(self.mpris_manager, name, player)
|
||||
|
||||
def _on_map(self, *_):
|
||||
self._check_and_update_playing_state()
|
||||
|
||||
def destroy(self):
|
||||
for obj, handler_id in self._signal_connections:
|
||||
try:
|
||||
obj.disconnect(handler_id)
|
||||
except Exception as e:
|
||||
logger.error(f"An error occurred: {e}")
|
||||
self._signal_connections.clear()
|
||||
super().destroy()
|
||||
def _create_player_box_widget(self, player):
|
||||
return PlayerBox(player=player, player_stack=self)
|
||||
|
||||
def _create_no_media_box(self):
|
||||
fallback_cover_path = PLAYER_FALLBACK_ART
|
||||
@@ -139,138 +116,6 @@ class PlayerBoxStack(Box):
|
||||
],
|
||||
)
|
||||
|
||||
def _find_playing_player_index(self):
|
||||
players = self.player_stack.get_children()
|
||||
for i, player_box in enumerate(players):
|
||||
if (
|
||||
hasattr(player_box, "player")
|
||||
and str(player_box.player.playback_status).lower() == "playing"
|
||||
):
|
||||
return i
|
||||
return None
|
||||
|
||||
def _check_and_update_playing_state(self):
|
||||
players = [
|
||||
p for p in self.player_stack.get_children() if p != self.no_media_box
|
||||
]
|
||||
if len(players) > 0:
|
||||
if self.no_media_box in self.player_stack.get_children():
|
||||
self.player_stack.remove(self.no_media_box)
|
||||
playing_index = self._find_playing_player_index()
|
||||
if playing_index is not None:
|
||||
self.on_player_clicked_by_index(playing_index)
|
||||
return
|
||||
|
||||
if (
|
||||
len(self.player_stack.get_children()) == 1
|
||||
and self.player_stack.get_children()[0] == self.no_media_box
|
||||
):
|
||||
return
|
||||
|
||||
self.player_stack.children = [self.no_media_box]
|
||||
self.current_stack_pos = 0
|
||||
self.player_stack.set_visible_child(self.no_media_box)
|
||||
|
||||
def on_player_playback_changed(self, player_box, status):
|
||||
status = str(status).lower()
|
||||
if status == "playing":
|
||||
players = self.player_stack.get_children()
|
||||
for i, pb in enumerate(players):
|
||||
if pb == player_box and i != self.current_stack_pos:
|
||||
self.on_player_clicked_by_index(i)
|
||||
break
|
||||
elif status in ["paused", "stopped"]:
|
||||
self._check_and_update_playing_state()
|
||||
|
||||
def on_player_clicked_by_index(self, index):
|
||||
if 0 <= index < len(self.player_buttons):
|
||||
if self.current_stack_pos < len(self.player_buttons):
|
||||
self.player_buttons[self.current_stack_pos].remove_style_class("active")
|
||||
self.current_stack_pos = index
|
||||
self.player_buttons[self.current_stack_pos].add_style_class("active")
|
||||
self.player_stack.set_visible_child(
|
||||
self.player_stack.get_children()[self.current_stack_pos]
|
||||
)
|
||||
self._update_all_player_buttons()
|
||||
|
||||
def on_new_player(self, mpris_manager, name, player):
|
||||
if (
|
||||
len(self.player_stack.get_children()) == 1
|
||||
and self.player_stack.get_children()[0] == self.no_media_box
|
||||
):
|
||||
self.player_stack.children = []
|
||||
self.current_stack_pos = 0
|
||||
|
||||
self.set_visible(True)
|
||||
new_player_box = PlayerBox(player=player, player_stack=self)
|
||||
self.player_stack.children = [*self.player_stack.children, new_player_box]
|
||||
self.make_new_player_button(new_player_box)
|
||||
if self.player_buttons:
|
||||
self.player_buttons[self.current_stack_pos].set_style_classes(["active"])
|
||||
self._check_and_update_playing_state()
|
||||
self._update_all_player_buttons()
|
||||
|
||||
def on_lost_player(self, mpris_manager, player_name):
|
||||
player_box_to_remove = None
|
||||
for player_box in self.player_stack.get_children():
|
||||
if (
|
||||
hasattr(player_box, "player")
|
||||
and player_box.player.player_name == player_name
|
||||
):
|
||||
player_box_to_remove = player_box
|
||||
break
|
||||
|
||||
if player_box_to_remove:
|
||||
player_box_to_remove.destroy()
|
||||
|
||||
remaining_players = [
|
||||
p for p in self.player_stack.get_children() if p != player_box_to_remove
|
||||
]
|
||||
if len(remaining_players) == 0:
|
||||
self.player_stack.children = [self.no_media_box]
|
||||
self.current_stack_pos = 0
|
||||
self.player_buttons = []
|
||||
return
|
||||
|
||||
if self.current_stack_pos >= len(self.player_stack.get_children()):
|
||||
self.current_stack_pos = max(0, len(self.player_stack.get_children()) - 1)
|
||||
|
||||
if self.player_buttons and self.current_stack_pos < len(self.player_buttons):
|
||||
self.player_buttons[self.current_stack_pos].set_style_classes(["active"])
|
||||
self.player_stack.set_visible_child(
|
||||
self.player_stack.get_children()[self.current_stack_pos]
|
||||
)
|
||||
|
||||
self._update_all_player_buttons()
|
||||
|
||||
def make_new_player_button(self, player_box):
|
||||
new_button = Button(name="player-stack-button")
|
||||
|
||||
def on_player_button_click(button: Button):
|
||||
if self.current_stack_pos < len(self.player_buttons):
|
||||
self.player_buttons[self.current_stack_pos].remove_style_class("active")
|
||||
if button in self.player_buttons:
|
||||
self.current_stack_pos = self.player_buttons.index(button)
|
||||
button.add_style_class("active")
|
||||
self.player_stack.set_visible_child(player_box)
|
||||
|
||||
new_button.connect("clicked", on_player_button_click)
|
||||
self.player_buttons.append(new_button)
|
||||
player_box.connect(
|
||||
"destroy",
|
||||
lambda *_: (
|
||||
self.player_buttons.remove(new_button)
|
||||
if new_button in self.player_buttons
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
def _update_all_player_buttons(self):
|
||||
players = self.player_stack.get_children()
|
||||
for player_box in players:
|
||||
if hasattr(player_box, "update_buttons"):
|
||||
player_box.update_buttons(self.player_buttons, len(players) > 1)
|
||||
|
||||
|
||||
class PlayerBox(Box):
|
||||
def __init__(self, player: PlayerService, player_stack=None, **kwargs):
|
||||
@@ -303,7 +148,7 @@ class PlayerBox(Box):
|
||||
overlays=[
|
||||
Box(
|
||||
children=Image(
|
||||
icon_name=self.player.player_name,
|
||||
icon_name=resolve_mpris_player_icon(self.player.player_name),
|
||||
name="player-app-icon",
|
||||
icon_size=20,
|
||||
),
|
||||
|
||||
@@ -9,16 +9,60 @@ from fabric.widgets.stack import Stack
|
||||
|
||||
import shared.data as data
|
||||
from services.mpris import PlayerManager, PlayerService
|
||||
from shared.widgets.player_box_stack import BasePlayerBoxStack
|
||||
from utils.gtk_utils import svg_file
|
||||
|
||||
CACHE_DIR = f"{data.CACHE_DIR}/media"
|
||||
PLAYER_FALLBACK_ART = get_relative_path("../../assets/icons/music.svg")
|
||||
MPRIS_ICON_FALLBACK = "application-x-executable-symbolic"
|
||||
if not os.path.exists(CACHE_DIR):
|
||||
os.makedirs(CACHE_DIR)
|
||||
|
||||
_shared_mpris_manager = None
|
||||
|
||||
|
||||
def resolve_mpris_player_icon(player_name: str) -> str:
|
||||
"""Resolve MPRIS player icon from .desktop file."""
|
||||
raw = player_name or ""
|
||||
app_name = raw.removeprefix("org.mpris.MediaPlayer2.")
|
||||
|
||||
search_dirs = [
|
||||
os.path.join(os.path.expanduser("~/.local/share"), "applications"),
|
||||
"/usr/share/applications",
|
||||
"/usr/local/share/applications",
|
||||
]
|
||||
for d in os.environ.get("XDG_DATA_DIRS", "/usr/share:/usr/local/share").split(":"):
|
||||
p = os.path.join(d, "applications")
|
||||
if p not in search_dirs:
|
||||
search_dirs.append(p)
|
||||
|
||||
candidates = [app_name]
|
||||
parts = app_name.split("-")
|
||||
for i in range(len(parts) - 1, 0, -1):
|
||||
shorter = "-".join(parts[:i])
|
||||
if shorter not in candidates:
|
||||
candidates.append(shorter)
|
||||
|
||||
for base in search_dirs:
|
||||
for name in candidates:
|
||||
dp = os.path.join(base, name + ".desktop")
|
||||
if os.path.exists(dp):
|
||||
try:
|
||||
with open(dp) as f:
|
||||
for line in f:
|
||||
if line.startswith("Icon="):
|
||||
return line[5:].strip()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# No desktop file found — try app_name as direct icon name
|
||||
for name in candidates:
|
||||
if name:
|
||||
return name
|
||||
|
||||
return MPRIS_ICON_FALLBACK
|
||||
|
||||
|
||||
def get_shared_mpris_manager():
|
||||
global _shared_mpris_manager
|
||||
if _shared_mpris_manager is None:
|
||||
@@ -61,7 +105,7 @@ def apply_player_art(album_cover_widget, player, service=None, path=None):
|
||||
return None
|
||||
|
||||
|
||||
class PlayerBoxStack(Box):
|
||||
class PlayerBoxStack(BasePlayerBoxStack):
|
||||
def __init__(
|
||||
self, mpris_manager: PlayerManager = None, control_center=None, **kwargs
|
||||
):
|
||||
@@ -72,53 +116,12 @@ class PlayerBoxStack(Box):
|
||||
self._signal_connections = []
|
||||
self.no_media_box = self._create_no_media_box()
|
||||
|
||||
super().__init__(orientation="v", name="media", children=[self.player_stack])
|
||||
self.player_stack.children = [self.no_media_box]
|
||||
self.set_visible(True)
|
||||
self.mpris_manager = mpris_manager or get_shared_mpris_manager()
|
||||
self.connect("map", self._on_map)
|
||||
super().__init__(mpris_manager or get_shared_mpris_manager(), **kwargs)
|
||||
|
||||
if self.mpris_manager is not None:
|
||||
self._init_mpris_manager()
|
||||
|
||||
def _init_mpris_manager(self):
|
||||
try:
|
||||
connections = bulk_connect(
|
||||
self.mpris_manager,
|
||||
{
|
||||
"new-player": self.on_new_player,
|
||||
"player-vanish": self.on_lost_player,
|
||||
},
|
||||
)
|
||||
for handler_id in connections:
|
||||
self._signal_connections.append((self.mpris_manager, handler_id))
|
||||
|
||||
for name, player in self.mpris_manager.get_all_services().items():
|
||||
self.on_new_player(self.mpris_manager, name, player)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize PlayerBoxStack signals: {e}")
|
||||
|
||||
def _on_map(self, *_):
|
||||
self._check_and_update_playing_state()
|
||||
|
||||
def destroy(self):
|
||||
for obj, handler_id in self._signal_connections:
|
||||
try:
|
||||
obj.disconnect(handler_id)
|
||||
except Exception as e:
|
||||
logger.error(f"An error occurred: {e}")
|
||||
self._signal_connections.clear()
|
||||
super().destroy()
|
||||
|
||||
def suspend(self):
|
||||
for child in self.player_stack.get_children():
|
||||
if hasattr(child, "suspend"):
|
||||
child.suspend()
|
||||
|
||||
def resume(self):
|
||||
for child in self.player_stack.get_children():
|
||||
if hasattr(child, "resume"):
|
||||
child.resume()
|
||||
def _create_player_box_widget(self, player):
|
||||
return PlayerBox(
|
||||
player=player, player_stack=self, control_center=self.control_center
|
||||
)
|
||||
|
||||
def _create_no_media_box(self):
|
||||
fallback_cover_path = get_relative_path("../../assets/icons/music.svg")
|
||||
@@ -172,138 +175,6 @@ class PlayerBoxStack(Box):
|
||||
],
|
||||
)
|
||||
|
||||
def _find_playing_player_index(self):
|
||||
players = self.player_stack.get_children()
|
||||
for i, player_box in enumerate(players):
|
||||
if (
|
||||
hasattr(player_box, "player")
|
||||
and str(player_box.player.playback_status).lower() == "playing"
|
||||
):
|
||||
return i
|
||||
return None
|
||||
|
||||
def _check_and_update_playing_state(self):
|
||||
players = [
|
||||
p for p in self.player_stack.get_children() if p != self.no_media_box
|
||||
]
|
||||
if len(players) > 0:
|
||||
if self.no_media_box in self.player_stack.get_children():
|
||||
self.player_stack.remove(self.no_media_box)
|
||||
playing_index = self._find_playing_player_index()
|
||||
if playing_index is not None:
|
||||
self.on_player_clicked_by_index(playing_index)
|
||||
return
|
||||
|
||||
if (
|
||||
len(self.player_stack.get_children()) == 1
|
||||
and self.player_stack.get_children()[0] == self.no_media_box
|
||||
):
|
||||
return
|
||||
|
||||
self.player_stack.children = [self.no_media_box]
|
||||
self.current_stack_pos = 0
|
||||
self.player_stack.set_visible_child(self.no_media_box)
|
||||
|
||||
def on_player_playback_changed(self, player_box, status):
|
||||
status = str(status).lower()
|
||||
if status == "playing":
|
||||
players = self.player_stack.get_children()
|
||||
for i, pb in enumerate(players):
|
||||
if pb == player_box and i != self.current_stack_pos:
|
||||
self.on_player_clicked_by_index(i)
|
||||
break
|
||||
elif status in ["paused", "stopped"]:
|
||||
self._check_and_update_playing_state()
|
||||
|
||||
def _update_all_player_buttons(self):
|
||||
show_buttons = len(self.player_buttons) > 1
|
||||
for child in self.player_stack.get_children():
|
||||
if hasattr(child, "update_buttons"):
|
||||
child.update_buttons(self.player_buttons, show_buttons)
|
||||
|
||||
def on_player_clicked_by_index(self, index):
|
||||
if 0 <= index < len(self.player_buttons):
|
||||
if self.current_stack_pos < len(self.player_buttons):
|
||||
self.player_buttons[self.current_stack_pos].remove_style_class("active")
|
||||
self.current_stack_pos = index
|
||||
self.player_buttons[self.current_stack_pos].add_style_class("active")
|
||||
self.player_stack.set_visible_child(
|
||||
self.player_stack.get_children()[self.current_stack_pos]
|
||||
)
|
||||
self._update_all_player_buttons()
|
||||
|
||||
def on_new_player(self, mpris_manager, name, player):
|
||||
if (
|
||||
len(self.player_stack.get_children()) == 1
|
||||
and self.player_stack.get_children()[0] == self.no_media_box
|
||||
):
|
||||
self.player_stack.children = []
|
||||
self.current_stack_pos = 0
|
||||
|
||||
new_player_box = PlayerBox(
|
||||
player=player, player_stack=self, control_center=self.control_center
|
||||
)
|
||||
self.player_stack.children = [*self.player_stack.children, new_player_box]
|
||||
self.make_new_player_button(new_player_box)
|
||||
if self.player_buttons:
|
||||
self.player_buttons[self.current_stack_pos].set_style_classes(["active"])
|
||||
self._check_and_update_playing_state()
|
||||
self._update_all_player_buttons()
|
||||
|
||||
def on_lost_player(self, mpris_manager, bus_name):
|
||||
player_box_to_remove = None
|
||||
for player_box in self.player_stack.get_children():
|
||||
if (
|
||||
hasattr(player_box, "player")
|
||||
and getattr(player_box.player, "player_name", None) == bus_name
|
||||
):
|
||||
player_box_to_remove = player_box
|
||||
break
|
||||
|
||||
if player_box_to_remove:
|
||||
player_box_to_remove.destroy()
|
||||
|
||||
remaining_players = [
|
||||
p for p in self.player_stack.get_children() if p != player_box_to_remove
|
||||
]
|
||||
if len(remaining_players) == 0:
|
||||
self.player_stack.children = [self.no_media_box]
|
||||
self.current_stack_pos = 0
|
||||
self.player_buttons = []
|
||||
return
|
||||
|
||||
if self.current_stack_pos >= len(self.player_stack.get_children()):
|
||||
self.current_stack_pos = max(0, len(self.player_stack.get_children()) - 1)
|
||||
|
||||
if self.player_buttons and self.current_stack_pos < len(self.player_buttons):
|
||||
self.player_buttons[self.current_stack_pos].set_style_classes(["active"])
|
||||
self.player_stack.set_visible_child(
|
||||
self.player_stack.get_children()[self.current_stack_pos]
|
||||
)
|
||||
self._update_all_player_buttons()
|
||||
|
||||
def make_new_player_button(self, player_box):
|
||||
new_button = Button(name="player-stack-button")
|
||||
|
||||
def on_player_button_click(button: Button):
|
||||
if self.current_stack_pos < len(self.player_buttons):
|
||||
self.player_buttons[self.current_stack_pos].remove_style_class("active")
|
||||
if button in self.player_buttons:
|
||||
self.current_stack_pos = self.player_buttons.index(button)
|
||||
button.add_style_class("active")
|
||||
self.player_stack.set_visible_child(player_box)
|
||||
|
||||
new_button.connect("clicked", on_player_button_click)
|
||||
self.player_buttons.append(new_button)
|
||||
player_box.connect(
|
||||
"destroy",
|
||||
lambda *_: (
|
||||
self.player_buttons.remove(new_button)
|
||||
if new_button in self.player_buttons
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class PlayerBox(Box):
|
||||
def __init__(
|
||||
@@ -327,7 +198,7 @@ class PlayerBox(Box):
|
||||
overlays=[
|
||||
Box(
|
||||
children=Image(
|
||||
icon_name=self.player.player_name,
|
||||
icon_name=resolve_mpris_player_icon(self.player.player_name),
|
||||
name="player-app-icon",
|
||||
icon_size=20,
|
||||
),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from fabric.utils import Gdk, GLib, idle_add, logger
|
||||
from fabric.utils import GLib, idle_add, logger
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.centerbox import CenterBox
|
||||
@@ -9,6 +9,7 @@ from fabric.widgets.separator import Separator
|
||||
from services import dns as dns_service
|
||||
from services.network import NetworkClient
|
||||
from shared.dialogs.sysauth_dialog import run_auth_dialog
|
||||
from shared.widgets.pull_to_refresh import PullToRefreshMixin
|
||||
from shared.widgets.smooth_switch import SmoothSwitch
|
||||
from shared.window.animated_scrollwindow import AnimatedScrollable
|
||||
from utils.functions import (
|
||||
@@ -402,7 +403,7 @@ class DnsSwitcher:
|
||||
self._load_current_dns()
|
||||
|
||||
|
||||
class WifiConnections(Box):
|
||||
class WifiConnections(PullToRefreshMixin, Box):
|
||||
def __init__(self, parent, show_back_button=True, network_service=None, **kwargs):
|
||||
super().__init__(
|
||||
spacing=8,
|
||||
@@ -506,7 +507,7 @@ class WifiConnections(Box):
|
||||
self.other_networks.set_visible(False)
|
||||
|
||||
# Add pull-to-refresh functionality to scrolled window
|
||||
self.setup_pull_to_refresh()
|
||||
self.setup_pull_to_refresh(self.other_networks_scrolled)
|
||||
|
||||
# Create More Settings button (same style as Other Networks button)
|
||||
self.more_settings_button = Button(
|
||||
@@ -574,33 +575,6 @@ class WifiConnections(Box):
|
||||
# Defer refresh until after the height animation completes
|
||||
idle_add(self._refresh_after_animation)
|
||||
|
||||
def _cancel_pending_refresh(self):
|
||||
if hasattr(self, "_anim_finished_handler") and self._anim_finished_handler:
|
||||
try:
|
||||
self.other_networks_scrolled.height_animator.disconnect(
|
||||
self._anim_finished_handler
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"[wifi] self.other_networks_scrolled.height_animator.disconnect( ... failed: {e}"
|
||||
)
|
||||
self._anim_finished_handler = None
|
||||
|
||||
def _refresh_after_animation(self):
|
||||
if self._destroyed:
|
||||
return False
|
||||
anim = self.other_networks_scrolled.height_animator
|
||||
if anim.playing:
|
||||
self._anim_finished_handler = anim.connect(
|
||||
"finished",
|
||||
lambda *_: (
|
||||
self.force_network_refresh() if not self._destroyed else None
|
||||
),
|
||||
)
|
||||
else:
|
||||
self.force_network_refresh()
|
||||
return False
|
||||
|
||||
def on_network_ready(self, *_):
|
||||
"""Called when network service is ready"""
|
||||
if self._network_ready_fired and self.wifi_service:
|
||||
@@ -812,78 +786,18 @@ class WifiConnections(Box):
|
||||
except Exception as e:
|
||||
logger.error(f"[WiFi] Error during forced network refresh: {e}")
|
||||
|
||||
def setup_pull_to_refresh(self):
|
||||
"""Setup pull-to-refresh gesture for the scrolled window"""
|
||||
self.vadjustment = self.other_networks_scrolled.get_vadjustment()
|
||||
def _trigger_scan(self):
|
||||
if self.wifi_service:
|
||||
self.wifi_service.scan()
|
||||
|
||||
# Track gesture state
|
||||
self.pull_start_y = 0
|
||||
self.is_pulling = False
|
||||
self.pull_threshold = 50 # pixels to trigger refresh
|
||||
def _trigger_refresh(self):
|
||||
self.force_network_refresh()
|
||||
|
||||
# Connect to scroll events
|
||||
self.other_networks_scrolled.connect("scroll-event", self.on_scroll_event)
|
||||
self.other_networks_scrolled.connect("button-press-event", self.on_button_press)
|
||||
self.other_networks_scrolled.connect(
|
||||
"button-release-event", self.on_button_release
|
||||
)
|
||||
self.other_networks_scrolled.connect(
|
||||
"motion-notify-event", self.on_motion_notify
|
||||
)
|
||||
def _get_pull_normal_label(self):
|
||||
return "↓ Pull to scan for networks"
|
||||
|
||||
# Enable events
|
||||
self.other_networks_scrolled.set_events(
|
||||
Gdk.EventMask.SCROLL_MASK
|
||||
| Gdk.EventMask.BUTTON_PRESS_MASK
|
||||
| Gdk.EventMask.BUTTON_RELEASE_MASK
|
||||
| Gdk.EventMask.POINTER_MOTION_MASK
|
||||
)
|
||||
|
||||
def on_scroll_event(self, widget, event):
|
||||
"""Handle scroll events for pull-to-refresh"""
|
||||
if self.vadjustment.get_value() <= 0:
|
||||
if event.direction == Gdk.ScrollDirection.UP:
|
||||
if self.wifi_service:
|
||||
self.wifi_service.scan()
|
||||
self.force_network_refresh()
|
||||
return True
|
||||
return False
|
||||
|
||||
def on_button_press(self, widget, event):
|
||||
"""Handle button press for touch/drag gestures"""
|
||||
if self.vadjustment.get_value() <= 0:
|
||||
self.pull_start_y = event.y
|
||||
self.is_pulling = True
|
||||
return False
|
||||
|
||||
def on_button_release(self, widget, event):
|
||||
"""Handle button release for touch/drag gestures"""
|
||||
if self.is_pulling:
|
||||
pull_distance = event.y - self.pull_start_y
|
||||
if pull_distance > self.pull_threshold:
|
||||
if self.wifi_service:
|
||||
self.wifi_service.scan()
|
||||
self.force_network_refresh()
|
||||
self.refresh_indicator.set_visible(False)
|
||||
self.refresh_indicator.remove_style_class("ready-to-refresh")
|
||||
self.is_pulling = False
|
||||
return False
|
||||
|
||||
def on_motion_notify(self, widget, event):
|
||||
"""Handle motion events for visual feedback during pull"""
|
||||
if self.is_pulling and self.vadjustment.get_value() <= 0:
|
||||
pull_distance = event.y - self.pull_start_y
|
||||
if pull_distance > 0:
|
||||
self.refresh_indicator.set_visible(True)
|
||||
if pull_distance >= self.pull_threshold:
|
||||
self.refresh_indicator.set_label("↑ Release to scan")
|
||||
self.refresh_indicator.add_style_class("ready-to-refresh")
|
||||
else:
|
||||
self.refresh_indicator.set_label("↓ Pull to scan for networks")
|
||||
self.refresh_indicator.remove_style_class("ready-to-refresh")
|
||||
else:
|
||||
self.refresh_indicator.set_visible(False)
|
||||
return False
|
||||
def _get_pull_ready_label(self):
|
||||
return "↑ Release to scan"
|
||||
|
||||
def on_destroy(self, widget):
|
||||
"""Cleanup when widget is destroyed"""
|
||||
|
||||
Reference in new issue
Block a user