Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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