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:
@@ -15,6 +15,29 @@ import { requireAuthHook } from "@/hooks/requireAuthHook";
|
||||
import { useHtmlThemeAttr } from "@/hooks/useHtmlThemeAttr";
|
||||
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 = {
|
||||
main: { label: "Main" },
|
||||
rock: { label: "Rock" },
|
||||
@@ -25,61 +48,79 @@ const STATIONS = {
|
||||
|
||||
|
||||
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);
|
||||
// Mouse wheel scroll fix for queue modal
|
||||
useEffect(() => {
|
||||
if (!isQueueVisible) return;
|
||||
setTimeout(() => {
|
||||
const modalContent = document.querySelector('.p-dialog .p-dialog-content > div');
|
||||
if (modalContent) {
|
||||
modalContent.style.overflowY = 'auto';
|
||||
modalContent.style.overscrollBehavior = 'contain';
|
||||
|
||||
const setupScrollHandlers = () => {
|
||||
// Target multiple possible scroll containers
|
||||
const selectors = [
|
||||
'.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 delta = e.deltaY;
|
||||
const atTop = modalContent.scrollTop === 0;
|
||||
const atBottom = modalContent.scrollTop + modalContent.clientHeight >= modalContent.scrollHeight;
|
||||
if ((delta < 0 && atTop) || (delta > 0 && atBottom)) {
|
||||
e.preventDefault();
|
||||
} else {
|
||||
e.stopPropagation();
|
||||
}
|
||||
e.stopPropagation();
|
||||
// Allow normal scrolling within the modal
|
||||
};
|
||||
modalContent.removeEventListener('wheel', wheelHandler);
|
||||
modalContent.addEventListener('wheel', wheelHandler, { passive: false });
|
||||
}
|
||||
}, 0);
|
||||
|
||||
element.removeEventListener('wheel', wheelHandler);
|
||||
element.addEventListener('wheel', wheelHandler, { passive: true });
|
||||
});
|
||||
};
|
||||
|
||||
// Use multiple timeouts to ensure elements are rendered
|
||||
setTimeout(setupScrollHandlers, 0);
|
||||
setTimeout(setupScrollHandlers, 100);
|
||||
setTimeout(setupScrollHandlers, 300);
|
||||
}, [isQueueVisible]);
|
||||
// Debugging output
|
||||
// Autocomplete for requests
|
||||
const fetchTypeahead = async (query) => {
|
||||
if (!query || query.length < 2) {
|
||||
setTypeaheadOptions([]);
|
||||
console.debug('Typeahead: query too short', query);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
console.debug('Typeahead: fetching for query', query);
|
||||
const response = await authFetch(`${API_URL}/radio/typeahead`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ station: activeStation, query }),
|
||||
});
|
||||
const data = await response.json();
|
||||
console.debug('Typeahead: response data', data);
|
||||
// Accept bare array or { options: [...] }
|
||||
setTypeaheadOptions(Array.isArray(data) ? data : data.options || []);
|
||||
console.debug('Typeahead: setTypeaheadOptions', Array.isArray(data) ? data : data.options || []);
|
||||
} catch (error) {
|
||||
console.error('Typeahead: error', error);
|
||||
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 [activeStation, setActiveStation] = useState("main");
|
||||
@@ -110,7 +151,7 @@ export default function Player({ user }) {
|
||||
const [queueData, setQueueData] = useState([]);
|
||||
const [queueTotalRecords, setQueueTotalRecords] = useState(0);
|
||||
const [queuePage, setQueuePage] = useState(0);
|
||||
const [queueRows, setQueueRows] = useState(10);
|
||||
const [queueRows, setQueueRows] = useState(20);
|
||||
|
||||
// DJ controls state
|
||||
const [requestInput, setRequestInput] = useState("");
|
||||
@@ -125,8 +166,10 @@ export default function Player({ user }) {
|
||||
const baseTrackElapsed = useRef(0);
|
||||
const lastUpdateTimestamp = useRef(Date.now());
|
||||
const activeStationRef = useRef(activeStation);
|
||||
const wsInstance = useRef(null);
|
||||
|
||||
const formatTime = (seconds) => {
|
||||
if (!seconds || isNaN(seconds) || seconds < 0) return "00:00";
|
||||
const mins = String(Math.floor(seconds / 60)).padStart(2, "0");
|
||||
const secs = String(Math.floor(seconds % 60)).padStart(2, "0");
|
||||
return `${mins}:${secs}`;
|
||||
@@ -211,7 +254,7 @@ export default function Player({ user }) {
|
||||
const intervalId = setInterval(() => {
|
||||
const now = Date.now();
|
||||
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;
|
||||
setElapsedTime(liveElapsed);
|
||||
}, 200);
|
||||
@@ -223,19 +266,12 @@ export default function Player({ user }) {
|
||||
// Scroll active lyric into view
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
console.debug('Lyric scroll: currentLyricIndex', currentLyricIndex);
|
||||
console.debug('Lyric scroll: lyrics', lyrics);
|
||||
const activeElement = document.querySelector('.lrc-line.active');
|
||||
const lyricsContainer = document.querySelector('.lrc-text');
|
||||
console.debug('Lyric scroll: activeElement', activeElement);
|
||||
console.debug('Lyric scroll: lyricsContainer', lyricsContainer);
|
||||
if (activeElement && lyricsContainer) {
|
||||
lyricsContainer.style.maxHeight = '220px';
|
||||
lyricsContainer.style.overflowY = 'auto';
|
||||
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);
|
||||
}, [currentLyricIndex, lyrics]);
|
||||
@@ -263,65 +299,170 @@ export default function Player({ user }) {
|
||||
document.title = `${metaData.title} - Radio [${activeStation}]`;
|
||||
}, [activeStation]);
|
||||
|
||||
const fetchMetadataAndLyrics = useCallback(async () => {
|
||||
const requestStation = activeStationRef.current;
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/radio/np?station=${requestStation}`, { method: 'POST' });
|
||||
const trackData = await response.json();
|
||||
const parseLrcString = useCallback((lrcString) => {
|
||||
if (!lrcString || typeof lrcString !== 'string') return [];
|
||||
|
||||
// If station changed while request was in-flight, ignore the response
|
||||
if (requestStation !== activeStationRef.current) return;
|
||||
const lines = lrcString.split('\n').filter(line => line.trim());
|
||||
const parsedLyrics = [];
|
||||
|
||||
if (!trackData.song || trackData.song === 'N/A') {
|
||||
setTrackTitle('No track playing');
|
||||
setLyrics([]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (trackData.uuid !== currentTrackUuid.current) {
|
||||
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);
|
||||
for (const line of lines) {
|
||||
const match = line.match(/\[(\d{2}):(\d{2}\.\d{2})\]\s*(.+)/);
|
||||
if (match) {
|
||||
const [, mins, secs, text] = match;
|
||||
const timestamp = Number(mins) * 60 + parseFloat(secs);
|
||||
if (!isNaN(timestamp) && text.trim()) {
|
||||
parsedLyrics.push({ timestamp, line: text.trim() });
|
||||
}
|
||||
} 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) {
|
||||
console.error('Error fetching metadata:', error);
|
||||
setTrackTitle('Error fetching track');
|
||||
setLyrics([]);
|
||||
console.error('Error fetching lyrics:', error);
|
||||
}
|
||||
}, [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) => {
|
||||
setTrackTitle(trackData.song || 'Unknown Title');
|
||||
@@ -330,23 +471,42 @@ export default function Player({ user }) {
|
||||
setTrackAlbum(trackData.album || 'Unknown Album');
|
||||
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();
|
||||
setElapsedTime(trackData.elapsed);
|
||||
setElapsedTime(elapsed);
|
||||
|
||||
setTrackDuration(trackData.duration);
|
||||
setTrackDuration(trackData.duration || 0);
|
||||
setPageTitle(trackData.artist, trackData.song);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// ensure the ref points to the current activeStation for in-flight guards
|
||||
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 progressColorClass =
|
||||
@@ -486,18 +646,16 @@ export default function Player({ user }) {
|
||||
};
|
||||
|
||||
const handleRemoveFromQueue = async (uuid) => {
|
||||
console.debug("handleRemoveFromQueue called with uuid:", uuid); // Debugging log
|
||||
try {
|
||||
const response = await authFetch(`${API_URL}/radio/queue_remove`, {
|
||||
const response = await authFetch(`${API_URL}/radio/remove_from_queue`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
uuid,
|
||||
station: activeStation,
|
||||
uuid,
|
||||
}),
|
||||
});
|
||||
const data = await response.json();
|
||||
console.debug("handleRemoveFromQueue response:", data); // Debugging log
|
||||
if (!data.err) {
|
||||
toast.success("OK!");
|
||||
fetchQueue();
|
||||
@@ -637,18 +795,15 @@ export default function Player({ user }) {
|
||||
value={requestInput}
|
||||
suggestions={typeaheadOptions}
|
||||
completeMethod={(e) => {
|
||||
console.debug('AutoComplete: completeMethod called', e);
|
||||
fetchTypeahead(e.query);
|
||||
}}
|
||||
onChange={e => {
|
||||
console.debug('AutoComplete: onChange', e);
|
||||
setRequestInput(e.target.value);
|
||||
}}
|
||||
onShow={() => {
|
||||
setTimeout(() => {
|
||||
const panel = document.querySelector('.p-autocomplete-panel');
|
||||
const items = panel?.querySelector('.p-autocomplete-items');
|
||||
console.debug('AutoComplete: onShow panel', panel);
|
||||
if (items) {
|
||||
items.style.maxHeight = '200px';
|
||||
items.style.overflowY = 'auto';
|
||||
@@ -750,14 +905,17 @@ export default function Player({ user }) {
|
||||
<DataTable
|
||||
value={queueData}
|
||||
paginator
|
||||
alwaysShowPaginator={true}
|
||||
rows={queueRows}
|
||||
first={queuePage * queueRows}
|
||||
totalRecords={queueTotalRecords}
|
||||
onPage={(e) => {
|
||||
setQueuePage(e.page);
|
||||
setQueueRows(e.rows);
|
||||
fetchQueue(e.page, e.rows, queueSearch);
|
||||
fetchQueue(e.page, queueRows, 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"
|
||||
style={{ minHeight: 'auto', height: 'auto', tableLayout: 'fixed', width: '100%' }}
|
||||
lazy
|
||||
@@ -776,7 +934,7 @@ export default function Player({ user }) {
|
||||
sortable
|
||||
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' }}
|
||||
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
|
||||
field="album"
|
||||
@@ -784,7 +942,7 @@ export default function Player({ user }) {
|
||||
sortable
|
||||
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' }}
|
||||
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
|
||||
field="genre"
|
||||
@@ -792,7 +950,7 @@ export default function Player({ user }) {
|
||||
sortable
|
||||
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' }}
|
||||
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>
|
||||
{isDJ && (
|
||||
<Column
|
||||
@@ -824,7 +982,6 @@ export default function Player({ user }) {
|
||||
<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"
|
||||
onClick={() => {
|
||||
console.debug("Remove button clicked for uuid:", rowData.uuid); // Debugging log
|
||||
handleRemoveFromQueue(rowData.uuid);
|
||||
}}
|
||||
title="Remove"
|
||||
|
||||
Reference in New Issue
Block a user