diff --git a/pkg/service/soundtouchweb/static/css/app.css b/pkg/service/soundtouchweb/static/css/app.css index db8f554..7bf000a 100644 --- a/pkg/service/soundtouchweb/static/css/app.css +++ b/pkg/service/soundtouchweb/static/css/app.css @@ -219,6 +219,26 @@ img { display: block; max-width: 100%; } .volume-slider { flex: 1; accent-color: var(--accent); } .volume-value { font-size: .8rem; color: var(--text-dim); min-width: 2.5ch; text-align: right; } +.bass-row { display: flex; align-items: center; gap: .75rem; margin-top: .5rem; } +.bass-label { font-size: .8rem; color: var(--text-dim); width: 2.5ch; } + +/* ── Progress bar ────────────────────────────────────────────────────────── */ +.progress-row { margin-top: .35rem; display: flex; align-items: center; gap: .5rem; } +.progress-bar { + flex: 1; + height: 3px; + background: var(--border); + border-radius: 2px; + overflow: hidden; +} +.progress-fill { + height: 100%; + background: var(--accent); + border-radius: 2px; + transition: width .9s linear; +} +.progress-time { font-size: .7rem; color: var(--text-dim); white-space: nowrap; flex-shrink: 0; } + /* ── Presets ─────────────────────────────────────────────────────────────── */ .presets-section, .sources-section { margin-top: 1.25rem; } .section-title { font-size: .8rem; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; color: var(--text-dim); margin-bottom: .6rem; } diff --git a/pkg/service/soundtouchweb/static/js/api.js b/pkg/service/soundtouchweb/static/js/api.js index 42fb05c..7caf130 100644 --- a/pkg/service/soundtouchweb/static/js/api.js +++ b/pkg/service/soundtouchweb/static/js/api.js @@ -1,3 +1,5 @@ +const JSON_HEADERS = { 'Content-Type': 'application/json' }; + async function req(url, opts = {}) { const r = await fetch(url, opts); return r.json(); @@ -9,6 +11,11 @@ export const api = { discover: () => req('/api/discover', { method: 'POST' }), key: (id, key) => req(`/api/device-key/${id}/${key}`, { method: 'POST' }), volume: (id, level) => req(`/api/device-volume/${id}/${level}`, { method: 'POST' }), + bass: (id, level) => req(`/api/control/${id}/bass`, { + method: 'POST', + headers: JSON_HEADERS, + body: JSON.stringify({ level }), + }), power: (id) => req(`/api/device-power/${id}`, { method: 'POST' }), tuneInBrowse: (path) => req(path ? `/api/tunein/navigate/${path}` : '/api/tunein/navigate'), tuneInSearch: (q) => req(`/api/tunein/search?q=${encodeURIComponent(q)}`), @@ -16,7 +23,7 @@ export const api = { selectSource: (id, source, account) => req(`/api/control/${id}/source?name=${encodeURIComponent(source)}&account=${encodeURIComponent(account || '')}`), tuneInPlay: (deviceId, item) => req(`/api/tunein/play/${deviceId}`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: JSON_HEADERS, body: JSON.stringify(item), }), }; \ No newline at end of file diff --git a/pkg/service/soundtouchweb/static/js/components/Controls.js b/pkg/service/soundtouchweb/static/js/components/Controls.js index 7541c9a..cacbd52 100644 --- a/pkg/service/soundtouchweb/static/js/components/Controls.js +++ b/pkg/service/soundtouchweb/static/js/components/Controls.js @@ -10,12 +10,16 @@ export function Controls({ deviceId, status }) { const isPlaying = np?.PlayStatus === 'PLAY_STATE'; const actualVolume = status?.volume?.ActualVolume ?? 0; const isMuted = status?.volume?.MuteEnabled ?? false; - const skipEnabled = np?.SkipEnabled != null; - const skipPrevEnabled = np?.SkipPreviousEnabled != null; + const shuffle = np?.ShuffleSetting ?? 'SHUFFLE_OFF'; + const repeat = np?.RepeatSetting ?? 'REPEAT_OFF'; + const actualBass = status?.bass?.TargetBass ?? 0; + const hasBass = status?.bass != null; const [localVolume, setLocalVolume] = useState(actualVolume); + const [localBass, setLocalBass] = useState(actualBass); useEffect(() => { setLocalVolume(actualVolume); }, [actualVolume]); + useEffect(() => { setLocalBass(actualBass); }, [actualBass]); const send = (key) => api.key(deviceId, key); @@ -25,6 +29,24 @@ export function Controls({ deviceId, status }) { api.volume(deviceId, val); } + function onBassChange(e) { + const val = parseInt(e.target.value, 10); + setLocalBass(val); + api.bass(deviceId, val); + } + + function toggleShuffle() { + send(shuffle === 'SHUFFLE_ON' ? 'SHUFFLE_OFF' : 'SHUFFLE_ON'); + } + + function cycleRepeat() { + if (repeat === 'REPEAT_OFF') send('REPEAT_ALL'); + else if (repeat === 'REPEAT_ALL') send('REPEAT_ONE'); + else send('REPEAT_OFF'); + } + + const repeatIcon = repeat === 'REPEAT_ONE' ? '🔂' : '🔁'; + return html`