mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-19 09:06:14 +00:00
feat(player): render announcement banners in soundtouch-player too
The design's three-area target model (chooser/app/admin) and the backend (HandleListAnnouncements' target=app filtering) already supported this, but nothing in soundtouch-player's frontend called it — chunk 5 only wired the admin UI. New Announcements Preact component (static/js/components/), mounted in App() above the main content so it's visible across every page, styled with the app's existing CSS variables (dark-mode-aware, unlike the admin UI's hardcoded inline colors). Currently renders nothing, since no announcement in the list targets "app" yet (only the admin-gate notice, targeting "admin") — this is just closing the parity gap so a future app-targeted announcement has somewhere to show up. Verified end-to-end against a running instance: the component is served under /app/static/js/components/, app.js references it, and /api/announcements?target=app responds correctly (empty today). Refs #419
This commit is contained in:
@@ -911,3 +911,34 @@ img { display: block; max-width: 100%; }
|
||||
animation: fade-in .2s ease;
|
||||
}
|
||||
@keyframes fade-in { from { opacity: 0; transform: translateX(-50%) translateY(8px); } }
|
||||
|
||||
/* ── Announcements ───────────────────────────────────────────────────────── */
|
||||
.announcements-banner {
|
||||
padding: .5rem 1rem 0;
|
||||
}
|
||||
.announcement {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: .75rem;
|
||||
padding: .6rem .9rem;
|
||||
margin-bottom: .5rem;
|
||||
border-radius: var(--radius);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
font-size: .85rem;
|
||||
}
|
||||
.announcement-dismiss {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1;
|
||||
color: var(--text-dim);
|
||||
flex-shrink: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.announcement-dismiss:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { RadioBrowser } from './components/RadioBrowser.js';
|
||||
import { Library } from './components/Library.js';
|
||||
import { PlayURL } from './components/PlayURL.js';
|
||||
import { TTS } from './components/TTS.js';
|
||||
import { Announcements } from './components/Announcements.js';
|
||||
import { api } from './api.js';
|
||||
|
||||
const html = htm.bind(h);
|
||||
@@ -233,6 +234,8 @@ function App() {
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<${Announcements} />
|
||||
|
||||
<main class="main-content">
|
||||
${page === 'devices' ? html`
|
||||
<${DeviceList}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { h } from 'preact';
|
||||
import { useState, useEffect } from 'preact/hooks';
|
||||
import htm from 'htm';
|
||||
|
||||
const html = htm.bind(h);
|
||||
|
||||
// Announcements renders dismissible banners for the player (target=app),
|
||||
// mirroring the admin UI's banner (pkg/service/handlers/web/js/script.js,
|
||||
// fetchAnnouncements/dismissAnnouncement) against the same backend endpoints
|
||||
// added for #419 — see _/i419/design-admin-area-auth-gate.md. Dismissal is
|
||||
// recorded server-side (not a client-only localStorage flag), so it stays
|
||||
// dismissed across sessions/devices.
|
||||
export function Announcements() {
|
||||
const [announcements, setAnnouncements] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/announcements?target=app')
|
||||
.then(res => res.ok ? res.json() : { announcements: [] })
|
||||
.then(data => setAnnouncements(data.announcements || []))
|
||||
.catch(err => console.error('Failed to fetch announcements:', err));
|
||||
}, []);
|
||||
|
||||
async function dismiss(id) {
|
||||
try {
|
||||
await fetch(`/api/announcements/${encodeURIComponent(id)}/dismiss`, { method: 'POST' });
|
||||
} catch (err) {
|
||||
console.error('Failed to dismiss announcement:', err);
|
||||
}
|
||||
setAnnouncements(prev => prev.filter(a => a.id !== id));
|
||||
}
|
||||
|
||||
if (announcements.length === 0) return null;
|
||||
|
||||
return html`
|
||||
<div class="announcements-banner">
|
||||
${announcements.map(a => html`
|
||||
<div class="announcement announcement-${a.level || 'info'}" key=${a.id}>
|
||||
<span>${a.message}</span>
|
||||
<button class="announcement-dismiss" onClick=${() => dismiss(a.id)} title="Dismiss">×</button>
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
Reference in New Issue
Block a user