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"
const PlaylistTypeDecorators = {
"single": () => <span className="playlistType">
<Icons.MdMusicNote />
Single
</span>,
"album": () => <span className="playlistType">
<Icons.MdAlbum />
Album
</span>,
"ep": () => <span className="playlistType">
<Icons.MdAlbum />
EP
</span>,
"mix": () => <span className="playlistType">
<Icons.MdMusicNote />
Mix
</span>,
single: () => (
<span className="playlistType">
<Icons.MdMusicNote />
Single
</span>
),
album: () => (
<span className="playlistType">
<Icons.MdAlbum />
Album
</span>
),
ep: () => (
<span className="playlistType">
<Icons.MdAlbum />
EP
</span>
),
mix: () => (
<span className="playlistType">
<Icons.MdMusicNote />
Mix
</span>
),
}
const PlaylistInfo = (props) => {
return <div>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
children={props.data.description}
/>
</div>
return (
<div>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
children={props.data.description}
/>
</div>
)
}
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)
},
"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")
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 [playlist, setPlaylist] = React.useState(props.playlist)
const [searchResults, setSearchResults] = React.useState(null)
const [owningPlaylist, setOwningPlaylist] = React.useState(checkUserIdIsSelf(props.playlist?.user_id))
const [playlist, setPlaylist] = React.useState(props.playlist)
const [searchResults, setSearchResults] = React.useState(null)
const [owningPlaylist, setOwningPlaylist] = React.useState(
checkUserIdIsSelf(props.playlist?.user_id),
)
const moreMenuItems = React.useMemo(() => {
const items = [{
key: "edit",
label: "Edit",
}]
const moreMenuItems = React.useMemo(() => {
const items = [
{
key: "edit",
label: "Edit",
},
]
if (!playlist.type || playlist.type === "playlist") {
if (checkUserIdIsSelf(playlist.user_id)) {
items.push({
key: "delete",
label: "Delete",
})
}
}
if (!playlist.type || playlist.type === "playlist") {
if (checkUserIdIsSelf(playlist.user_id)) {
items.push({
key: "delete",
label: "Delete",
})
}
}
return items
})
return items
})
const contextValues = {
playlist_data: playlist,
owning_playlist: owningPlaylist,
add_track: (track) => {
const contextValues = {
playlist_data: playlist,
owning_playlist: owningPlaylist,
add_track: (track) => {},
remove_track: (track) => {},
}
},
remove_track: (track) => {
let debounceSearch = null
}
}
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) => {
//TODO: Implement me using API
return app.message.info("Not implemented yet...")
}
const handleOnSearchEmpty = () => {
if (debounceSearch) {
clearTimeout(debounceSearch)
}
const handleOnSearchChange = (value) => {
debounceSearch = setTimeout(() => {
makeSearch(value)
}, 500)
}
setSearchResults(null)
}
const handleOnSearchEmpty = () => {
if (debounceSearch) {
clearTimeout(debounceSearch)
}
const handleOnClickPlaylistPlay = () => {
app.cores.player.start(playlist.items)
}
setSearchResults(null)
}
const handleOnClickViewDetails = () => {
app.layout.modal.open("playlist_info", PlaylistInfo, {
props: {
data: playlist,
},
})
}
const handleOnClickPlaylistPlay = () => {
app.cores.player.start(playlist.list)
}
const handleOnClickTrack = (track) => {
// search index of track
const index = playlist.items.findIndex((item) => {
return item._id === track._id
})
const handleOnClickViewDetails = () => {
app.layout.modal.open("playlist_info", PlaylistInfo, {
props: {
data: playlist
}
})
}
if (index === -1) {
return
}
const handleOnClickTrack = (track) => {
// search index of track
const index = playlist.list.findIndex((item) => {
return item._id === track._id
})
// check if clicked track is currently playing
if (app.cores.player.state.track_manifest?._id === track._id) {
app.cores.player.playback.toggle()
} else {
app.cores.player.start(playlist.items, {
startIndex: index,
})
}
}
if (index === -1) {
return
}
const handleUpdateTrackLike = (track_id, liked) => {
setPlaylist((prev) => {
const index = prev.list.findIndex((item) => {
return item._id === track_id
})
// check if clicked track is currently playing
if (app.cores.player.state.track_manifest?._id === track._id) {
app.cores.player.playback.toggle()
} else {
app.cores.player.start(playlist.list, {
startIndex: index
})
}
}
if (index !== -1) {
const newState = {
...prev,
}
const handleUpdateTrackLike = (track_id, liked) => {
setPlaylist((prev) => {
const index = prev.list.findIndex((item) => {
return item._id === track_id
})
newState.list[index].liked = liked
if (index !== -1) {
const newState = {
...prev,
}
return newState
}
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) => {
setPlaylist((prev) => {
const index = prev.list.findIndex((item) => {
return item._id === track_id
})
newState.list[index] = {
...newState.list[index],
...update,
}
if (index !== -1) {
const newState = {
...prev,
}
return newState
}
newState.list[index] = {
...newState.list[index],
...update
}
return prev
})
}
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) => {
const handler = MoreMenuHandlers[e.key]
return await handler(playlist)
}
if (typeof handler !== "function") {
throw new Error(`Invalid menu handler [${e.key}]`)
}
useWsEvents(
{
"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({
"music:track:toggle:like": (data) => {
handleUpdateTrackLike(data.track_id, data.action === "liked")
}
}, {
socketName: "music",
})
if (!playlist) {
return <antd.Skeleton active />
}
React.useEffect(() => {
setPlaylist(props.playlist)
setOwningPlaylist(checkUserIdIsSelf(props.playlist?.user_id))
}, [props.playlist])
const playlistType = playlist.type?.toLowerCase() ?? "playlist"
if (!playlist) {
return <antd.Skeleton active />
}
return (
<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}>
<WithPlayerContext>
<div
className={classnames(
"playlist_view",
playlistType,
)}
>
{
!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>
<div className="play_info_statistics">
{playlistType &&
PlaylistTypeDecorators[
playlistType
] && (
<div className="play_info_statistics_item">
{PlaylistTypeDecorators[
playlistType
]()}
</div>
)}
<div className="play_info_statistics_item">
<p>
<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_title">
{
playlist.service === "tidal" && <Icons.SiTidal />
}
{
typeof playlist.title === "function" ?
playlist.title :
<h1>{playlist.title}</h1>
}
</div>
<div className="play_info_actions">
<antd.Button
type="primary"
shape="rounded"
size="large"
onClick={handleOnClickPlaylistPlay}
>
<Icons.MdPlayArrow />
Play
</antd.Button>
<div className="play_info_statistics">
{
playlistType && PlaylistTypeDecorators[playlistType] && <div className="play_info_statistics_item">
{
PlaylistTypeDecorators[playlistType]()
}
</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 />
{playlist.description && (
<antd.Button
icon={<Icons.MdInfo />}
onClick={
handleOnClickViewDetails
}
/>
)}
Publised by <a>{playlist.publisher.username}</a>
</p>
</div>
}
</div>
{owningPlaylist && (
<antd.Dropdown
trigger={["click"]}
placement="bottom"
menu={{
items: moreMenuItems,
onClick:
handleMoreMenuClick,
}}
>
<antd.Button
icon={<Icons.MdMoreVert />}
/>
</antd.Dropdown>
)}
</div>
</div>
</div>
</div>
)}
<div className="play_info_actions">
<antd.Button
type="primary"
shape="rounded"
size="large"
onClick={handleOnClickPlaylistPlay}
>
<Icons.MdPlayArrow />
Play
</antd.Button>
<div className="list">
{!props.noHeader && playlist.items.length > 0 && (
<div className="list_header">
<h1>
<Icons.MdPlaylistPlay /> Tracks
</h1>
{
playlist.description && <antd.Button
icon={<Icons.MdInfo />}
onClick={handleOnClickViewDetails}
/>
}
<SearchButton
onChange={handleOnSearchChange}
onEmpty={handleOnSearchEmpty}
disabled
/>
</div>
)}
{
owningPlaylist &&
<antd.Dropdown
trigger={["click"]}
placement="bottom"
menu={{
items: moreMenuItems,
onClick: handleMoreMenuClick
}}
>
<antd.Button
icon={<Icons.MdMoreVert />}
/>
</antd.Dropdown>
{playlist.items.length === 0 && (
<antd.Empty
description={
<>
<Icons.MdLibraryMusic /> This playlist
its empty!
</>
}
/>
)}
}
</div>
</div>
</div>
</div>
}
{searchResults &&
searchResults.map((item) => {
return (
<MusicTrack
key={item._id}
order={item._id}
track={item}
onClickPlayBtn={() =>
handleOnClickTrack(item)
}
changeState={(update) =>
handleTrackChangeState(
item._id,
update,
)
}
/>
)
})}
<div className="list">
{
playlist.list.length > 0 && <div className="list_header">
<h1>
<Icons.MdPlaylistPlay /> Tracks
</h1>
<SearchButton
onChange={handleOnSearchChange}
onEmpty={handleOnSearchEmpty}
disabled
/>
</div>
}
{
playlist.list.length === 0 && <antd.Empty
description={
<>
<Icons.MdLibraryMusic /> This playlist its empty!
</>
}
/>
}
{
searchResults && searchResults.map((item) => {
return <MusicTrack
key={item._id}
order={item._id}
track={item}
onClickPlayBtn={() => handleOnClickTrack(item)}
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>
{!searchResults && playlist.items.length > 0 && (
<LoadMore
className="list_content"
loadingComponent={() => <antd.Skeleton />}
onBottom={props.onLoadMore}
hasMore={props.hasMore}
>
<WithPlayerContext>
{playlist.items.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";
html {
&.mobile {
.playlist_view {
display: flex;
flex-direction: column;
&.mobile {
.playlist_view {
display: flex;
flex-direction: column;
width: 100%;
width: 100%;
padding: 0 10px !important;
padding: 0 10px !important;
.play_info_wrapper {
position: relative;
.play_info_wrapper {
position: relative;
width: 100%;
width: 100%;
.play_info {
display: flex;
flex-direction: column;
.play_info {
display: flex;
flex-direction: column;
width: 100%;
width: 100%;
.play_info_details {
width: 100%;
}
.play_info_details {
width: 100%;
}
.play_info_cover {
width: 30vh !important;
height: 30vh !important;
.play_info_cover {
width: 30vh !important;
height: 30vh !important;
img {
width: 100%;
height: 100%;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
}
}
}
}
object-fit: contain;
}
}
}
}
}
}
}
.playlist_view {
position: relative;
position: relative;
display: flex;
flex-direction: column;
display: flex;
flex-direction: column;
width: 100%;
width: 100%;
gap: 20px;
gap: 20px;
.play_info_wrapper {
display: flex;
flex-direction: column;
.play_info_wrapper {
display: flex;
flex-direction: column;
top: 0;
left: 0;
top: 0;
left: 0;
align-items: center;
justify-content: center;
align-items: center;
justify-content: center;
width: 100%;
width: 100%;
z-index: 45;
z-index: 45;
color: var(--text-color);
color: var(--text-color);
.play_info {
display: inline-flex;
flex-direction: row;
.play_info {
display: inline-flex;
flex-direction: row;
gap: 20px;
gap: 20px;
align-self: center;
align-self: center;
width: 100%;
height: 100%;
width: 100%;
height: 100%;
padding: 20px;
padding: 20px;
overflow: hidden;
overflow: hidden;
background-color: var(--background-color-accent);
border-radius: 12px;
background-color: var(--background-color-accent);
border-radius: 12px;
.play_info_cover {
display: flex;
flex-direction: column;
.play_info_cover {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
align-items: center;
justify-content: center;
align-self: center;
align-self: center;
height: 15vh !important;
width: 15vh !important;
height: 15vh !important;
width: 15vh !important;
min-height: 15vh;
min-width: 15vh;
min-height: 15vh;
min-width: 15vh;
max-width: 400px;
max-height: 400px;
max-width: 400px;
max-height: 400px;
background-color: black;
border-radius: 12px;
background-color: black;
border-radius: 12px;
overflow: hidden;
overflow: hidden;
img {
width: 100%;
height: 100%;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
object-fit: contain;
}
}
.play_info_details {
display: flex;
flex-direction: column;
.play_info_details {
display: flex;
flex-direction: column;
width: 90%;
width: 90%;
gap: 10px;
gap: 10px;
.play_info_title {
display: inline-flex;
flex-direction: row;
.play_info_title {
display: inline-flex;
flex-direction: row;
align-items: center;
align-items: center;
font-size: 1.2rem;
font-family: "Space Grotesk", sans-serif;
font-size: 1.2rem;
font-family: "Space Grotesk", sans-serif;
h1 {
margin: 0;
font-weight: 600;
word-break: break-all;
}
}
h1 {
margin: 0;
font-weight: 600;
word-break: break-all;
}
}
.play_info_description {
font-size: 0.8rem;
font-weight: 400;
.play_info_description {
font-size: 0.8rem;
font-weight: 400;
max-height: 10vh;
max-height: 10vh;
text-overflow: ellipsis;
text-overflow: ellipsis;
p {
margin: 0;
overflow: hidden;
white-space: nowrap;
}
}
p {
margin: 0;
overflow: hidden;
white-space: nowrap;
}
}
.play_info_statistics {
display: flex;
flex-direction: column;
.play_info_statistics {
display: flex;
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 {
display: flex;
flex-direction: row;
.play_info_statistics_item {
display: flex;
flex-direction: row;
align-items: center;
align-items: center;
margin-bottom: 10px;
margin-bottom: 10px;
h1,
h2,
h3,
h4,
h5,
h6,
p,
span {
margin: 0;
}
h1,
h2,
h3,
h4,
h5,
h6,
p,
span {
margin: 0;
}
.play_info_statistics_item_icon {
margin-right: 10px;
}
.play_info_statistics_item_icon {
margin-right: 10px;
}
.play_info_statistics_item_text {
font-size: 0.8rem;
font-weight: 400;
}
.play_info_statistics_item_text {
font-size: 0.8rem;
font-weight: 400;
}
&:last-child {
margin-bottom: 0;
}
}
&:last-child {
margin-bottom: 0;
}
}
}
}
.play_info_actions {
display: inline-flex;
flex-direction: row;
.play_info_actions {
display: inline-flex;
flex-direction: row;
align-items: center;
align-items: center;
gap: 10px;
}
}
}
}
gap: 10px;
}
}
}
}
.list {
display: flex;
flex-direction: column;
.list {
display: flex;
flex-direction: column;
color: var(--text-color);
color: var(--text-color);
gap: 10px;
gap: 10px;
width: 100%;
width: 100%;
h1 {
margin: 0;
}
h1 {
margin: 0;
}
.list_header {
display: flex;
flex-direction: row;
.list_header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
align-items: center;
justify-content: space-between;
}
.list_content {
display: flex;
flex-direction: column;
.list_content {
display: flex;
flex-direction: column;
gap: 10px;
}
}
}
gap: 7px;
}
}
}

View File

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