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,42 +22,52 @@ import MusicModel from "@models/music"
import "./index.less" import "./index.less"
const PlaylistTypeDecorators = { const PlaylistTypeDecorators = {
"single": () => <span className="playlistType"> single: () => (
<span className="playlistType">
<Icons.MdMusicNote /> <Icons.MdMusicNote />
Single Single
</span>, </span>
"album": () => <span className="playlistType"> ),
album: () => (
<span className="playlistType">
<Icons.MdAlbum /> <Icons.MdAlbum />
Album Album
</span>, </span>
"ep": () => <span className="playlistType"> ),
ep: () => (
<span className="playlistType">
<Icons.MdAlbum /> <Icons.MdAlbum />
EP EP
</span>, </span>
"mix": () => <span className="playlistType"> ),
mix: () => (
<span className="playlistType">
<Icons.MdMusicNote /> <Icons.MdMusicNote />
Mix Mix
</span>, </span>
),
} }
const PlaylistInfo = (props) => { const PlaylistInfo = (props) => {
return <div> return (
<div>
<ReactMarkdown <ReactMarkdown
remarkPlugins={[remarkGfm]} remarkPlugins={[remarkGfm]}
children={props.data.description} children={props.data.description}
/> />
</div> </div>
)
} }
const MoreMenuHandlers = { const MoreMenuHandlers = {
"edit": async (playlist) => { edit: async (playlist) => {},
delete: async (playlist) => {
},
"delete": async (playlist) => {
return antd.Modal.confirm({ return antd.Modal.confirm({
title: "Are you sure you want to delete this playlist?", title: "Are you sure you want to delete this playlist?",
onOk: async () => { onOk: async () => {
const result = await MusicModel.deletePlaylist(playlist._id).catch((err) => { const result = await MusicModel.deletePlaylist(
playlist._id,
).catch((err) => {
console.log(err) console.log(err)
app.message.error("Failed to delete playlist") app.message.error("Failed to delete playlist")
@ -68,21 +78,25 @@ const MoreMenuHandlers = {
if (result) { if (result) {
app.navigation.goToMusic() 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", key: "edit",
label: "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)) {
@ -99,12 +113,8 @@ const PlaylistView = (props) => {
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) => {},
},
remove_track: (track) => {
}
} }
let debounceSearch = null let debounceSearch = null
@ -129,20 +139,20 @@ const PlaylistView = (props) => {
} }
const handleOnClickPlaylistPlay = () => { const handleOnClickPlaylistPlay = () => {
app.cores.player.start(playlist.list) app.cores.player.start(playlist.items)
} }
const handleOnClickViewDetails = () => { const handleOnClickViewDetails = () => {
app.layout.modal.open("playlist_info", PlaylistInfo, { app.layout.modal.open("playlist_info", PlaylistInfo, {
props: { props: {
data: playlist data: playlist,
} },
}) })
} }
const handleOnClickTrack = (track) => { const handleOnClickTrack = (track) => {
// search index of track // search index of track
const index = playlist.list.findIndex((item) => { const index = playlist.items.findIndex((item) => {
return item._id === track._id return item._id === track._id
}) })
@ -154,8 +164,8 @@ const PlaylistView = (props) => {
if (app.cores.player.state.track_manifest?._id === track._id) { if (app.cores.player.state.track_manifest?._id === track._id) {
app.cores.player.playback.toggle() app.cores.player.playback.toggle()
} else { } else {
app.cores.player.start(playlist.list, { app.cores.player.start(playlist.items, {
startIndex: index startIndex: index,
}) })
} }
} }
@ -193,7 +203,7 @@ const PlaylistView = (props) => {
newState.list[index] = { newState.list[index] = {
...newState.list[index], ...newState.list[index],
...update ...update,
} }
return newState return newState
@ -213,13 +223,16 @@ const PlaylistView = (props) => {
return await handler(playlist) return await handler(playlist)
} }
useWsEvents({ useWsEvents(
{
"music:track:toggle:like": (data) => { "music:track:toggle:like": (data) => {
handleUpdateTrackLike(data.track_id, data.action === "liked") handleUpdateTrackLike(data.track_id, data.action === "liked")
} },
}, { },
{
socketName: "music", socketName: "music",
}) },
)
React.useEffect(() => { React.useEffect(() => {
setPlaylist(props.playlist) setPlaylist(props.playlist)
@ -232,59 +245,77 @@ const PlaylistView = (props) => {
const playlistType = playlist.type?.toLowerCase() ?? "playlist" const playlistType = playlist.type?.toLowerCase() ?? "playlist"
return <PlaylistContext.Provider value={contextValues}> return (
<PlaylistContext.Provider value={contextValues}>
<WithPlayerContext> <WithPlayerContext>
<div <div className={classnames("playlist_view")}>
className={classnames( {!props.noHeader && (
"playlist_view", <div className="play_info_wrapper">
playlistType,
)}
>
{
!props.noHeader && <div className="play_info_wrapper">
<div className="play_info"> <div className="play_info">
<div className="play_info_cover"> <div className="play_info_cover">
<ImageViewer src={playlist.cover ?? playlist?.thumbnail ?? "/assets/no_song.png"} /> <ImageViewer
src={
playlist.cover ??
playlist?.thumbnail ??
"/assets/no_song.png"
}
/>
</div> </div>
<div className="play_info_details"> <div className="play_info_details">
<div className="play_info_title"> <div className="play_info_title">
{ {playlist.service === "tidal" && (
playlist.service === "tidal" && <Icons.SiTidal /> <Icons.SiTidal />
} )}
{ {typeof playlist.title ===
typeof playlist.title === "function" ? "function" ? (
playlist.title : playlist.title
) : (
<h1>{playlist.title}</h1> <h1>{playlist.title}</h1>
} )}
</div> </div>
<div className="play_info_statistics"> <div className="play_info_statistics">
{ {playlistType &&
playlistType && PlaylistTypeDecorators[playlistType] && <div className="play_info_statistics_item"> PlaylistTypeDecorators[
{ playlistType
PlaylistTypeDecorators[playlistType]() ] && (
} <div className="play_info_statistics_item">
{PlaylistTypeDecorators[
playlistType
]()}
</div> </div>
} )}
<div className="play_info_statistics_item"> <div className="play_info_statistics_item">
<p> <p>
<Icons.MdLibraryMusic /> {props.length ?? playlist.total_length ?? playlist.list.length} Items <Icons.MdLibraryMusic />{" "}
{props.length ??
playlist.total_length ??
playlist.items.length}{" "}
Items
</p> </p>
</div> </div>
{ {playlist.publisher && (
playlist.publisher && <div className="play_info_statistics_item"> <div className="play_info_statistics_item">
<p <p
onClick={() => { onClick={() => {
app.navigation.goToAccount(playlist.publisher.username) app.navigation.goToAccount(
playlist.publisher
.username,
)
}} }}
> >
<Icons.MdPerson /> <Icons.MdPerson />
Publised by{" "}
Publised by <a>{playlist.publisher.username}</a> <a>
{
playlist.publisher
.username
}
</a>
</p> </p>
</div> </div>
} )}
</div> </div>
<div className="play_info_actions"> <div className="play_info_actions">
@ -298,38 +329,39 @@ const PlaylistView = (props) => {
Play Play
</antd.Button> </antd.Button>
{ {playlist.description && (
playlist.description && <antd.Button <antd.Button
icon={<Icons.MdInfo />} icon={<Icons.MdInfo />}
onClick={handleOnClickViewDetails} onClick={
/> handleOnClickViewDetails
} }
/>
)}
{ {owningPlaylist && (
owningPlaylist &&
<antd.Dropdown <antd.Dropdown
trigger={["click"]} trigger={["click"]}
placement="bottom" placement="bottom"
menu={{ menu={{
items: moreMenuItems, items: moreMenuItems,
onClick: handleMoreMenuClick onClick:
handleMoreMenuClick,
}} }}
> >
<antd.Button <antd.Button
icon={<Icons.MdMoreVert />} icon={<Icons.MdMoreVert />}
/> />
</antd.Dropdown> </antd.Dropdown>
)}
}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
} )}
<div className="list"> <div className="list">
{ {!props.noHeader && playlist.items.length > 0 && (
playlist.list.length > 0 && <div className="list_header"> <div className="list_header">
<h1> <h1>
<Icons.MdPlaylistPlay /> Tracks <Icons.MdPlaylistPlay /> Tracks
</h1> </h1>
@ -340,55 +372,72 @@ const PlaylistView = (props) => {
disabled disabled
/> />
</div> </div>
} )}
{ {playlist.items.length === 0 && (
playlist.list.length === 0 && <antd.Empty <antd.Empty
description={ description={
<> <>
<Icons.MdLibraryMusic /> This playlist its empty! <Icons.MdLibraryMusic /> This playlist
its empty!
</> </>
} }
/> />
} )}
{ {searchResults &&
searchResults && searchResults.map((item) => { searchResults.map((item) => {
return <MusicTrack return (
<MusicTrack
key={item._id} key={item._id}
order={item._id} order={item._id}
track={item} track={item}
onClickPlayBtn={() => handleOnClickTrack(item)} onClickPlayBtn={() =>
changeState={(update) => handleTrackChangeState(item._id, update)} handleOnClickTrack(item)
/>
})
} }
changeState={(update) =>
handleTrackChangeState(
item._id,
update,
)
}
/>
)
})}
{ {!searchResults && playlist.items.length > 0 && (
!searchResults && playlist.list.length > 0 && <LoadMore <LoadMore
className="list_content" className="list_content"
loadingComponent={() => <antd.Skeleton />} loadingComponent={() => <antd.Skeleton />}
onBottom={props.onLoadMore} onBottom={props.onLoadMore}
hasMore={props.hasMore} hasMore={props.hasMore}
> >
<WithPlayerContext> <WithPlayerContext>
{ {playlist.items.map((item, index) => {
playlist.list.map((item, index) => { return (
return <MusicTrack <MusicTrack
order={index + 1} order={index + 1}
track={item} track={item}
onClickPlayBtn={() => handleOnClickTrack(item)} onClickPlayBtn={() =>
changeState={(update) => handleTrackChangeState(item._id, update)} handleOnClickTrack(item)
/>
})
} }
changeState={(update) =>
handleTrackChangeState(
item._id,
update,
)
}
/>
)
})}
</WithPlayerContext> </WithPlayerContext>
</LoadMore> </LoadMore>
} )}
</div> </div>
</div> </div>
</WithPlayerContext> </WithPlayerContext>
</PlaylistContext.Provider> </PlaylistContext.Provider>
)
} }
export default PlaylistView export default PlaylistView

View File

@ -198,7 +198,6 @@ html {
margin-bottom: 0; margin-bottom: 0;
} }
} }
} }
.play_info_actions { .play_info_actions {
@ -239,7 +238,7 @@ html {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 7px;
} }
} }
} }

View File

@ -15,7 +15,7 @@ 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({
@ -23,7 +23,7 @@ const handlers = {
}) })
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({
@ -31,14 +31,18 @@ const handlers = {
}) })
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)
@ -51,7 +55,9 @@ const Track = (props) => {
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(
"Searcher: onClick is not a function, using default action...",
)
if (!isCurrent) { if (!isCurrent) {
app.cores.player.start(props.track) app.cores.player.start(props.track)
} else { } else {
@ -87,7 +93,7 @@ const Track = (props) => {
}, },
changeState: props.changeState, changeState: props.changeState,
}, },
props.track props.track,
) )
} }
} }
@ -111,12 +117,19 @@ const Track = (props) => {
label: "Add to playlist", label: "Add to playlist",
disabled: true, disabled: true,
}, },
{
type: "divider",
},
{ {
key: "add_to_queue", key: "add_to_queue",
icon: <Icons.MdQueueMusic />, icon: <Icons.MdQueueMusic />,
label: "Add to queue", label: "Add to queue",
disabled: true, },
} {
key: "play_next",
icon: <Icons.MdSkipNext />,
label: "Play next",
},
] ]
if (props.track.liked) { if (props.track.liked) {
@ -144,69 +157,70 @@ const Track = (props) => {
return items return items
}, [props.track]) }, [props.track])
return <div return (
<div
id={props.track._id} id={props.track._id}
className={classnames( className={classnames("music-track", {
"music-track",
{
["current"]: isCurrent, ["current"]: isCurrent,
["playing"]: isPlaying, ["playing"]: isPlaying,
["loading"]: isCurrent && loading ["loading"]: isCurrent && loading,
} })}
)}
style={{ style={{
"--cover_average-color": RGBStringToValues(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}
props.order
}
</span> </span>
<antd.Button <antd.Button
type="primary" type="primary"
shape="circle" shape="circle"
icon={isPlaying ? <Icons.MdPause /> : <Icons.MdPlayArrow />} icon={
isPlaying ? (
<Icons.MdPause />
) : (
<Icons.MdPlayArrow />
)
}
onClick={handleClickPlayBtn} onClick={handleClickPlayBtn}
/> />
</div> </div>
</div> </div>
} )}
<div className="music-track_cover"> <div className="music-track_cover">
<ImageViewer src={props.track.cover ?? props.track.thumbnail} /> <ImageViewer
src={props.track.cover ?? props.track.thumbnail}
/>
</div> </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> </span>
</div> </div>
<div className="music-track_artist"> <div className="music-track_artist">
<span> <span>
{ {Array.isArray(props.track.artists)
Array.isArray(props.track.artists) ? props.track.artists.join(", ") : props.track.artist ? props.track.artists.join(", ")
} : props.track.artist}
</span> </span>
</div> </div>
</div> </div>
@ -215,7 +229,7 @@ const Track = (props) => {
<antd.Dropdown <antd.Dropdown
menu={{ menu={{
items: moreMenuItems, items: moreMenuItems,
onClick: handleMoreMenuItemClick onClick: handleMoreMenuItemClick,
}} }}
onOpenChange={handleMoreMenuOpen} onOpenChange={handleMoreMenuOpen}
open={moreMenuOpened} open={moreMenuOpened}
@ -230,6 +244,7 @@ const Track = (props) => {
</div> </div>
</div> </div>
</div> </div>
)
} }
export default Track export default Track