Files
codey.lol/src/components/AudioPlayer.jsx

853 lines
32 KiB
JavaScript

import React, { useState, useEffect, useRef, Suspense, lazy, useMemo, useCallback } from "react";
import { metaData } from "../config";
import Play from "@mui/icons-material/PlayArrow";
import Pause from "@mui/icons-material/Pause";
import "@styles/player.css";
import { Dialog } from "primereact/dialog";
import { AutoComplete } from "primereact/autocomplete";
import { DataTable } from "primereact/datatable";
import { Column } from "primereact/column";
import { toast } from "react-toastify";
import { ENVIRONMENT } from "../config";
import { API_URL } from "@/config";
import { authFetch } from "@/utils/authFetch";
import { requireAuthHook } from "@/hooks/requireAuthHook";
import { useHtmlThemeAttr } from "@/hooks/useHtmlThemeAttr";
import "@/components/TRip/RequestManagement.css";
const STATIONS = {
main: { label: "Main" },
rock: { label: "Rock" },
rap: { label: "Rap" },
electronic: { label: "Electronic" },
pop: { label: "Pop" },
};
export default function Player({ user }) {
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 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();
}
};
modalContent.removeEventListener('wheel', wheelHandler);
modalContent.addEventListener('wheel', wheelHandler, { passive: false });
}
}, 0);
}, [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");
const [isPlaying, setIsPlaying] = useState(false);
const [trackTitle, setTrackTitle] = useState("");
const [trackArtist, setTrackArtist] = useState("");
// ...existing code...
const [trackGenre, setTrackGenre] = useState("");
const [trackAlbum, setTrackAlbum] = useState("");
const [coverArt, setCoverArt] = useState("/images/radio_art_default.jpg");
const [lyrics, setLyrics] = useState([]);
const [currentLyricIndex, setCurrentLyricIndex] = useState(0);
const [elapsedTime, setElapsedTime] = useState(0);
// Update currentLyricIndex as song progresses
useEffect(() => {
if (!lyrics || lyrics.length === 0) return;
// Find the last lyric whose timestamp is <= elapsedTime
let idx = 0;
for (let i = 0; i < lyrics.length; i++) {
if (lyrics[i].timestamp <= elapsedTime) {
idx = i;
} else {
break;
}
}
setCurrentLyricIndex(idx);
}, [elapsedTime, lyrics]);
const [trackDuration, setTrackDuration] = useState(0);
const [queueData, setQueueData] = useState([]);
const [queueTotalRecords, setQueueTotalRecords] = useState(0);
const [queuePage, setQueuePage] = useState(0);
const [queueRows, setQueueRows] = useState(10);
// DJ controls state
const [requestInput, setRequestInput] = useState("");
const [typeaheadOptions, setTypeaheadOptions] = useState([]);
const [requestInputArtist, setRequestInputArtist] = useState("");
const [requestInputSong, setRequestInputSong] = useState("");
const [requestInputUuid, setRequestInputUuid] = useState("");
const audioElement = useRef(null);
const hlsInstance = useRef(null);
const currentTrackUuid = useRef(null);
const baseTrackElapsed = useRef(0);
const lastUpdateTimestamp = useRef(Date.now());
const activeStationRef = useRef(activeStation);
const formatTime = (seconds) => {
const mins = String(Math.floor(seconds / 60)).padStart(2, "0");
const secs = String(Math.floor(seconds % 60)).padStart(2, "0");
return `${mins}:${secs}`;
};
// Set page title based on current track and station
const setPageTitle = (artist, song) => {
document.title = `${metaData.title} - Radio - ${artist} - ${song} [${activeStationRef.current}]`;
};
// Initialize or switch HLS stream
const initializeStream = (station) => {
import('hls.js').then(({ default: Hls }) => {
const audio = audioElement.current;
if (!audio) return;
const streamUrl = `https://stream.codey.lol/hls/${station}/${station}.m3u8`;
// Clean up previous HLS
if (hlsInstance.current) {
hlsInstance.current.destroy();
hlsInstance.current = null;
}
audio.pause();
audio.removeAttribute("src");
audio.load();
// Handle audio load errors
audio.onerror = () => {
setIsPlaying(false);
};
if (audio.canPlayType("application/vnd.apple.mpegurl")) {
audio.src = streamUrl;
audio.load();
audio.play().then(() => setIsPlaying(true)).catch(() => {
setTrackTitle("Offline");
setIsPlaying(false);
});
return;
}
if (!Hls.isSupported()) {
console.error("HLS not supported");
return;
}
const hls = new Hls({
lowLatencyMode: true,
abrEnabled: false,
liveSyncDuration: 0.5, // seconds behind live edge target
liveMaxLatencyDuration: 3.0, // max allowed latency before catchup
liveCatchUpPlaybackRate: 1.02,
maxBufferLength: 30,
maxMaxBufferLength: 60,
maxBufferHole: 0.5,
manifestLoadingTimeOut: 4000,
fragLoadingTimeOut: 10000, // playback speed when catching up
});
hlsInstance.current = hls;
hls.attachMedia(audio);
hls.on(Hls.Events.MEDIA_ATTACHED, () => hls.loadSource(streamUrl));
hls.on(Hls.Events.MANIFEST_PARSED, () => {
audio.play().then(() => setIsPlaying(true)).catch(() => {
setIsPlaying(false);
});
});
hls.on(Hls.Events.ERROR, (event, data) => {
console.warn("HLS error:", data);
if (data.fatal) {
hls.destroy();
hlsInstance.current = null;
setTrackTitle("Offline");
setIsPlaying(false);
}
});
});
};
// Update elapsed time smoothly
useEffect(() => {
const intervalId = setInterval(() => {
const now = Date.now();
const deltaSec = (now - lastUpdateTimestamp.current) / 1000;
let liveElapsed = baseTrackElapsed.current + deltaSec;
if (trackDuration && liveElapsed > trackDuration) liveElapsed = trackDuration;
setElapsedTime(liveElapsed);
}, 200);
return () => clearInterval(intervalId);
}, [isPlaying, trackDuration]);
// ...existing code...
// ...existing code...
// 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]);
// Handle station changes: reset and start new stream
useEffect(() => {
setIsPlaying(false);
setTrackTitle("");
setTrackArtist("");
setTrackGenre("");
setTrackAlbum("");
setCoverArt("/images/radio_art_default.jpg");
setLyrics([]);
setCurrentLyricIndex(0);
setElapsedTime(0);
setTrackDuration(0);
currentTrackUuid.current = null;
baseTrackElapsed.current = 0;
lastUpdateTimestamp.current = Date.now();
initializeStream(activeStation);
// If no track is loaded, update page title to just station
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();
// If station changed while request was in-flight, ignore the response
if (requestStation !== activeStationRef.current) return;
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);
}
} else {
// Same track - do not update elapsed or elapsed base here
setTrackDuration(trackData.duration);
}
} catch (error) {
console.error('Error fetching metadata:', error);
setTrackTitle('Error fetching track');
setLyrics([]);
}
}, [activeStation]);
const setTrackMetadata = useCallback((trackData, requestStation) => {
setTrackTitle(trackData.song || 'Unknown Title');
setTrackArtist(trackData.artist || 'Unknown Artist');
setTrackGenre(trackData.genre || '');
setTrackAlbum(trackData.album || 'Unknown Album');
setCoverArt(`${API_URL}/radio/album_art?station=${requestStation}&_=${Date.now()}`);
baseTrackElapsed.current = trackData.elapsed;
lastUpdateTimestamp.current = Date.now();
setElapsedTime(trackData.elapsed);
setTrackDuration(trackData.duration);
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;
const remaining = trackDuration - elapsedTime;
const progressColorClass =
progress >= 90
? "bg-red-500 dark:bg-red-400"
: progress >= 75 || remaining <= 20
? "bg-yellow-400 dark:bg-yellow-300"
: "bg-blue-500 dark:bg-blue-400";
// ...existing code...
const handleSkip = async (uuid = null) => {
try {
const response = await authFetch(`${API_URL}/radio/skip`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
skipTo: uuid,
station: activeStation,
}),
});
const data = await response.json();
if (data.success) {
toast.success("OK!");
fetchQueue();
} else {
toast.error("Skip failed.");
}
} catch (error) {
console.error("Error skipping track:", error);
toast.error("Skip failed.");
}
};
const handleReshuffle = async () => {
try {
const response = await authFetch(`${API_URL}/radio/reshuffle`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
station: activeStation,
}),
});
const data = await response.json();
if (data.ok) {
toast.success("OK!");
fetchQueue();
} else {
toast.error("Reshuffle failed.");
}
} catch (error) {
console.error("Error reshuffling queue:", error);
toast.error("Reshuffle failed.");
}
};
const handleQueueShift = async (uuid, next = false) => {
try {
const response = await authFetch(`${API_URL}/radio/queue_shift`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
uuid,
next,
station: activeStation,
}),
});
const data = await response.json();
if (!data.err) {
toast.success("OK!");
fetchQueue();
} else {
toast.error("Queue shift failed.");
}
} catch (error) {
console.error("Error shifting queue:", error);
toast.error("Queue shift failed.");
}
};
const handlePlayNow = async (artistSong, next = false) => {
const toastId = "playNowToast"; // Unique ID for this toast
if (!toast.isActive(toastId)) {
toast.info("Trying...", { toastId });
}
try {
const response = await authFetch(`${API_URL}/radio/request`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
artistsong: artistSong,
alsoSkip: !next,
station: activeStation,
}),
});
const data = await response.json();
if (data.result) {
setRequestInput(""); // Clear the input immediately
toast.update(toastId, { render: "OK!", type: "success", autoClose: 2000 });
fetchQueue();
} else {
toast.update(toastId, { render: "Play Now failed.", type: "error", autoClose: 3000 });
}
} catch (error) {
console.error("Error playing song immediately:", error);
toast.update(toastId, { render: "Play Now failed.", type: "error", autoClose: 3000 });
}
};
const handleSongRequest = async (artistSong) => {
const toastId = "songRequestToast"; // Unique ID for this toast
if (!toast.isActive(toastId)) {
toast.info("Trying...", { toastId });
}
try {
const response = await authFetch(`${API_URL}/radio/request`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
artistsong: artistSong,
alsoSkip: false, // Ensure alsoSkip is false for requests
station: activeStation,
}),
});
const data = await response.json();
if (data.result) {
setRequestInput(""); // Clear the input immediately
toast.update(toastId, { render: "OK!", type: "success", autoClose: 2000 });
fetchQueue();
} else {
toast.update(toastId, { render: "Song request failed.", type: "error", autoClose: 3000 });
}
} catch (error) {
console.error("Error requesting song:", error);
toast.update(toastId, { render: "Song request failed.", type: "error", autoClose: 3000 });
}
};
const handleRemoveFromQueue = async (uuid) => {
console.debug("handleRemoveFromQueue called with uuid:", uuid); // Debugging log
try {
const response = await authFetch(`${API_URL}/radio/queue_remove`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
uuid,
station: activeStation,
}),
});
const data = await response.json();
console.debug("handleRemoveFromQueue response:", data); // Debugging log
if (!data.err) {
toast.success("OK!");
fetchQueue();
} else {
toast.error("Remove from queue failed.");
}
} catch (error) {
console.error("Error removing from queue:", error);
toast.error("Remove from queue failed.");
}
};
const [queueSearch, setQueueSearch] = useState("");
const fetchQueue = async (page = queuePage, rows = queueRows, search = queueSearch) => {
const start = page * rows;
try {
const response = await authFetch(`${API_URL}/radio/get_queue`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
station: activeStation,
start,
length: rows,
search,
}),
});
const data = await response.json();
setQueueData(data.items || []);
// Use recordsFiltered for search, otherwise recordsTotal
setQueueTotalRecords(
typeof search === 'string' && search.length > 0
? data.recordsFiltered ?? data.recordsTotal ?? 0
: data.recordsTotal ?? 0
);
} catch (error) {
console.error("Error fetching queue:", error);
}
};
useEffect(() => {
if (isQueueVisible) {
fetchQueue(queuePage, queueRows, queueSearch);
}
}, [isQueueVisible, queuePage, queueRows, queueSearch]);
// Always define queueFooter, fallback to Close button if user is not available
const isDJ = (user && user.roles.includes('dj')) || ENVIRONMENT === "Dev";
const queueFooter = isDJ
? (
<div className="flex gap-2 justify-end">
<button
className="px-3 py-1 rounded bg-green-400 text-white hover:bg-green-500 text-xs font-bold"
onClick={() => handleReshuffle()}
>
Reshuffle
</button>
<button
className="px-3 py-1 rounded bg-gray-400 text-white hover:bg-gray-500 text-xs font-bold"
onClick={() => setQueueVisible(false)}
>
Close
</button>
</div>
)
: (
<div className="flex gap-2 justify-end">
<button
className="px-3 py-1 rounded bg-gray-400 text-white hover:bg-gray-500 text-xs font-bold"
onClick={() => setQueueVisible(false)}
>
Close
</button>
</div>
);
return (
<>
<div className="station-tabs flex gap-2 justify-center mb-4 flex-wrap z-10 relative">
{Object.entries(STATIONS).map(([stationKey, { label }]) => (
<button
key={stationKey}
onClick={() => setActiveStation(stationKey)}
className={`px-3 py-1 rounded-full text-sm font-semibold transition-colors ${activeStation === stationKey
? "bg-neutral-800 text-white dark:bg-white dark:text-black"
: "bg-neutral-200 text-black dark:bg-neutral-700 dark:text-white"
}`}
aria-pressed={activeStation === stationKey}
>
{label}
</button>
))}
</div>
<div className="c-container">
{/* {user && <span className="text-lg font-semibold">Hello, {user.user}</span>} */}
<div className="music-container mt-8">
<section className="album-cover">
<div className="music-player__album" title="Album">
{trackAlbum}
</div>
<img src={coverArt} className="cover rounded-lg shadow-md" alt="Cover Art" />
</section>
<section className="music-player">
<h1 className="music-player__header text-2xl font-bold">serious.FM</h1>
<h1 className="music-player__title text-xl font-semibold">{trackTitle}</h1>
<h2 className="music-player__author text-lg font-medium">{trackArtist}</h2>
{trackGenre && <h2 className="music-player__genre text-md italic">{trackGenre}</h2>}
<div className="music-time flex justify-between items-center mt-4">
<p className="music-time__current text-sm">{formatTime(elapsedTime)}</p>
<p className="music-time__last text-sm">{formatTime(trackDuration - elapsedTime)}</p>
</div>
<div className="progress-bar-container w-full h-2 rounded bg-neutral-300 dark:bg-neutral-700 overflow-hidden">
<div
className={`h-full transition-all duration-200 ${progressColorClass}`}
style={{ width: `${progress}%` }}
></div>
</div>
<div className={`lrc-text mt-4 p-4 rounded-lg bg-neutral-100 dark:bg-neutral-800 ${lyrics.length === 0 ? "empty" : ""}`}>
{lyrics.map((lyricObj, index) => (
<p
key={index}
className={`lrc-line text-sm ${index === currentLyricIndex ? "active font-bold" : ""}`}
>
{lyricObj.line}
</p>
))}
</div>
{isDJ && (
<div className="dj-controls mt-6 flex flex-wrap justify-center items-center gap-2" style={{ minWidth: '24rem', maxWidth: '100%', alignItems: 'flex-start' }}>
<div className="flex flex-col items-center gap-2 w-full" style={{ minWidth: '24rem' }}>
<AutoComplete
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';
items.style.overscrollBehavior = 'contain';
const wheelHandler = (e) => {
const delta = e.deltaY;
const atTop = items.scrollTop === 0;
const atBottom = items.scrollTop + items.clientHeight >= items.scrollHeight;
if ((delta < 0 && atTop) || (delta > 0 && atBottom)) {
e.preventDefault();
} else {
e.stopPropagation();
}
};
items.removeEventListener('wheel', wheelHandler);
items.addEventListener('wheel', wheelHandler, { passive: false });
}
}, 0);
}}
placeholder="Request a song..."
inputStyle={{
width: '24rem',
minWidth: '24rem',
padding: '0.35rem 0.75rem',
borderRadius: '9999px',
border: '1px solid #d1d5db',
fontSize: '1rem',
fontWeight: 'bold',
}}
className="typeahead-input"
forceSelection={false}
/>
<div className="flex flex-row flex-wrap justify-center gap-2 items-center" style={{ minWidth: '24rem' }}>
<button className="px-3 py-1 rounded-full bg-blue-400 text-white hover:bg-blue-500 text-xs font-bold"
onClick={() => handleSongRequest(requestInput)}>
Request
</button>
<button className="px-3 py-1 rounded-full bg-green-400 text-white hover:bg-green-500 text-xs font-bold"
onClick={() => handlePlayNow(requestInput)}>
Play Now
</button>
<button className="px-3 py-1 rounded-full bg-red-400 text-white hover:bg-red-500 text-xs font-bold" onClick={() => handleSkip()}>Skip</button>
<button
className="px-3 py-1 rounded-full bg-gray-400 text-white hover:bg-gray-500 text-xs font-bold"
onClick={() => setQueueVisible(true)}
>
Queue
</button>
</div>
</div>
</div>
)}
{/* Always show play/pause button */}
<div className="music-control mt-4 flex justify-center">
<div
className="music-control__play"
onClick={() => {
const audio = audioElement.current;
if (!audio) return;
if (isPlaying) {
audio.pause();
setIsPlaying(false);
} else {
audio.play().then(() => setIsPlaying(true));
}
}}
role="button"
tabIndex={0}
aria-pressed={isPlaying}
>
{isPlaying ? <Pause /> : <Play />}
</div>
</div>
</section>
</div>
<audio ref={audioElement} preload="none" />
<Dialog
header={`Queue - ${activeStation}`}
visible={isQueueVisible}
style={{ width: "80vw", maxWidth: "1200px", height: "auto", maxHeight: "90vh" }}
footer={queueFooter}
onHide={() => setQueueVisible(false)}
className={theme === "dark" ? "dark-theme" : "light-theme"}
dismissableMask={true}
>
<div style={{ maxHeight: "calc(90vh - 100px)", overflow: "visible" }}>
<div className="mb-2 flex justify-end">
<input
type="text"
value={queueSearch}
onChange={e => setQueueSearch(e.target.value)}
placeholder="Search..."
className="px-2 py-1 rounded border border-neutral-300 dark:border-neutral-700 text-sm"
style={{ minWidth: 180 }}
/>
</div>
<DataTable
value={queueData}
paginator
rows={queueRows}
first={queuePage * queueRows}
totalRecords={queueTotalRecords}
onPage={(e) => {
setQueuePage(e.page);
setQueueRows(e.rows);
fetchQueue(e.page, e.rows, queueSearch);
}}
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
>
<Column
field="pos"
header="Position"
body={(rowData) => rowData.pos + 1}
sortable
headerClassName="bg-neutral-100 dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 font-bold font-sans"
style={{ width: '50px', textAlign: 'center' }}
></Column>
<Column
field="artistsong"
header="Track"
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>}
></Column>
<Column
field="album"
header="Album"
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>}
></Column>
<Column
field="genre"
header="Genre"
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>}
></Column>
{isDJ && (
<Column
field="actions"
header="Actions"
body={(rowData) => (
<div className="flex gap-1 flex-nowrap" style={{ minWidth: '220px', justifyContent: 'center' }}>
<button
className="px-2 py-1 rounded bg-blue-400 text-neutral-900 hover:bg-blue-500 text-xs shadow-md cursor-pointer font-bold font-sans"
onClick={() => handleSkip(rowData.uuid)}
title="Skip To"
>
Skip To
</button>
<button
className="px-2 py-1 rounded bg-yellow-400 text-neutral-900 hover:bg-yellow-500 text-xs shadow-md cursor-pointer font-bold font-sans"
onClick={() => handleQueueShift(rowData.uuid, true, false)}
title="Play"
>
Play
</button>
<button
className="px-2 py-1 rounded bg-green-400 text-neutral-900 hover:bg-green-500 text-xs shadow-md cursor-pointer font-bold font-sans"
onClick={() => handleQueueShift(rowData.uuid, false, true)}
title="Play Next"
>
Play Next
</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"
onClick={() => {
console.debug("Remove button clicked for uuid:", rowData.uuid); // Debugging log
handleRemoveFromQueue(rowData.uuid);
}}
title="Remove"
>
Remove
</button>
</div>
)}
headerClassName="bg-neutral-100 dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 font-bold font-sans"
style={{ width: '220px', textAlign: 'center' }}
></Column>
)}
</DataTable>
</div>
</Dialog>
</div>
</>
);
}