mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
feat: now-playing liveness dot + admin-gated radio management (#1086)
* feat(now-playing): liveness indicator dot in the listening popover Replace the raw "Nm ago" line in the "Who is listening?" popover with a derived presence dot (green playing / amber paused / dim idle). The presence is computed in one tested helper that unifies the playbackReport transport state with the legacy getNowPlaying recency, instead of formatting a raw timestamp inline. The dot carries the localized status as an aria-label and tooltip so it is not conveyed by colour alone. * feat(radio): gate station create/edit/delete behind Navidrome admin role Navidrome >= 0.62 restricts internet-radio management to admins (GHSA-jw24-qqrj-633c); non-admin requests fail. Hide Add Station, Search Directory, the per-card edit chip and delete button for confirmed standard Navidrome users via a canManageNavidromeRadio() helper on the existing useNavidromeAdminRole framework. Admins, non-Navidrome servers and transient states stay unrestricted; playback and favourites remain available to all. * docs(changelog): now-playing status dot + admin-gated radio (#1086)
This commit is contained in:
@@ -16,6 +16,8 @@ interface RadioCardProps {
|
||||
deleteConfirmId: string | null;
|
||||
isFavorite: boolean;
|
||||
isManual: boolean;
|
||||
/** Navidrome ≥ 0.62 only lets admins manage stations — hides edit/delete. */
|
||||
canManage: boolean;
|
||||
dropIndicator: 'before' | 'after' | null;
|
||||
onPlay: (e: React.MouseEvent) => void;
|
||||
onDelete: (e: React.MouseEvent) => void;
|
||||
@@ -28,7 +30,7 @@ interface RadioCardProps {
|
||||
}
|
||||
|
||||
export default function RadioCard({
|
||||
s, isActive, isPlaying, deleteConfirmId, isFavorite, isManual, dropIndicator,
|
||||
s, isActive, isPlaying, deleteConfirmId, isFavorite, isManual, canManage, dropIndicator,
|
||||
onPlay, onDelete, onEdit, onFavoriteToggle, onDragEnter, onDragLeave,
|
||||
onDropOnto, onCardMouseLeave,
|
||||
}: RadioCardProps) {
|
||||
@@ -117,25 +119,29 @@ export default function RadioCard({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="playlist-card-actions">
|
||||
<button
|
||||
className={`playlist-card-action playlist-card-action--delete ${deleteConfirmId === s.id ? 'playlist-card-action--delete-confirm' : ''}`}
|
||||
onClick={onDelete}
|
||||
data-tooltip={deleteConfirmId === s.id ? t('radio.confirmDelete') : t('radio.deleteStation')}
|
||||
data-tooltip-pos="bottom"
|
||||
>
|
||||
{deleteConfirmId === s.id ? <Trash2 size={12} /> : <X size={12} />}
|
||||
</button>
|
||||
</div>
|
||||
{canManage && (
|
||||
<div className="playlist-card-actions">
|
||||
<button
|
||||
className={`playlist-card-action playlist-card-action--delete ${deleteConfirmId === s.id ? 'playlist-card-action--delete-confirm' : ''}`}
|
||||
onClick={onDelete}
|
||||
data-tooltip={deleteConfirmId === s.id ? t('radio.confirmDelete') : t('radio.deleteStation')}
|
||||
data-tooltip-pos="bottom"
|
||||
>
|
||||
{deleteConfirmId === s.id ? <Trash2 size={12} /> : <X size={12} />}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div className="album-card-info">
|
||||
<div className="album-card-title">{s.name}</div>
|
||||
<div className="album-card-artist" style={{ display: 'flex', gap: '0.35rem', alignItems: 'center' }}>
|
||||
<button className="radio-card-chip" onClick={onEdit}>
|
||||
{t('radio.editStation')}
|
||||
</button>
|
||||
{canManage && (
|
||||
<button className="radio-card-chip" onClick={onEdit}>
|
||||
{t('radio.editStation')}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className={`player-btn player-btn-sm radio-favorite-btn${isFavorite ? ' active' : ''}`}
|
||||
onClick={e => { e.stopPropagation(); onFavoriteToggle(); }}
|
||||
|
||||
Reference in New Issue
Block a user