mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-24 14:47:23 +00:00
#500: the absolutely-centered page title and the right-aligned icon bar shared the same space in the fixed-height navbar and overlapped on phones (portrait). On <=600px the navbar now wraps into two rows: row 1 keeps the logo with the title beside it (the title fills the remaining width and ellipsizes), and the icon bar drops onto its own centered, full-width row below. CSS-only. #498: the zone/grouping UI showed raw IP addresses instead of device names. Root cause was a field-name casing bug: Zone.js read info.Name (uppercase), but the device info field is info.name (lowercase) everywhere else in the UI (app.js, DeviceList, Library, TTS, ...). So the lookup always missed and fell back to the IP. Fixed the casing in the deviceName() helper, and made the "Add to zone" picker show the device name with the IP as a smaller secondary line (reusing the .picker-device-info/name/ip pattern the other pickers already use). Member/master rows resolve names via deviceName(). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
121 lines
4.7 KiB
JavaScript
121 lines
4.7 KiB
JavaScript
import { h } from 'preact';
|
|
import { useState, useEffect } from 'preact/hooks';
|
|
import htm from 'htm';
|
|
import { api } from '../api.js';
|
|
|
|
const html = htm.bind(h);
|
|
|
|
export function Zone({ deviceId, devices }) {
|
|
const [zone, setZone] = useState(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [showPicker, setShowPicker] = useState(false);
|
|
|
|
function refresh() {
|
|
api.zone(deviceId).then(resp => {
|
|
if (resp.success) setZone(resp.data);
|
|
}).finally(() => setLoading(false));
|
|
}
|
|
|
|
useEffect(() => { refresh(); }, [deviceId]);
|
|
|
|
async function addDevice(slaveId) {
|
|
setShowPicker(false);
|
|
await api.zoneAdd(deviceId, slaveId);
|
|
refresh();
|
|
}
|
|
|
|
async function removeDevice(slaveId) {
|
|
await api.zoneRemove(deviceId, slaveId);
|
|
refresh();
|
|
}
|
|
|
|
async function dissolve() {
|
|
await api.zoneDissolve(deviceId);
|
|
refresh();
|
|
}
|
|
|
|
async function leave() {
|
|
await api.zoneLeave(deviceId);
|
|
refresh();
|
|
}
|
|
|
|
if (loading) return html`
|
|
<div class="zone-section">
|
|
<div class="section-title">Zone</div>
|
|
<div class="loading-bar"></div>
|
|
</div>
|
|
`;
|
|
|
|
if (!zone) return null;
|
|
|
|
// Devices not already in the zone are available to add
|
|
const zoneIps = new Set([zone.masterIp, ...(zone.members || []).map(m => m.ip)].filter(Boolean));
|
|
const available = Object.entries(devices || {}).filter(([ip]) => !zoneIps.has(ip));
|
|
|
|
const deviceName = (ip) => devices[ip]?.info?.name || ip;
|
|
|
|
return html`
|
|
<div class="zone-section">
|
|
<div class="section-title">Zone</div>
|
|
|
|
${zone.isStandalone && html`
|
|
<div class="zone-row">
|
|
<span class="zone-status-label">Standalone</span>
|
|
${available.length > 0 && html`
|
|
<button class="btn-secondary zone-btn" onClick=${() => setShowPicker(true)}>+ Group with…</button>
|
|
`}
|
|
</div>
|
|
`}
|
|
|
|
${zone.isMaster && html`
|
|
<div class="zone-members">
|
|
<div class="zone-member zone-master-row">
|
|
<span class="zone-badge master">Master</span>
|
|
<span class="zone-member-name">${deviceName(deviceId)}</span>
|
|
</div>
|
|
${(zone.members || []).map(m => html`
|
|
<div class="zone-member" key=${m.ip}>
|
|
<span class="zone-badge slave">Member</span>
|
|
<span class="zone-member-name">${m.name || deviceName(m.ip)}</span>
|
|
<button class="btn-icon zone-remove" title="Remove from zone"
|
|
onClick=${() => removeDevice(m.ip)}>✕</button>
|
|
</div>
|
|
`)}
|
|
<div class="zone-actions">
|
|
${available.length > 0 && html`
|
|
<button class="btn-secondary zone-btn" onClick=${() => setShowPicker(true)}>+ Add speaker</button>
|
|
`}
|
|
<button class="btn-secondary zone-btn" onClick=${dissolve}>Dissolve zone</button>
|
|
</div>
|
|
</div>
|
|
`}
|
|
|
|
${zone.isSlave && html`
|
|
<div class="zone-row">
|
|
<span class="zone-badge slave">Member</span>
|
|
<span class="zone-member-name">Zone: ${zone.masterName || deviceName(zone.masterIp)}</span>
|
|
<button class="btn-secondary zone-btn" onClick=${leave}>Leave zone</button>
|
|
</div>
|
|
`}
|
|
|
|
${showPicker && html`
|
|
<div class="overlay" onClick=${() => setShowPicker(false)}>
|
|
<div class="device-picker" onClick=${e => e.stopPropagation()}>
|
|
<div class="picker-title">Add to zone</div>
|
|
<div class="picker-devices">
|
|
${available.map(([ip, d]) => html`
|
|
<button class="picker-device-btn" key=${ip} onClick=${() => addDevice(ip)}>
|
|
<div class="picker-device-info">
|
|
<span class="picker-device-name">${d.info?.name || ip}</span>
|
|
<span class="picker-device-ip">${d.info?.ip_address || ip}</span>
|
|
</div>
|
|
</button>
|
|
`)}
|
|
</div>
|
|
<button class="btn-secondary picker-cancel" onClick=${() => setShowPicker(false)}>Cancel</button>
|
|
</div>
|
|
</div>
|
|
`}
|
|
</div>
|
|
`;
|
|
} |