Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions app/api/spotify/playlists/[playlistId]/tracks/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,11 @@ async function getPlaylistTracks(
id: track.id,
name: track.name,
artists: track.artists.map((artist) => artist.name).join(', '),
albumArt:
track.album.images && track.album.images.length > 0
? (track.album.images[0]?.url ?? null)
: null,
duration: track.duration_ms,
album: {
name: track.album.name,
images: track.album.images,
},
duration_ms: track.duration_ms,
uri: track.uri,
}
})
Expand Down
4 changes: 2 additions & 2 deletions app/client/spotify-selection/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@ const SpotifySelectionPage = () => {
executeSpotify('PLAY', { contextUri: uri })
}

const handleTrackPlay = (index: number) => {
const handleTrackPlay = (uri: string) => {
if (selectedPlaylistId) {
executeSpotify('PLAY', {
contextUri: `spotify:playlist:${selectedPlaylistId}`,
offset: { position: index },
offset: { uri },
})
}
}
Expand Down
186 changes: 48 additions & 138 deletions components/Playlist/PlaylistTracksDisplay.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// File: components/Playlist/PlaylistTracksDisplay.tsx
'use client'
import { useCallback, useEffect, useState } from 'react'
import { useWebSocket } from '@/context/WebSocketContext'
Expand All @@ -7,28 +6,10 @@ import Box from '@mui/material/Box'
import Typography from '@mui/material/Typography'
import CircularProgress from '@mui/material/CircularProgress'
import Alert from '@mui/material/Alert'
import IconButton from '@mui/material/IconButton'
import PlayArrowIcon from '@mui/icons-material/PlayArrow'
import PauseIcon from '@mui/icons-material/Pause'
import Table from '@mui/material/Table'
import TableBody from '@mui/material/TableBody'
import TableCell from '@mui/material/TableCell'
import TableContainer from '@mui/material/TableContainer'
import TableHead from '@mui/material/TableHead'
import TableRow from '@mui/material/TableRow'
import List from '@mui/material/List'
import Paper from '@mui/material/Paper'
import Button from '@mui/material/Button'
import { formatDuration } from '@/lib/utils'
import Image from 'next/image'

interface Track {
id: string
name: string
artists: string
albumArt: string | null
duration: number
uri: string
}
import { SpotifyPlaylistItem } from '@/types/core'
import { TrackListItem } from '../Spotify/TrackListItem'

interface PlaylistTracksDisplayProps {
playlistId: string
Expand All @@ -37,65 +18,44 @@ interface PlaylistTracksDisplayProps {
const PlaylistTracksDisplay = ({ playlistId }: PlaylistTracksDisplayProps) => {
const { spotifyData } = useWebSocket()
const { execute: executeSpotify } = useSpotifyCommand()
const [tracks, setTracks] = useState<Track[]>([])
const [tracks, setTracks] = useState<SpotifyPlaylistItem[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [offset, setOffset] = useState(0)
const [total, setTotal] = useState(0)
const limit = 20

const fetchTracks = useCallback(
async (currentOffset: number) => {
try {
setLoading(true)
const response = await fetch(
`/api/spotify/playlists/${playlistId}/tracks?limit=${limit}&offset=${currentOffset}`
)
if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.message || 'Failed to fetch tracks')
}
const data = await response.json()
setTracks(data.tracks)
setTotal(data.total)
setLoading(false)
} catch (err) {
setError(
err instanceof Error ? err.message : 'An unknown error occurred'
)
setLoading(false)
const fetchTracks = useCallback(async () => {
try {
setLoading(true)
const response = await fetch(
`/api/spotify/playlists/${playlistId}/tracks?limit=50`
)
if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.message || 'Failed to fetch tracks')
}
},
[playlistId]
)
const data = await response.json()
setTracks(data.tracks)
setLoading(false)
} catch (err) {
setError(err instanceof Error ? err.message : 'An unknown error occurred')
setLoading(false)
}
}, [playlistId])

useEffect(() => {
fetchTracks(offset)
}, [fetchTracks, offset])
fetchTracks()
}, [fetchTracks])

