From 1270eed554c646cf4c6521554b57e321dc2a20cb Mon Sep 17 00:00:00 2001 From: Tobias Gesellchen Date: Fri, 17 Jul 2026 17:46:53 +0200 Subject: [PATCH] feat(player): add name/IP device sort toggle (#571) (#576) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Relates to #571. ## What Adds a **Name / IP** sort toggle to the Player device list. The choice is persisted in `localStorage` (`aftertouch_device_sort`), following the same preference pattern as the service-URL field in `PlayURL.js`. ## Why The device list was previously ordered only by IP: the service datastore keys devices by IP address and Go marshals map keys lexicographically, so the frontend received an already-IP-ordered object and rendered it as-is. BirdyBA (#571) asked to be able to sort by name instead. ## Changes - `DeviceList.js`: a `sortEntries()` helper plus a `useState`-backed toggle seeded from `localStorage`. Name mode sorts by `device.info.name` (falling back to the IP key when a device has no name yet); IP mode sorts the IP key **numerically** (`.2` before `.10`), which also tidies the old lexicographic ordering. - `css/app.css`: additive `.device-sort` / `.sort-btn` styling, reusing the existing accent / `.active` look. No existing rules touched. No backend change: the device name and IP are already in the payload. ## Testing - `node --check` on `DeviceList.js` passes. - `make build-player` succeeds (the static tree is `//go:embed`ed into the binary). - Manual: open the Player, toggle Name / IP, confirm the order changes and the choice survives a page reload. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) --- pkg/service/soundtouchweb/static/css/app.css | 24 +++++++++++++ .../static/js/components/DeviceList.js | 34 ++++++++++++++++++- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/pkg/service/soundtouchweb/static/css/app.css b/pkg/service/soundtouchweb/static/css/app.css index f4fa05e..9f2daf4 100644 --- a/pkg/service/soundtouchweb/static/css/app.css +++ b/pkg/service/soundtouchweb/static/css/app.css @@ -324,6 +324,30 @@ img { display: block; max-width: 100%; } } .back-btn:hover { background: var(--bg); } +/* ── Device sort toggle ──────────────────────────────────────────────────── */ +.device-sort { + display: flex; + align-items: center; + gap: .4rem; + margin-bottom: 1rem; +} +.device-sort-label { + font-size: .85rem; + opacity: .65; + margin-right: .1rem; +} +.sort-btn { + padding: .25rem .7rem; + border: 1px solid var(--border); + border-radius: 999px; + background: var(--surface); + color: inherit; + font-size: .85rem; + cursor: pointer; +} +.sort-btn:hover { background: var(--bg); } +.sort-btn.active { background: var(--accent); color: var(--accent-fg); border-color: var(--accent); } + /* ── Device grid ─────────────────────────────────────────────────────────── */ .device-grid { display: grid; diff --git a/pkg/service/soundtouchweb/static/js/components/DeviceList.js b/pkg/service/soundtouchweb/static/js/components/DeviceList.js index 2a69517..4785300 100644 --- a/pkg/service/soundtouchweb/static/js/components/DeviceList.js +++ b/pkg/service/soundtouchweb/static/js/components/DeviceList.js @@ -1,8 +1,26 @@ import { h } from 'preact'; +import { useState } from 'preact/hooks'; import htm from 'htm'; const html = htm.bind(h); +const SORT_LS_KEY = 'aftertouch_device_sort'; + +function sortEntries(entries, mode) { + const copy = [...entries]; + if (mode === 'name') { + // Sort by the speaker's display name, falling back to the map key (its IP) + // when a device has no name yet. + copy.sort(([idA, a], [idB, b]) => + (a?.info?.name || idA).localeCompare(b?.info?.name || idB, undefined, { sensitivity: 'base' })); + } else { + // Default: by IP (the map key), ordered numerically so .2 precedes .10. + copy.sort(([idA], [idB]) => + idA.localeCompare(idB, undefined, { numeric: true, sensitivity: 'base' })); + } + return copy; +} + function DeviceCard({ id, device, onSelect, onRemove }) { const { info, status } = device; const np = status?.nowPlaying; @@ -37,7 +55,14 @@ function DeviceCard({ id, device, onSelect, onRemove }) { } export function DeviceList({ devices, isDiscovering, onSelect, onDiscover, onRemove }) { - const entries = Object.entries(devices); + const [sortMode, setSortMode] = useState(() => localStorage.getItem(SORT_LS_KEY) || 'ip'); + + function changeSort(mode) { + setSortMode(mode); + localStorage.setItem(SORT_LS_KEY, mode); + } + + const entries = sortEntries(Object.entries(devices), sortMode); return html`
@@ -51,6 +76,13 @@ export function DeviceList({ devices, isDiscovering, onSelect, onDiscover, onRem
` : html` +
+ Sort by + + +
${entries.map(([id, device]) => html` <${DeviceCard} key=${id} id=${id} device=${device} onSelect=${onSelect} onRemove=${onRemove} />