improve & support for new list standart

This commit is contained in:
SrGooglo 2025-02-05 02:36:51 +00:00
parent a34d165b97
commit ec1e574ce9
3 changed files with 738 additions and 675 deletions

View File

@ -22,373 +22,422 @@ import MusicModel from "@models/music"
import "./index.less" import "./index.less"
const PlaylistTypeDecorators = { const PlaylistTypeDecorators = {
"single": () => <span className="playlistType"> single: () => (
<Icons.MdMusicNote /> <span className="playlistType">
Single <Icons.MdMusicNote />
</span>, Single
"album": () => <span className="playlistType"> </span>
<Icons.MdAlbum /> ),
Album album: () => (
</span>, <span className="playlistType">
"ep": () => <span className="playlistType"> <Icons.MdAlbum />
<Icons.MdAlbum /> Album
EP </span>
</span>, ),
"mix": () => <span className="playlistType"> ep: () => (
<Icons.MdMusicNote /> <span className="playlistType">
Mix <Icons.MdAlbum />
</span>, EP
</span>
),
mix: () => (
<span className="playlistType">
<Icons.MdMusicNote />
Mix
</span>
),
} }
const PlaylistInfo = (props) => { const PlaylistInfo = (props) => {
return <div> return (
<ReactMarkdown <div>
remarkPlugins={[remarkGfm]} <ReactMarkdown
children={props.data.description} remarkPlugins={[remarkGfm]}
/> children={props.data.description}
</div> />
</div>
)
} }
const MoreMenuHandlers = { const MoreMenuHandlers = {
"edit": async (playlist) => { edit: async (playlist) => {},
delete: async (playlist) => {
return antd.Modal.confirm({
title: "Are you sure you want to delete this playlist?",
onOk: async () => {
const result = await MusicModel.deletePlaylist(
playlist._id,
).catch((err) => {
console.log(err)
}, app.message.error("Failed to delete playlist")
"delete": async (playlist) => {
return antd.Modal.confirm({
title: "Are you sure you want to delete this playlist?",
onOk: async () => {
const result = await MusicModel.deletePlaylist(playlist._id).catch((err) => {
console.log(err)
app.message.error("Failed to delete playlist") return null
})
return null if (result) {
}) app.navigation.goToMusic()
}
if (result) { },
app.navigation.goToMusic() })
} },
}
})
}
} }
const PlaylistView = (props) => { const PlaylistView = (props) => {
const [playlist, setPlaylist] = React.useState(props.playlist) const [playlist, setPlaylist] = React.useState(props.playlist)
const [searchResults, setSearchResults] = React.useState(null) const [searchResults, setSearchResults] = React.useState(null)
const [owningPlaylist, setOwningPlaylist] = React.useState(checkUserIdIsSelf(props.playlist?.user_id)) const [owningPlaylist, setOwningPlaylist] = React.useState(
checkUserIdIsSelf(props.playlist?.user_id),
)
const moreMenuItems = React.useMemo(() => { const moreMenuItems = React.useMemo(() => {
const items = [{ const items = [
key: "edit", {
label: "Edit", key: "edit",
}] label: "Edit",
},
]
if (!playlist.type || playlist.type === "playlist") { if (!playlist.type || playlist.type === "playlist") {
if (checkUserIdIsSelf(playlist.user_id)) { if (checkUserIdIsSelf(playlist.user_id)) {
items.push({ items.push({
key: "delete", key: "delete",
label: "Delete", label: "Delete",
}) })
} }
} }
return items return items
}) })
const contextValues = { const contextValues = {
playlist_data: playlist, playlist_data: playlist,
owning_playlist: owningPlaylist, owning_playlist: owningPlaylist,
add_track: (track) => { add_track: (track) => {},
remove_track: (track) => {},
}
}, let debounceSearch = null
remove_track: (track) => {
} const makeSearch = (value) => {
} //TODO: Implement me using API
return app.message.info("Not implemented yet...")
}
let debounceSearch = null const handleOnSearchChange = (value) => {
debounceSearch = setTimeout(() => {
makeSearch(value)
}, 500)
}
const makeSearch = (value) => { const handleOnSearchEmpty = () => {
//TODO: Implement me using API if (debounceSearch) {
return app.message.info("Not implemented yet...") clearTimeout(debounceSearch)
} }
const handleOnSearchChange = (value) => { setSearchResults(null)
debounceSearch = setTimeout(() => { }
makeSearch(value)
}, 500)
}
const handleOnSearchEmpty = () => { const handleOnClickPlaylistPlay = () => {
if (debounceSearch) { app.cores.player.start(playlist.items)
clearTimeout(debounceSearch) }
}
setSearchResults(null) const handleOnClickViewDetails = () => {
} app.layout.modal.open("playlist_info", PlaylistInfo, {
props: {
data: playlist,
},
})
}
const handleOnClickPlaylistPlay = () => { const handleOnClickTrack = (track) => {
app.cores.player.start(playlist.list) // search index of track
} const index = playlist.items.findIndex((item) => {
return item._id === track._id
})
const handleOnClickViewDetails = () => { if (index === -1) {
app.layout.modal.open("playlist_info", PlaylistInfo, { return
props: { }
data: playlist
}
})
}
const handleOnClickTrack = (track) => { // check if clicked track is currently playing
// search index of track if (app.cores.player.state.track_manifest?._id === track._id) {
const index = playlist.list.findIndex((item) => { app.cores.player.playback.toggle()
return item._id === track._id } else {
}) app.cores.player.start(playlist.items, {
startIndex: index,
})
}
}
if (index === -1) { const handleUpdateTrackLike = (track_id, liked) => {
return setPlaylist((prev) => {
} const index = prev.list.findIndex((item) => {
return item._id === track_id
})
// check if clicked track is currently playing if (index !== -1) {
if (app.cores.player.state.track_manifest?._id === track._id) { const newState = {
app.cores.player.playback.toggle() ...prev,
} else { }
app.cores.player.start(playlist.list, {
startIndex: index
})
}
}
const handleUpdateTrackLike = (track_id, liked) => { newState.list[index].liked = liked
setPlaylist((prev) => {
const index = prev.list.findIndex((item) => {
return item._id === track_id
})
if (index !== -1) { return newState
const newState = { }
...prev,
}
newState.list[index].liked = liked return prev
})
}
return newState const handleTrackChangeState = (track_id, update) => {
} setPlaylist((prev) => {
const index = prev.list.findIndex((item) => {
return item._id === track_id
})
return prev if (index !== -1) {
}) const newState = {
} ...prev,
}
const handleTrackChangeState = (track_id, update) => { newState.list[index] = {
setPlaylist((prev) => { ...newState.list[index],
const index = prev.list.findIndex((item) => { ...update,
return item._id === track_id }
})
if (index !== -1) { return newState
const newState = { }
...prev,
}
newState.list[index] = { return prev
...newState.list[index], })
...update }
}
return newState const handleMoreMenuClick = async (e) => {
} const handler = MoreMenuHandlers[e.key]
return prev if (typeof handler !== "function") {
}) throw new Error(`Invalid menu handler [${e.key}]`)
} }
const handleMoreMenuClick = async (e) => { return await handler(playlist)
const handler = MoreMenuHandlers[e.key] }
if (typeof handler !== "function") { useWsEvents(
throw new Error(`Invalid menu handler [${e.key}]`) {
} "music:track:toggle:like": (data) => {
handleUpdateTrackLike(data.track_id, data.action === "liked")
},
},
{
socketName: "music",
},
)
return await handler(playlist) React.useEffect(() => {
} setPlaylist(props.playlist)
setOwningPlaylist(checkUserIdIsSelf(props.playlist?.user_id))
}, [props.playlist])
useWsEvents({ if (!playlist) {
"music:track:toggle:like": (data) => { return <antd.Skeleton active />
handleUpdateTrackLike(data.track_id, data.action === "liked") }
}
}, {
socketName: "music",
})
React.useEffect(() => { const playlistType = playlist.type?.toLowerCase() ?? "playlist"
setPlaylist(props.playlist)
setOwningPlaylist(checkUserIdIsSelf(props.playlist?.user_id))
}, [props.playlist])
if (!playlist) { return (
return <antd.Skeleton active /> <PlaylistContext.Provider value={contextValues}>
} <WithPlayerContext>
<div className={classnames("playlist_view")}>
{!props.noHeader && (
<div className="play_info_wrapper">
<div className="play_info">
<div className="play_info_cover">
<ImageViewer
src={
playlist.cover ??
playlist?.thumbnail ??
"/assets/no_song.png"
}
/>
</div>
const playlistType = playlist.type?.toLowerCase() ?? "playlist" <div className="play_info_details">
<div className="play_info_title">
{playlist.service === "tidal" && (
<Icons.SiTidal />
)}
{typeof playlist.title ===
"function" ? (
playlist.title
) : (
<h1>{playlist.title}</h1>
)}
</div>
return <PlaylistContext.Provider value={contextValues}> <div className="play_info_statistics">
<WithPlayerContext> {playlistType &&
<div PlaylistTypeDecorators[
className={classnames( playlistType
"playlist_view", ] && (
playlistType, <div className="play_info_statistics_item">
)} {PlaylistTypeDecorators[
> playlistType
{ ]()}
!props.noHeader && <div className="play_info_wrapper"> </div>
<div className="play_info"> )}
<div className="play_info_cover"> <div className="play_info_statistics_item">
<ImageViewer src={playlist.cover ?? playlist?.thumbnail ?? "/assets/no_song.png"} /> <p>
</div> <Icons.MdLibraryMusic />{" "}
{props.length ??
playlist.total_length ??
playlist.items.length}{" "}
Items
</p>
</div>
{playlist.publisher && (
<div className="play_info_statistics_item">
<p
onClick={() => {
app.navigation.goToAccount(
playlist.publisher
.username,
)
}}
>
<Icons.MdPerson />
Publised by{" "}
<a>
{
playlist.publisher
.username
}
</a>
</p>
</div>
)}
</div>
<div className="play_info_details"> <div className="play_info_actions">
<div className="play_info_title"> <antd.Button
{ type="primary"
playlist.service === "tidal" && <Icons.SiTidal /> shape="rounded"
} size="large"
{ onClick={handleOnClickPlaylistPlay}
typeof playlist.title === "function" ? >
playlist.title : <Icons.MdPlayArrow />
<h1>{playlist.title}</h1> Play
} </antd.Button>
</div>
<div className="play_info_statistics"> {playlist.description && (
{ <antd.Button
playlistType && PlaylistTypeDecorators[playlistType] && <div className="play_info_statistics_item"> icon={<Icons.MdInfo />}
{ onClick={
PlaylistTypeDecorators[playlistType]() handleOnClickViewDetails
} }
</div> />
} )}
<div className="play_info_statistics_item">
<p>
<Icons.MdLibraryMusic /> {props.length ?? playlist.total_length ?? playlist.list.length} Items
</p>
</div>
{
playlist.publisher && <div className="play_info_statistics_item">
<p
onClick={() => {
app.navigation.goToAccount(playlist.publisher.username)
}}
>
<Icons.MdPerson />
Publised by <a>{playlist.publisher.username}</a> {owningPlaylist && (
</p> <antd.Dropdown
</div> trigger={["click"]}
} placement="bottom"
</div> menu={{
items: moreMenuItems,
onClick:
handleMoreMenuClick,
}}
>
<antd.Button
icon={<Icons.MdMoreVert />}
/>
</antd.Dropdown>
)}
</div>
</div>
</div>
</div>
)}
<div className="play_info_actions"> <div className="list">
<antd.Button {!props.noHeader && playlist.items.length > 0 && (
type="primary" <div className="list_header">
shape="rounded" <h1>
size="large" <Icons.MdPlaylistPlay /> Tracks
onClick={handleOnClickPlaylistPlay} </h1>
>
<Icons.MdPlayArrow />
Play
</antd.Button>
{ <SearchButton
playlist.description && <antd.Button onChange={handleOnSearchChange}
icon={<Icons.MdInfo />} onEmpty={handleOnSearchEmpty}
onClick={handleOnClickViewDetails} disabled
/> />
} </div>
)}
{ {playlist.items.length === 0 && (
owningPlaylist && <antd.Empty
<antd.Dropdown description={
trigger={["click"]} <>
placement="bottom" <Icons.MdLibraryMusic /> This playlist
menu={{ its empty!
items: moreMenuItems, </>
onClick: handleMoreMenuClick }
}} />
> )}
<antd.Button
icon={<Icons.MdMoreVert />}
/>
</antd.Dropdown>
} {searchResults &&
</div> searchResults.map((item) => {
</div> return (
</div> <MusicTrack
</div> key={item._id}
} order={item._id}
track={item}
onClickPlayBtn={() =>
handleOnClickTrack(item)
}
changeState={(update) =>
handleTrackChangeState(
item._id,
update,
)
}
/>
)
})}
<div className="list"> {!searchResults && playlist.items.length > 0 && (
{ <LoadMore
playlist.list.length > 0 && <div className="list_header"> className="list_content"
<h1> loadingComponent={() => <antd.Skeleton />}
<Icons.MdPlaylistPlay /> Tracks onBottom={props.onLoadMore}
</h1> hasMore={props.hasMore}
>
<SearchButton <WithPlayerContext>
onChange={handleOnSearchChange} {playlist.items.map((item, index) => {
onEmpty={handleOnSearchEmpty} return (
disabled <MusicTrack
/> order={index + 1}
</div> track={item}
} onClickPlayBtn={() =>
handleOnClickTrack(item)
{ }
playlist.list.length === 0 && <antd.Empty changeState={(update) =>
description={ handleTrackChangeState(
<> item._id,
<Icons.MdLibraryMusic /> This playlist its empty! update,
</> )
} }
/> />
} )
})}
{ </WithPlayerContext>
searchResults && searchResults.map((item) => { </LoadMore>
return <MusicTrack )}
key={item._id} </div>
order={item._id} </div>
track={item} </WithPlayerContext>
onClickPlayBtn={() => handleOnClickTrack(item)} </PlaylistContext.Provider>
changeState={(update) => handleTrackChangeState(item._id, update)} )
/>
})
}
{
!searchResults && playlist.list.length > 0 && <LoadMore
className="list_content"
loadingComponent={() => <antd.Skeleton />}
onBottom={props.onLoadMore}
hasMore={props.hasMore}
>
<WithPlayerContext>
{
playlist.list.map((item, index) => {
return <MusicTrack
order={index + 1}
track={item}
onClickPlayBtn={() => handleOnClickTrack(item)}
changeState={(update) => handleTrackChangeState(item._id, update)}
/>
})
}
</WithPlayerContext>
</LoadMore>
}
</div>
</div>
</WithPlayerContext>
</PlaylistContext.Provider>
} }
export default PlaylistView export default PlaylistView