const handlePlayTrack = (playlistUri: string, position: number) => {
const handlePlayTrack = (playlistUri: string, uri: string) => {
executeSpotify('PLAY', {
contextUri: playlistUri,
offset: { position },
offset: { uri },
})
}

const handlePause = () => {
executeSpotify('PAUSE')
}

const handleNextPage = () => {
if (offset + limit < total) {
setOffset(offset + limit)
}
}

const handlePreviousPage = () => {
if (offset - limit >= 0) {
setOffset(offset - limit)
}
}

if (loading) {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 4 }}>
Expand All @@ -122,79 +82,29 @@ const PlaylistTracksDisplay = ({ playlistId }: PlaylistTracksDisplayProps) => {

return (
<Box>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>Play</TableCell>
<TableCell>Title</TableCell>
<TableCell>Artist</TableCell>
<TableCell>Duration</TableCell>
</TableRow>
</TableHead>
<TableBody>
{tracks.map((track, index) => {
const isPlaying =
spotifyData.playback.is_playing &&
spotifyData.playback.track.id === track.id
const playlistUri = `spotify:playlist:${playlistId}`
return (
<TableRow
key={track.id}
sx={{
backgroundColor: isPlaying ? 'action.selected' : 'inherit',
}}
>
<TableCell>
<IconButton
onClick={() =>
isPlaying
? handlePause()
: handlePlayTrack(playlistUri, offset + index)
}
aria-label={isPlaying ? 'Pause' : 'Play'}
>
{isPlaying ? <PauseIcon /> : <PlayArrowIcon />}
</IconButton>
</TableCell>
<TableCell>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
{track.albumArt && (
<Image
src={track.albumArt}
alt={track.name}
width={40}
height={40}
style={{ marginRight: '8px' }}
/>
)}
{track.name}
</Box>
</TableCell>
<TableCell>{track.artists}</TableCell>
<TableCell>
{formatDuration(track.duration, {
unit: 'milliseconds',
format: 'MM:SS',
})}
</TableCell>
</TableRow>
)
})}
</TableBody>
</Table>
</TableContainer>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mt: 2 }}>
<Button onClick={handlePreviousPage} disabled={offset === 0}>
Previous
</Button>
<Typography>
Showing {offset + 1}-{Math.min(offset + limit, total)} of {total}
</Typography>
<Button onClick={handleNextPage} disabled={offset + limit >= total}>
Next
</Button>
</Box>
<Paper>
<List dense sx={{ width: '100%', bgcolor: 'background.paper', p: 0 }}>
{tracks.map((track) => {
const isPlaying =
spotifyData.playback.is_playing &&
spotifyData.playback.track.id === track.id
const playlistUri = `spotify:playlist:${playlistId}`

return (
<TrackListItem
key={track.id}
track={track}
isSelected={isPlaying}
onClick={() =>
isPlaying
? handlePause()
: handlePlayTrack(playlistUri, track.uri)
}
/>
)
})}
</List>
</Paper>
</Box>
)
}
Expand Down
26 changes: 8 additions & 18 deletions components/Spotify/PlaylistDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
// components/Spotify/PlaylistDetails.tsx
import MusicNote from '@mui/icons-material/MusicNote'
import Box from '@mui/material/Box'
import CircularProgress from '@mui/material/CircularProgress'
import List from '@mui/material/List'
import ListItem from '@mui/material/ListItem'
import ListItemButton from '@mui/material/ListItemButton'
import ListItemText from '@mui/material/ListItemText'
import Paper from '@mui/material/Paper'
import Typography from '@mui/material/Typography'
import { useCallback, useEffect, useState } from 'react'
import InfiniteScroll from 'react-infinite-scroll-component'
import { SpotifyPlaylistItem as Track } from '../../types/core'
import { TrackListItem } from './TrackListItem'

