Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ classifiers = [
"Programming Language :: Python :: 3.13",
]
dependencies = [
"aiosendspin[server]~=5.2",
"aiosendspin[server]~=5.3",
"aiosendspin-mpris~=2.1.1",
"av>=15.0.0",
"numpy>=1.26.0",
Expand Down
5 changes: 5 additions & 0 deletions sendspin/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ class ClientSettings(BaseSettings):
# IP address of the network interface to use for mDNS discovery and (in daemon
# server-initiated mode) for binding the incoming-connection listener.
interface: str | None = None
# User-selected color theme ("dark" or "light"), re-engaged on next palette.
color_mode: str | None = None

def update(
self,
Expand All @@ -152,6 +154,7 @@ def update(
visualizer: bool | None = None,
last_played_server_id: str | None = None,
interface: str | None = None,
color_mode: str | None = None,
) -> None:
"""Update settings fields. Only changed fields trigger a save."""
changed = False
Expand Down Expand Up @@ -184,6 +187,7 @@ def update(
"visualizer": visualizer,
"last_played_server_id": last_played_server_id,
"interface": interface,
"color_mode": color_mode,
}
)
or changed
Expand Down Expand Up @@ -226,6 +230,7 @@ def _load(self) -> bool:
self.product_name = data.get("product_name")
self.last_played_server_id = data.get("last_played_server_id")
self.interface = data.get("interface")
self.color_mode = data.get("color_mode")
logger.info(
"Loaded settings from %s: volume=%d%%, muted=%s",
self._settings_file,
Expand Down
19 changes: 17 additions & 2 deletions sendspin/tui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
from sendspin.hooks import run_hook
from sendspin.settings import ClientSettings
from sendspin.tui.keyboard import keyboard_loop
from sendspin.tui.ui import SendspinUI
from sendspin.tui.ui import ColorMode, SendspinUI
from sendspin.utils import create_task, get_device_info
from sendspin.visualizer_connector import VisualizerHandler

Expand Down Expand Up @@ -284,7 +284,7 @@ def _build_visualizer_support() -> ClientHelloVisualizerSupport:
def _create_client(self) -> SendspinClient:
"""Create a new SendspinClient with roles based on current visualizer state."""
args = self._args
roles = [Roles.CONTROLLER, Roles.PLAYER, Roles.METADATA]
roles = [Roles.CONTROLLER, Roles.PLAYER, Roles.METADATA, Roles.COLOR]
visualizer_support = None
if self._visualizer_enabled:
visualizer_support = self._build_visualizer_support()
Expand Down Expand Up @@ -322,6 +322,7 @@ def _attach_client(self) -> None:
self._client.add_group_update_listener(self._handle_group_update),
self._client.add_controller_state_listener(self._handle_server_state),
self._client.add_server_command_listener(self._handle_server_command),
self._client.add_color_listener(self._handle_color_update),
]
self._audio_handler.attach_client(self._client)

Expand All @@ -343,6 +344,9 @@ def _detach_client(self) -> None:
unsub()
self._listener_unsubscribes = []
self._audio_handler.detach_client()
# Drop the per-server palette so a non-color server reverts to unthemed.
if self._ui is not None:
self._ui.reset_palette()

if self._visualizer_handler:
self._visualizer_handler.detach()
Expand Down Expand Up @@ -411,6 +415,8 @@ def request_shutdown() -> None:
player_muted=self._audio_handler.muted,
use_external_volume=self._audio_handler.uses_external_volume_controller,
visualizer_enabled=self._visualizer_enabled,
color_mode=ColorMode.parse(self._settings.color_mode),
on_color_mode_change=self._persist_color_mode,
)
self._ui.start()
self._ui.add_event(f"Using client ID: {args.client_id}")
Expand Down Expand Up @@ -712,6 +718,15 @@ def _handle_metadata_update(self, payload: ServerStatePayload) -> None:
ui.set_repeat_shuffle(state.repeat_mode, state.shuffle)
ui.add_event(state.describe())

def _handle_color_update(self, payload: ServerStatePayload) -> None:
"""Forward a color@v1 palette payload to the UI."""
assert self._ui is not None
self._ui.update_palette(payload.color)

def _persist_color_mode(self, mode: ColorMode) -> None:
"""Persist the user's color theme choice."""
self._settings.update(color_mode=mode.value)

def _handle_group_update(self, payload: GroupUpdateServerPayload) -> None:
"""Handle group update messages."""
assert self._ui is not None
Expand Down
1 change: 1 addition & 0 deletions sendspin/tui/keyboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ async def keyboard_loop(
"r": ("repeat", handler.cycle_repeat),
"x": ("shuffle", handler.toggle_shuffle),
"v": ("visualizer", on_toggle_visualizer),
"t": ("theme", ui.cycle_color_mode),
# Delay adjustment
",": ("delay-", lambda: handler.adjust_delay(-10)),
".": ("delay+", lambda: handler.adjust_delay(10)),
Expand Down
Loading