View File

@ -1,245 +1,244 @@
@import "@styles/vars.less"; @import "@styles/vars.less";
html { html {
&.mobile { &.mobile {
.playlist_view { .playlist_view {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
padding: 0 10px !important; padding: 0 10px !important;
.play_info_wrapper { .play_info_wrapper {
position: relative; position: relative;
width: 100%; width: 100%;
.play_info { .play_info {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
.play_info_details { .play_info_details {
width: 100%; width: 100%;
} }
.play_info_cover { .play_info_cover {
width: 30vh !important; width: 30vh !important;
height: 30vh !important; height: 30vh !important;
img { img {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: contain; object-fit: contain;
} }
} }
} }
} }
} }
} }
} }
.playlist_view { .playlist_view {
position: relative; position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
gap: 20px; gap: 20px;
.play_info_wrapper { .play_info_wrapper {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
top: 0; top: 0;
left: 0; left: 0;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 100%; width: 100%;
z-index: 45; z-index: 45;
color: var(--text-color); color: var(--text-color);
.play_info { .play_info {
display: inline-flex; display: inline-flex;
flex-direction: row; flex-direction: row;
gap: 20px; gap: 20px;
align-self: center; align-self: center;
width: 100%; width: 100%;
height: 100%; height: 100%;
padding: 20px; padding: 20px;
overflow: hidden; overflow: hidden;
background-color: var(--background-color-accent); background-color: var(--background-color-accent);
border-radius: 12px; border-radius: 12px;
.play_info_cover { .play_info_cover {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
align-self: center; align-self: center;
height: 15vh !important; height: 15vh !important;
width: 15vh !important; width: 15vh !important;
min-height: 15vh; min-height: 15vh;
min-width: 15vh; min-width: 15vh;
max-width: 400px; max-width: 400px;
max-height: 400px; max-height: 400px;
background-color: black; background-color: black;
border-radius: 12px; border-radius: 12px;
overflow: hidden; overflow: hidden;
img { img {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: contain; object-fit: contain;
} }
} }
.play_info_details { .play_info_details {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 90%; width: 90%;
gap: 10px; gap: 10px;
.play_info_title { .play_info_title {
display: inline-flex; display: inline-flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
font-size: 1.2rem; font-size: 1.2rem;
font-family: "Space Grotesk", sans-serif; font-family: "Space Grotesk", sans-serif;
h1 { h1 {
margin: 0; margin: 0;
font-weight: 600; font-weight: 600;
word-break: break-all; word-break: break-all;
} }
} }
.play_info_description { .play_info_description {
font-size: 0.8rem; font-size: 0.8rem;
font-weight: 400; font-weight: 400;
max-height: 10vh; max-height: 10vh;
text-overflow: ellipsis; text-overflow: ellipsis;
p { p {
margin: 0; margin: 0;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
} }
} }
.play_info_statistics { .play_info_statistics {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background-color: var(--background-color-primary); background-color: var(--background-color-primary);
padding: 20px; padding: 20px;
border-radius: 8px; border-radius: 8px;
.play_info_statistics_item { .play_info_statistics_item {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
margin-bottom: 10px; margin-bottom: 10px;
h1, h1,
h2, h2,
h3, h3,
h4, h4,
h5, h5,
h6, h6,
p, p,
span { span {
margin: 0; margin: 0;
} }
.play_info_statistics_item_icon { .play_info_statistics_item_icon {
margin-right: 10px; margin-right: 10px;
} }
.play_info_statistics_item_text { .play_info_statistics_item_text {
font-size: 0.8rem; font-size: 0.8rem;
font-weight: 400; font-weight: 400;
} }
&:last-child { &:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
} }
}
} .play_info_actions {
display: inline-flex;
flex-direction: row;
.play_info_actions { align-items: center;
display: inline-flex;
flex-direction: row;
align-items: center; gap: 10px;
}
}
}
}
gap: 10px; .list {
} display: flex;
} flex-direction: column;
}
}
.list { color: var(--text-color);
display: flex;
flex-direction: column;
color: var(--text-color); gap: 10px;
gap: 10px; width: 100%;
width: 100%; h1 {
margin: 0;
}
h1 { .list_header {
margin: 0; display: flex;
} flex-direction: row;
.list_header { align-items: center;
display: flex; justify-content: space-between;
flex-direction: row; }
align-items: center; .list_content {
justify-content: space-between; display: flex;
} flex-direction: column;
.list_content { gap: 7px;
display: flex; }
flex-direction: column; }
gap: 10px;
}
}
} }

View File

@ -15,221 +15,236 @@ import { Context as PlaylistContext } from "@contexts/WithPlaylistContext"
import "./index.less" import "./index.less"
const handlers = { const handlers = {
"like": async (ctx, track) => { like: async (ctx, track) => {
await MusicModel.toggleItemFavourite("track", track._id, true) await MusicModel.toggleItemFavourite("track", track._id, true)
ctx.changeState({ ctx.changeState({
liked: true, liked: true,
}) })
ctx.closeMenu() ctx.closeMenu()
}, },
"unlike": async (ctx, track) => { unlike: async (ctx, track) => {
await MusicModel.toggleItemFavourite("track", track._id, false) await MusicModel.toggleItemFavourite("track", track._id, false)
ctx.changeState({ ctx.changeState({
liked: false, liked: false,
}) })
ctx.closeMenu() ctx.closeMenu()
}, },
add_to_playlist: async (ctx, track) => {},
add_to_queue: async (ctx, track) => {
await app.cores.player.queue.add(track)
},
play_next: async (ctx, track) => {
await app.cores.player.queue.add(track, { next: true })
},
} }
const Track = (props) => { const Track = (props) => {
const [{ const [{ loading, track_manifest, playback_status }] =
loading, usePlayerStateContext()
track_manifest,
playback_status,
}] = usePlayerStateContext()
const playlist_ctx = React.useContext(PlaylistContext) const playlist_ctx = React.useContext(PlaylistContext)
const [moreMenuOpened, setMoreMenuOpened] = React.useState(false) const [moreMenuOpened, setMoreMenuOpened] = React.useState(false)
const isCurrent = track_manifest?._id === props.track._id const isCurrent = track_manifest?._id === props.track._id
const isPlaying = isCurrent && playback_status === "playing" const isPlaying = isCurrent && playback_status === "playing"
const handleClickPlayBtn = React.useCallback(() => { const handleClickPlayBtn = React.useCallback(() => {
if (typeof props.onClickPlayBtn === "function") { if (typeof props.onClickPlayBtn === "function") {
props.onClickPlayBtn(props.track) props.onClickPlayBtn(props.track)
} else { } else {
console.warn("Searcher: onClick is not a function, using default action...") console.warn(
if (!isCurrent) { "Searcher: onClick is not a function, using default action...",
app.cores.player.start(props.track) )
} else { if (!isCurrent) {
app.cores.player.playback.toggle() app.cores.player.start(props.track)
} } else {
} app.cores.player.playback.toggle()
}) }
}
})
const handleOnClickItem = () => { const handleOnClickItem = () => {
if (app.isMobile) { if (app.isMobile) {
handleClickPlayBtn() handleClickPlayBtn()
} }
} }
const handleMoreMenuOpen = () => { const handleMoreMenuOpen = () => {
if (app.isMobile) { if (app.isMobile) {
return return
} }
return setMoreMenuOpened((prev) => { return setMoreMenuOpened((prev) => {
return !prev return !prev
}) })
} }
const handleMoreMenuItemClick = (e) => { const handleMoreMenuItemClick = (e) => {
const { key } = e const { key } = e
if (typeof handlers[key] === "function") { if (typeof handlers[key] === "function") {
return handlers[key]( return handlers[key](
{ {
closeMenu: () => { closeMenu: () => {
setMoreMenuOpened(false) setMoreMenuOpened(false)
}, },
changeState: props.changeState, changeState: props.changeState,
}, },
props.track props.track,
) )
} }
} }
const moreMenuItems = React.useMemo(() => { const moreMenuItems = React.useMemo(() => {
const items = [ const items = [
{ {
key: "like", key: "like",
icon: <Icons.MdFavorite />, icon: <Icons.MdFavorite />,
label: "Like", label: "Like",
}, },
{ {
key: "share", key: "share",
icon: <Icons.MdShare />, icon: <Icons.MdShare />,
label: "Share", label: "Share",
disabled: true, disabled: true,
}, },
{ {
key: "add_to_playlist", key: "add_to_playlist",
icon: <Icons.MdPlaylistAdd />, icon: <Icons.MdPlaylistAdd />,
label: "Add to playlist", label: "Add to playlist",
disabled: true, disabled: true,
}, },
{ {
key: "add_to_queue", type: "divider",
icon: <Icons.MdQueueMusic />, },
label: "Add to queue", {
disabled: true, key: "add_to_queue",
} icon: <Icons.MdQueueMusic />,
] label: "Add to queue",
},
{
key: "play_next",
icon: <Icons.MdSkipNext />,
label: "Play next",
},
]
if (props.track.liked) { if (props.track.liked) {
items[0] = { items[0] = {
key: "unlike", key: "unlike",
icon: <Icons.MdFavorite />, icon: <Icons.MdFavorite />,
label: "Unlike", label: "Unlike",
} }
} }
if (playlist_ctx) { if (playlist_ctx) {
if (playlist_ctx.owning_playlist) { if (playlist_ctx.owning_playlist) {
items.push({ items.push({
type: "divider", type: "divider",
}) })
items.push({ items.push({
key: "remove_from_playlist", key: "remove_from_playlist",
icon: <Icons.MdPlaylistRemove />, icon: <Icons.MdPlaylistRemove />,
label: "Remove from playlist", label: "Remove from playlist",
}) })
} }
} }
return items return items
}, [props.track]) }, [props.track])
return <div return (
id={props.track._id} <div
className={classnames( id={props.track._id}
"music-track", className={classnames("music-track", {
{ ["current"]: isCurrent,
["current"]: isCurrent, ["playing"]: isPlaying,
["playing"]: isPlaying, ["loading"]: isCurrent && loading,
["loading"]: isCurrent && loading })}
} style={{
)} "--cover_average-color": RGBStringToValues(
style={{ track_manifest?.cover_analysis?.rgb,
"--cover_average-color": RGBStringToValues(track_manifest?.cover_analysis?.rgb), ),
}} }}
onClick={handleOnClickItem} onClick={handleOnClickItem}
> >
<div <div className="music-track_background" />
className="music-track_background"
/>
<div className="music-track_content"> <div className="music-track_content">
{ {!app.isMobile && (
!app.isMobile && <div className={classnames( <div
"music-track_actions", className={classnames("music-track_actions", {
{ ["withOrder"]: props.order !== undefined,
["withOrder"]: props.order !== undefined, })}
} >
)}> <div className="music-track_action">
<div className="music-track_action"> <span className="music-track_orderIndex">
<span className="music-track_orderIndex"> {props.order}
{ </span>
props.order <antd.Button
} type="primary"
</span> shape="circle"
<antd.Button icon={
type="primary" isPlaying ? (
shape="circle" <Icons.MdPause />
icon={isPlaying ? <Icons.MdPause /> : <Icons.MdPlayArrow />} ) : (
onClick={handleClickPlayBtn} <Icons.MdPlayArrow />
/> )
</div> }
</div> onClick={handleClickPlayBtn}
} />
</div>
</div>
)}
<div className="music-track_cover"> <div className="music-track_cover">
<ImageViewer src={props.track.cover ?? props.track.thumbnail} /> <ImageViewer
</div> src={props.track.cover ?? props.track.thumbnail}
/>
</div>
<div className="music-track_details"> <div className="music-track_details">
<div className="music-track_title"> <div className="music-track_title">
<span> <span>
{ {props.track.service === "tidal" && (
props.track.service === "tidal" && <Icons.SiTidal /> <Icons.SiTidal />
} )}
{ {props.track.title}
props.track.title </span>
} </div>
</span> <div className="music-track_artist">
</div> <span>
<div className="music-track_artist"> {Array.isArray(props.track.artists)
<span> ? props.track.artists.join(", ")
{ : props.track.artist}
Array.isArray(props.track.artists) ? props.track.artists.join(", ") : props.track.artist </span>
} </div>
</span> </div>
</div>
</div>
<div className="music-track_right_actions"> <div className="music-track_right_actions">
<antd.Dropdown <antd.Dropdown
menu={{ menu={{
items: moreMenuItems, items: moreMenuItems,
onClick: handleMoreMenuItemClick onClick: handleMoreMenuItemClick,
}} }}
onOpenChange={handleMoreMenuOpen} onOpenChange={handleMoreMenuOpen}
open={moreMenuOpened} open={moreMenuOpened}
trigger={["click"]} trigger={["click"]}
> >
<antd.Button <antd.Button
type="ghost" type="ghost"
size="large" size="large"
icon={<Icons.IoMdMore />} icon={<Icons.IoMdMore />}
/> />
</antd.Dropdown> </antd.Dropdown>
</div> </div>
</div> </div>
</div> </div>
)
} }
export default Track export default Track