interface PlaylistDetailsProps {
playlistId: string
onTrackPlay: (index: number) => void
onTrackPlay: (uri: string) => void
}

const PlaylistDetails: React.FC<PlaylistDetailsProps> = ({
Expand Down Expand Up @@ -101,19 +97,13 @@ const PlaylistDetails: React.FC<PlaylistDetailsProps> = ({
}
scrollableTarget="scrollable-playlist"
>
<List dense>
<List dense sx={{ width: '100%', bgcolor: 'background.paper', p: 0 }}>
{tracks.map((track, index) => (
<ListItem key={`${track.id}-${index}`} divider disablePadding>
<ListItemButton onClick={() => onTrackPlay(index)}>
<MusicNote
sx={{ mr: 1.5, color: 'text.secondary', fontSize: 20 }}
/>
<ListItemText
primary={track.name}
secondary={`${Array.isArray(track.artists) ? track.artists.map((a) => a.name).join(', ') : track.artists} - ${track.album?.name || 'Unknown Album'}`}
/>
</ListItemButton>
</ListItem>
<TrackListItem
key={`${track.id}-${index}`}
track={track}
onClick={() => onTrackPlay(track.uri)}
/>
))}
</List>
</InfiniteScroll>
Expand Down
75 changes: 75 additions & 0 deletions components/Spotify/TrackListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React from 'react'
import Avatar from '@mui/material/Avatar'
import ListItem from '@mui/material/ListItem'
import ListItemAvatar from '@mui/material/ListItemAvatar'
import ListItemButton from '@mui/material/ListItemButton'
import ListItemText from '@mui/material/ListItemText'
import Typography from '@mui/material/Typography'
import MusicNoteIcon from '@mui/icons-material/MusicNote'
import { formatDuration } from '@/lib/utils'
import { getArtistNames } from '@/lib/spotify'
import { SpotifyPlaylistItem } from '@/types/core'

interface TrackListItemProps {
track: SpotifyPlaylistItem
onClick: () => void
secondaryAction?: React.ReactNode
isSelected?: boolean
}

export const TrackListItem: React.FC<TrackListItemProps> = ({
track,
onClick,
secondaryAction,
isSelected = false,
}) => {
const albumName = track.album?.name || 'Single'
const albumThumbnail = track.album?.images?.at(-1)?.url || ''

return (
<ListItem
divider
disablePadding
sx={{
backgroundColor: isSelected ? 'action.selected' : 'inherit',
}}
secondaryAction={
secondaryAction ||
(!!track.duration_ms && (
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
{formatDuration(track.duration_ms, {
unit: 'milliseconds',
format: 'MM:SS',
})}
</Typography>
))
}
>
<ListItemButton onClick={onClick} sx={{ py: 0.5, px: 1 }}>
<ListItemAvatar sx={{ minWidth: 48 }}>
<Avatar
variant="rounded"
src={albumThumbnail}
alt={track.name}
sx={{ width: 32, height: 32 }}
>
<MusicNoteIcon fontSize="small" />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={track.name}
secondary={`${getArtistNames(track.artists)} • ${albumName}`}
primaryTypographyProps={{
variant: 'body2',
noWrap: true,
fontWeight: 'medium',
}}
secondaryTypographyProps={{
variant: 'caption',
noWrap: true,
}}
/>
</ListItemButton>
</ListItem>
)
}
2 changes: 1 addition & 1 deletion hooks/useSpotifyCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type SpotifyPlayPayload = SpotifyBasePayload & {
playlistUri?: string
contextUri?: string
uri?: string
offset?: { position: number }
offset?: { position?: number; uri?: string }
}

type SpotifyVolumePayload = SpotifyBasePayload & {
Expand Down
Loading
Loading