import React, { useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { Cast, Globe, Heart, Square, Trash2, X } from 'lucide-react'; import { open } from '@tauri-apps/plugin-shell'; import type { InternetRadioStation } from '../../api/subsonicTypes'; import { useDragDrop, useDragSource } from '../../contexts/DragDropContext'; import { CoverArtImage } from '../../cover/CoverArtImage'; import { albumCoverRef } from '../../cover/ref'; import { coverArtIdFromRadio } from '../../cover/ids'; import { COVER_DENSE_GRID_MIN_CELL_CSS_PX } from '../../cover/layoutSizes'; interface RadioCardProps { s: InternetRadioStation; isActive: boolean; isPlaying: boolean; 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; onEdit: () => void; onFavoriteToggle: () => void; onDragEnter: (side: 'before' | 'after') => void; onDragLeave: () => void; onDropOnto: (srcId: string, side: 'before' | 'after') => void; onCardMouseLeave: () => void; } export default function RadioCard({ s, isActive, isPlaying, deleteConfirmId, isFavorite, isManual, canManage, dropIndicator, onPlay, onDelete, onEdit, onFavoriteToggle, onDragEnter, onDragLeave, onDropOnto, onCardMouseLeave, }: RadioCardProps) { const { t } = useTranslation(); const cardRef = useRef(null); const lastSideRef = useRef<'before' | 'after'>('after'); const { isDragging, payload } = useDragDrop(); const isBeingDragged = isDragging && !!payload && (() => { try { return JSON.parse(payload.data).id === s.id; } catch { return false; } })(); const dragHandlers = useDragSource(() => ({ data: JSON.stringify({ type: 'radio', id: s.id }), label: s.name, })); // Calculate which half of the card the cursor is on const getSide = (e: React.MouseEvent): 'before' | 'after' => { const rect = cardRef.current?.getBoundingClientRect(); if (!rect) return 'after'; return e.clientX < rect.left + rect.width / 2 ? 'before' : 'after'; }; // psy-drop listener: fires when a drag is released over this card useEffect(() => { if (!isManual) return; const el = cardRef.current; if (!el) return; const handler = (e: Event) => { const data = JSON.parse((e as CustomEvent).detail?.data ?? '{}'); if (data.type === 'radio' && data.id !== s.id) onDropOnto(data.id, lastSideRef.current); }; el.addEventListener('psy-drop', handler); return () => el.removeEventListener('psy-drop', handler); }, [isManual, s.id, onDropOnto]); return (
{ if (!isDragging || !isManual) return; const side = getSide(e); lastSideRef.current = side; onDragEnter(side); }} onMouseLeave={() => { onDragLeave(); onCardMouseLeave(); }} > {/* Cover */}
{s.coverArt ? ( ) : (
)} {isActive && isPlaying && (
{t('radio.live')}
)}
{canManage && (
)}
{/* Info */}
{s.name}
{canManage && ( )} {s.homepageUrl && ( )}
); }