Enhance AudioPlayer with custom paginator styles, improved scroll handling, and WebSocket integration for real-time track updates. Refactor typeahead and queue management functionalities for better performance and user experience.

This commit is contained in:
2025-09-26 11:36:33 -04:00
parent c034a0c4ff
commit 153f50f774

View File

@@ -15,6 +15,29 @@ import { requireAuthHook } from "@/hooks/requireAuthHook";
import { useHtmlThemeAttr } from "@/hooks/useHtmlThemeAttr"; import { useHtmlThemeAttr } from "@/hooks/useHtmlThemeAttr";
import "@/components/TRip/RequestManagement.css"; import "@/components/TRip/RequestManagement.css";
// Custom styles for paginator
const paginatorStyles = `
.queue-paginator .p-paginator-current {
background: transparent !important;
color: inherit !important;
border: none !important;
}
.dark .queue-paginator .p-paginator-current {
color: rgb(212 212 212) !important;
background: transparent !important;
}
.queue-paginator .p-paginator-bottom {
padding: 16px !important;
border-top: 1px solid rgb(229 229 229) !important;
}
.dark .queue-paginator .p-paginator-bottom {
border-top-color: rgb(82 82 82) !important;
}
`;
const STATIONS = { const STATIONS = {
main: { label: "Main" }, main: { label: "Main" },
rock: { label: "Rock" }, rock: { label: "Rock" },
@@ -25,61 +48,79 @@ const STATIONS = {
export default function Player({ user }) { export default function Player({ user }) {
// Inject custom paginator styles
useEffect(() => {
const styleId = 'queue-paginator-styles';
if (!document.getElementById(styleId)) {
const style = document.createElement('style');
style.id = styleId;
style.textContent = paginatorStyles;
document.head.appendChild(style);
}
}, []);
const [isQueueVisible, setQueueVisible] = useState(false); const [isQueueVisible, setQueueVisible] = useState(false);
// Mouse wheel scroll fix for queue modal // Mouse wheel scroll fix for queue modal
useEffect(() => { useEffect(() => {
if (!isQueueVisible) return; if (!isQueueVisible) return;
setTimeout(() => {
const modalContent = document.querySelector('.p-dialog .p-dialog-content > div'); const setupScrollHandlers = () => {
if (modalContent) { // Target multiple possible scroll containers
modalContent.style.overflowY = 'auto'; const selectors = [
modalContent.style.overscrollBehavior = 'contain'; '.p-dialog .p-dialog-content',
'.p-dialog .p-datatable-wrapper',
'.p-dialog .p-datatable-scrollable-body',
'.p-dialog .p-datatable-tbody'
];
const elements = selectors
.map(selector => document.querySelector(selector))
.filter(Boolean);
// Also allow scrolling on the entire dialog
const dialog = document.querySelector('.p-dialog');
if (dialog) elements.push(dialog);
elements.forEach(element => {
element.style.overflowY = 'auto';
element.style.overscrollBehavior = 'contain';
const wheelHandler = (e) => { const wheelHandler = (e) => {
const delta = e.deltaY; e.stopPropagation();
const atTop = modalContent.scrollTop === 0; // Allow normal scrolling within the modal
const atBottom = modalContent.scrollTop + modalContent.clientHeight >= modalContent.scrollHeight;
if ((delta < 0 && atTop) || (delta > 0 && atBottom)) {
e.preventDefault();
} else {
e.stopPropagation();
}
}; };
modalContent.removeEventListener('wheel', wheelHandler);
modalContent.addEventListener('wheel', wheelHandler, { passive: false }); element.removeEventListener('wheel', wheelHandler);
} element.addEventListener('wheel', wheelHandler, { passive: true });
}, 0); });
};
// Use multiple timeouts to ensure elements are rendered
setTimeout(setupScrollHandlers, 0);
setTimeout(setupScrollHandlers, 100);
setTimeout(setupScrollHandlers, 300);
}, [isQueueVisible]); }, [isQueueVisible]);
// Debugging output // Debugging output
// Autocomplete for requests // Autocomplete for requests
const fetchTypeahead = async (query) => { const fetchTypeahead = async (query) => {
if (!query || query.length < 2) { if (!query || query.length < 2) {
setTypeaheadOptions([]); setTypeaheadOptions([]);
console.debug('Typeahead: query too short', query);
return; return;
} }
try { try {
console.debug('Typeahead: fetching for query', query);
const response = await authFetch(`${API_URL}/radio/typeahead`, { const response = await authFetch(`${API_URL}/radio/typeahead`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ station: activeStation, query }), body: JSON.stringify({ station: activeStation, query }),
}); });
const data = await response.json(); const data = await response.json();
console.debug('Typeahead: response data', data);
// Accept bare array or { options: [...] } // Accept bare array or { options: [...] }
setTypeaheadOptions(Array.isArray(data) ? data : data.options || []); setTypeaheadOptions(Array.isArray(data) ? data : data.options || []);
console.debug('Typeahead: setTypeaheadOptions', Array.isArray(data) ? data : data.options || []);
} catch (error) { } catch (error) {
console.error('Typeahead: error', error); console.error('Typeahead: error', error);
setTypeaheadOptions([]); setTypeaheadOptions([]);
} }
}; };
console.debug('AudioPlayer user:', user);
if (user) {
console.debug('isAuthenticated:', user);
console.debug('roles:', user.roles);
console.debug('isDJ:', user && Array.isArray(user.roles) && user.roles.includes('dj'));
}
const theme = useHtmlThemeAttr(); const theme = useHtmlThemeAttr();
const [activeStation, setActiveStation] = useState("main"); const [activeStation, setActiveStation] = useState("main");
@@ -110,7 +151,7 @@ export default function Player({ user }) {
const [queueData, setQueueData] = useState([]); const [queueData, setQueueData] = useState([]);
const [queueTotalRecords, setQueueTotalRecords] = useState(0); const [queueTotalRecords, setQueueTotalRecords] = useState(0);
const [queuePage, setQueuePage] = useState(0); const [queuePage, setQueuePage] = useState(0);
const [queueRows, setQueueRows] = useState(10); const [queueRows, setQueueRows] = useState(20);
// DJ controls state // DJ controls state
const [requestInput, setRequestInput] = useState(""); const [requestInput, setRequestInput] = useState("");
@@ -125,8 +166,10 @@ export default function Player({ user }) {
const baseTrackElapsed = useRef(0); const baseTrackElapsed = useRef(0);
const lastUpdateTimestamp = useRef(Date.now()); const lastUpdateTimestamp = useRef(Date.now());
const activeStationRef = useRef(activeStation); const activeStationRef = useRef(activeStation);
const wsInstance = useRef(null);
const formatTime = (seconds) => { const formatTime = (seconds) => {
if (!seconds || isNaN(seconds) || seconds < 0) return "00:00";
const mins = String(Math.floor(seconds / 60)).padStart(2, "0"); const mins = String(Math.floor(seconds / 60)).padStart(2, "0");
const secs = String(Math.floor(seconds % 60)).padStart(2, "0"); const secs = String(Math.floor(seconds % 60)).padStart(2, "0");
return `${mins}:${secs}`; return `${mins}:${secs}`;
@@ -211,7 +254,7 @@ export default function Player({ user }) {
const intervalId = setInterval(() => { const intervalId = setInterval(() => {
const now = Date.now(); const now = Date.now();
const deltaSec = (now - lastUpdateTimestamp.current) / 1000; const deltaSec = (now - lastUpdateTimestamp.current) / 1000;
let liveElapsed = baseTrackElapsed.current + deltaSec; let liveElapsed = (typeof baseTrackElapsed.current === 'number' && !isNaN(baseTrackElapsed.current) ? baseTrackElapsed.current : 0) + deltaSec;
if (trackDuration && liveElapsed > trackDuration) liveElapsed = trackDuration; if (trackDuration && liveElapsed > trackDuration) liveElapsed = trackDuration;
setElapsedTime(liveElapsed); setElapsedTime(liveElapsed);
}, 200); }, 200);
@@ -223,19 +266,12 @@ export default function Player({ user }) {
// Scroll active lyric into view // Scroll active lyric into view
useEffect(() => { useEffect(() => {
setTimeout(() => { setTimeout(() => {
console.debug('Lyric scroll: currentLyricIndex', currentLyricIndex);
console.debug('Lyric scroll: lyrics', lyrics);
const activeElement = document.querySelector('.lrc-line.active'); const activeElement = document.querySelector('.lrc-line.active');
const lyricsContainer = document.querySelector('.lrc-text'); const lyricsContainer = document.querySelector('.lrc-text');
console.debug('Lyric scroll: activeElement', activeElement);
console.debug('Lyric scroll: lyricsContainer', lyricsContainer);
if (activeElement && lyricsContainer) { if (activeElement && lyricsContainer) {
lyricsContainer.style.maxHeight = '220px'; lyricsContainer.style.maxHeight = '220px';
lyricsContainer.style.overflowY = 'auto'; lyricsContainer.style.overflowY = 'auto';
activeElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); activeElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
console.debug('Lyric scroll: scrolled into view');
} else {
console.debug('Lyric scroll: could not scroll, missing element(s)');
} }
}, 0); }, 0);
}, [currentLyricIndex, lyrics]); }, [currentLyricIndex, lyrics]);
@@ -263,65 +299,170 @@ export default function Player({ user }) {
document.title = `${metaData.title} - Radio [${activeStation}]`; document.title = `${metaData.title} - Radio [${activeStation}]`;
}, [activeStation]); }, [activeStation]);
const fetchMetadataAndLyrics = useCallback(async () => { const parseLrcString = useCallback((lrcString) => {
const requestStation = activeStationRef.current; if (!lrcString || typeof lrcString !== 'string') return [];
try {
const response = await fetch(`${API_URL}/radio/np?station=${requestStation}`, { method: 'POST' });
const trackData = await response.json();
// If station changed while request was in-flight, ignore the response const lines = lrcString.split('\n').filter(line => line.trim());
if (requestStation !== activeStationRef.current) return; const parsedLyrics = [];
if (!trackData.song || trackData.song === 'N/A') { for (const line of lines) {
setTrackTitle('No track playing'); const match = line.match(/\[(\d{2}):(\d{2}\.\d{2})\]\s*(.+)/);
setLyrics([]); if (match) {
return; const [, mins, secs, text] = match;
} const timestamp = Number(mins) * 60 + parseFloat(secs);
if (!isNaN(timestamp) && text.trim()) {
if (trackData.uuid !== currentTrackUuid.current) { parsedLyrics.push({ timestamp, line: text.trim() });
currentTrackUuid.current = trackData.uuid;
setTrackMetadata(trackData, requestStation);
// Refresh queue when track changes
fetchQueue();
const lyricsResponse = await fetch(`${API_URL}/lyric/search`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
a: trackData.artist,
s: trackData.song,
excluded_sources: [],
extra: false,
lrc: true,
src: 'WEB-RADIO',
}),
});
const lyricsData = await lyricsResponse.json();
// Guard against station or track changes during lyrics fetch
if (requestStation !== activeStationRef.current || trackData.uuid !== currentTrackUuid.current) return;
if (!lyricsData.err && Array.isArray(lyricsData.lrc)) {
const parsedLyrics = lyricsData.lrc.map(({ timeTag, words }) => {
const [mins, rest] = timeTag.split(':');
const secs = parseFloat(rest);
return { timestamp: Number(mins) * 60 + secs, line: words };
});
setLyrics(parsedLyrics);
setCurrentLyricIndex(0);
} }
} else { }
// Same track - do not update elapsed or elapsed base here }
setTrackDuration(trackData.duration);
return parsedLyrics.sort((a, b) => a.timestamp - b.timestamp);
}, []);
const fetchLyrics = useCallback(async (trackData, requestStation) => {
try {
const lyricsResponse = await fetch(`${API_URL}/lyric/search`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
a: trackData.artist,
s: trackData.song,
excluded_sources: [],
extra: false,
lrc: true,
src: 'WEB-RADIO',
}),
});
const lyricsData = await lyricsResponse.json();
// Guard against station or track changes during lyrics fetch
if (requestStation !== activeStationRef.current || trackData.uuid !== currentTrackUuid.current) return;
if (!lyricsData.err && Array.isArray(lyricsData.lrc)) {
const parsedLyrics = lyricsData.lrc.map(({ timeTag, words }) => {
const [mins, rest] = timeTag.split(':');
const secs = parseFloat(rest);
return { timestamp: Number(mins) * 60 + secs, line: words };
});
setLyrics(parsedLyrics);
setCurrentLyricIndex(0);
} }
} catch (error) { } catch (error) {
console.error('Error fetching metadata:', error); console.error('Error fetching lyrics:', error);
setTrackTitle('Error fetching track');
setLyrics([]);
} }
}, [activeStation]); }, []);
const handleTrackData = useCallback((trackData) => {
console.debug('handleTrackData called with:', trackData);
const requestStation = activeStationRef.current;
// Only set "No track playing" if we have clear indication of no track
// and not just missing song field (to avoid flickering during transitions)
if ((!trackData.song || trackData.song === 'N/A') && (!trackData.artist || trackData.artist === 'N/A')) {
console.debug('Setting no track playing state');
setTrackTitle('No track playing');
setLyrics([]);
return;
}
if (trackData.uuid !== currentTrackUuid.current) {
currentTrackUuid.current = trackData.uuid;
setTrackMetadata(trackData, requestStation);
// Clear lyrics immediately when new track is detected
setLyrics([]);
setCurrentLyricIndex(0);
// Refresh queue when track changes
fetchQueue();
// Fetch lyrics for the new track
fetchLyrics(trackData, requestStation);
} else {
// Same track - update duration and elapsed time
setTrackDuration(trackData.duration || 0);
if (trackData.elapsed !== undefined && typeof trackData.elapsed === 'number' && !isNaN(trackData.elapsed)) {
baseTrackElapsed.current = trackData.elapsed;
lastUpdateTimestamp.current = Date.now();
setElapsedTime(trackData.elapsed);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fetchLyrics]);
const initializeWebSocket = useCallback((station) => {
// Clean up existing WebSocket
if (wsInstance.current) {
wsInstance.current.close();
wsInstance.current = null;
}
const connectWebSocket = (retryCount = 0) => {
const maxRetries = 5;
const baseDelay = 1000; // 1 second
const maxDelay = 30000; // 30 seconds
const wsUrl = `${API_URL.replace(/^https?:/, 'wss:')}/radio/ws/${station}`;
const ws = new WebSocket(wsUrl);
ws.onopen = function () {
console.log('Connected to radio WebSocket for station:', station);
// Reset retry count on successful connection
};
ws.onmessage = function (event) {
try {
const data = JSON.parse(event.data);
if (data.type === 'track_change') {
// Handle track change
handleTrackData(data.data);
} else if (data.type === 'lrc') {
// Handle LRC data
const parsedLyrics = parseLrcString(data.data);
setLyrics(parsedLyrics);
setCurrentLyricIndex(0);
} else {
// Handle initial now playing data
handleTrackData(data);
}
} catch (error) {
console.error('Error parsing WebSocket message:', error);
}
};
ws.onclose = function (event) {
console.log('Radio WebSocket connection closed, code:', event.code, 'reason:', event.reason);
// Don't retry if it was a clean close (code 1000)
if (event.code === 1000) return;
// Attempt reconnection with exponential backoff
if (retryCount < maxRetries) {
const delay = Math.min(baseDelay * Math.pow(2, retryCount), maxDelay);
console.log(`Attempting WebSocket reconnection in ${delay}ms (attempt ${retryCount + 1}/${maxRetries})`);
setTimeout(() => {
connectWebSocket(retryCount + 1);
}, delay);
} else {
console.error('Max WebSocket reconnection attempts reached');
setTrackTitle('Connection lost');
setLyrics([]);
}
};
ws.onerror = function (error) {
console.error('Radio WebSocket error:', error);
// Don't set error state here - let onclose handle reconnection
};
wsInstance.current = ws;
};
connectWebSocket();
}, [handleTrackData, parseLrcString]);
const setTrackMetadata = useCallback((trackData, requestStation) => { const setTrackMetadata = useCallback((trackData, requestStation) => {
setTrackTitle(trackData.song || 'Unknown Title'); setTrackTitle(trackData.song || 'Unknown Title');
@@ -330,23 +471,42 @@ export default function Player({ user }) {
setTrackAlbum(trackData.album || 'Unknown Album'); setTrackAlbum(trackData.album || 'Unknown Album');
setCoverArt(`${API_URL}/radio/album_art?station=${requestStation}&_=${Date.now()}`); setCoverArt(`${API_URL}/radio/album_art?station=${requestStation}&_=${Date.now()}`);
baseTrackElapsed.current = trackData.elapsed; const elapsed = typeof trackData.elapsed === 'number' && !isNaN(trackData.elapsed) ? trackData.elapsed : 0;
baseTrackElapsed.current = elapsed;
lastUpdateTimestamp.current = Date.now(); lastUpdateTimestamp.current = Date.now();
setElapsedTime(trackData.elapsed); setElapsedTime(elapsed);
setTrackDuration(trackData.duration); setTrackDuration(trackData.duration || 0);
setPageTitle(trackData.artist, trackData.song); setPageTitle(trackData.artist, trackData.song);
}, []); }, []);
useEffect(() => { useEffect(() => {
// ensure the ref points to the current activeStation for in-flight guards // ensure the ref points to the current activeStation for in-flight guards
activeStationRef.current = activeStation; activeStationRef.current = activeStation;
fetchMetadataAndLyrics();
const metadataInterval = setInterval(fetchMetadataAndLyrics, 700);
return () => clearInterval(metadataInterval);
}, [activeStation]);
const progress = (elapsedTime / trackDuration) * 100; // Initialize WebSocket connection for metadata
initializeWebSocket(activeStation);
return () => {
// Clean up WebSocket on station change or component unmount
if (wsInstance.current) {
wsInstance.current.close();
wsInstance.current = null;
}
};
}, [activeStation, initializeWebSocket]);
// Cleanup WebSocket on component unmount
useEffect(() => {
return () => {
if (wsInstance.current) {
wsInstance.current.close();
wsInstance.current = null;
}
};
}, []);
const progress = trackDuration > 0 ? (elapsedTime / trackDuration) * 100 : 0;
const remaining = trackDuration - elapsedTime; const remaining = trackDuration - elapsedTime;
const progressColorClass = const progressColorClass =
@@ -486,18 +646,16 @@ export default function Player({ user }) {
}; };
const handleRemoveFromQueue = async (uuid) => { const handleRemoveFromQueue = async (uuid) => {
console.debug("handleRemoveFromQueue called with uuid:", uuid); // Debugging log
try { try {
const response = await authFetch(`${API_URL}/radio/queue_remove`, { const response = await authFetch(`${API_URL}/radio/remove_from_queue`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: JSON.stringify({
uuid,
station: activeStation, station: activeStation,
uuid,
}), }),
}); });
const data = await response.json(); const data = await response.json();
console.debug("handleRemoveFromQueue response:", data); // Debugging log
if (!data.err) { if (!data.err) {
toast.success("OK!"); toast.success("OK!");
fetchQueue(); fetchQueue();
@@ -637,18 +795,15 @@ export default function Player({ user }) {
value={requestInput} value={requestInput}
suggestions={typeaheadOptions} suggestions={typeaheadOptions}
completeMethod={(e) => { completeMethod={(e) => {
console.debug('AutoComplete: completeMethod called', e);
fetchTypeahead(e.query); fetchTypeahead(e.query);
}} }}
onChange={e => { onChange={e => {
console.debug('AutoComplete: onChange', e);
setRequestInput(e.target.value); setRequestInput(e.target.value);
}} }}
onShow={() => { onShow={() => {
setTimeout(() => { setTimeout(() => {
const panel = document.querySelector('.p-autocomplete-panel'); const panel = document.querySelector('.p-autocomplete-panel');
const items = panel?.querySelector('.p-autocomplete-items'); const items = panel?.querySelector('.p-autocomplete-items');
console.debug('AutoComplete: onShow panel', panel);
if (items) { if (items) {
items.style.maxHeight = '200px'; items.style.maxHeight = '200px';
items.style.overflowY = 'auto'; items.style.overflowY = 'auto';
@@ -750,14 +905,17 @@ export default function Player({ user }) {
<DataTable <DataTable
value={queueData} value={queueData}
paginator paginator
alwaysShowPaginator={true}
rows={queueRows} rows={queueRows}
first={queuePage * queueRows} first={queuePage * queueRows}
totalRecords={queueTotalRecords} totalRecords={queueTotalRecords}
onPage={(e) => { onPage={(e) => {
setQueuePage(e.page); setQueuePage(e.page);
setQueueRows(e.rows); fetchQueue(e.page, queueRows, queueSearch);
fetchQueue(e.page, e.rows, queueSearch);
}} }}
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport"
currentPageReportTemplate="Showing {first} to {last} of {totalRecords} entries"
paginatorClassName="queue-paginator !bg-neutral-50 dark:!bg-neutral-800 !text-neutral-900 dark:!text-neutral-100 !border-neutral-200 dark:!border-neutral-700"
className="p-datatable-gridlines rounded-lg shadow-md border-t border-neutral-300 dark:border-neutral-700" className="p-datatable-gridlines rounded-lg shadow-md border-t border-neutral-300 dark:border-neutral-700"
style={{ minHeight: 'auto', height: 'auto', tableLayout: 'fixed', width: '100%' }} style={{ minHeight: 'auto', height: 'auto', tableLayout: 'fixed', width: '100%' }}
lazy lazy
@@ -776,7 +934,7 @@ export default function Player({ user }) {
sortable sortable
headerClassName="bg-neutral-100 dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 font-bold font-sans" headerClassName="bg-neutral-100 dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 font-bold font-sans"
style={{ width: '300px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }} style={{ width: '300px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}
body={(rowData) => <span style={{ display: 'block', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: '300px' }}>{rowData.artistsong}</span>} body={(rowData) => <span title={rowData.artistsong} style={{ display: 'block', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: '300px' }}>{rowData.artistsong}</span>}
></Column> ></Column>
<Column <Column
field="album" field="album"
@@ -784,7 +942,7 @@ export default function Player({ user }) {
sortable sortable
headerClassName="bg-neutral-100 dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 font-bold font-sans" headerClassName="bg-neutral-100 dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 font-bold font-sans"
style={{ width: '220px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }} style={{ width: '220px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}
body={(rowData) => <span style={{ display: 'block', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: '220px' }}>{rowData.album}</span>} body={(rowData) => <span title={rowData.album} style={{ display: 'block', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: '220px' }}>{rowData.album}</span>}
></Column> ></Column>
<Column <Column
field="genre" field="genre"
@@ -792,7 +950,7 @@ export default function Player({ user }) {
sortable sortable
headerClassName="bg-neutral-100 dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 font-bold font-sans" headerClassName="bg-neutral-100 dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 font-bold font-sans"
style={{ width: '120px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }} style={{ width: '120px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}
body={(rowData) => <span style={{ display: 'block', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: '120px' }}>{rowData.genre}</span>} body={(rowData) => <span title={rowData.genre} style={{ display: 'block', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: '120px' }}>{rowData.genre}</span>}
></Column> ></Column>
{isDJ && ( {isDJ && (
<Column <Column
@@ -824,7 +982,6 @@ export default function Player({ user }) {
<button <button
className="px-2 py-1 rounded bg-red-400 text-neutral-900 hover:bg-red-500 text-xs shadow-md cursor-pointer font-bold font-sans" className="px-2 py-1 rounded bg-red-400 text-neutral-900 hover:bg-red-500 text-xs shadow-md cursor-pointer font-bold font-sans"
onClick={() => { onClick={() => {
console.debug("Remove button clicked for uuid:", rowData.uuid); // Debugging log
handleRemoveFromQueue(rowData.uuid); handleRemoveFromQueue(rowData.uuid);
}} }}
title="Remove" title="Remove"