371 lines
10 KiB
JavaScript
Executable File

import React from "react"
import * as antd from "antd"
import classnames from "classnames"
import { Translation } from "react-i18next"
import Searcher from "components/Searcher"
import { ImageViewer, UserPreview } from "components"
import { Icons, createIconRender } from "components/Icons"
import { WithPlayerContext } from "contexts/WithPlayerContext"
import FeedModel from "models/feed"
import PlaylistModel from "models/playlists"
import MusicTrack from "components/MusicTrack"
import "./index.less"
const PlaylistsList = (props) => {
const hopNumber = props.hopsPerPage ?? 6
const [offset, setOffset] = React.useState(0)
const [ended, setEnded] = React.useState(false)
const [loading, result, error, makeRequest] = app.cores.api.useRequest(props.fetchMethod, {
limit: hopNumber,
trim: offset
})
const onClickPrev = () => {
if (offset === 0) {
return
}
setOffset((value) => {
const newOffset = value - hopNumber
// check if newOffset is NaN
if (newOffset !== newOffset) {
return false
}
if (typeof makeRequest === "function") {
makeRequest({
trim: newOffset,
limit: hopNumber,
})
}
return newOffset
})
}
const onClickNext = () => {
if (ended) {
return
}
setOffset((value) => {
const newOffset = value + hopNumber
// check if newOffset is NaN
if (newOffset !== newOffset) {
return false
}
if (typeof makeRequest === "function") {
makeRequest({
trim: newOffset,
limit: hopNumber,
})
}
return newOffset
})
}
React.useEffect(() => {
if (result) {
setEnded(result.length < hopNumber)
}
}, [result])
if (error) {
console.error(error)
return <div className="playlistExplorer_section">
<antd.Result
status="warning"
title="Failed to load"
subTitle="We are sorry, but we could not load this requests. Please try again later."
/>
</div>
}
return <div className="playlistExplorer_section">
<div className="playlistExplorer_section_header">
<h1>
{
props.headerIcon
}
<Translation>
{(t) => t(props.headerTitle)}
</Translation>
</h1>
<div className="playlistExplorer_section_header_actions">
<antd.Button
icon={<Icons.MdChevronLeft />}
onClick={onClickPrev}
disabled={offset === 0 || loading}
/>
<antd.Button
icon={<Icons.MdChevronRight />}
onClick={onClickNext}
disabled={ended || loading}
/>
</div>
</div>
<div className="playlistExplorer_section_list">
{
loading && <antd.Skeleton active />
}
{
!loading && result.map((playlist, index) => {
return <PlaylistItem
key={index}
playlist={playlist}
/>
})
}
</div>
</div>
}
const PlaylistItem = (props) => {
const [coverHover, setCoverHover] = React.useState(false)
const { playlist } = props
const onClick = () => {
if (typeof props.onClick === "function") {
return props.onClick(playlist)
}
return app.setLocation(`/play/${playlist._id}`)
}
const onClickPlay = (e) => {
e.stopPropagation()
app.cores.player.startPlaylist(playlist.list)
}
return <div
id={playlist._id}
key={props.key}
className={classnames(
"playlistItem",
{
"cover-hovering": coverHover
}
)}
>
<div
className="playlistItem_cover"
onMouseEnter={() => setCoverHover(true)}
onMouseLeave={() => setCoverHover(false)}
onClick={onClickPlay}
>
<div className="playlistItem_cover_mask">
<Icons.MdPlayArrow />
</div>
<ImageViewer
src={playlist.thumbnail ?? "/assets/no_song.png"}
/>
</div>
<div className="playlistItem_info">
<div className="playlistItem_info_title" onClick={onClick}>
<h1>{playlist.title}</h1>
</div>
{
playlist.publisher && <UserPreview user={playlist.publisher} />
}
</div>
</div>
}
const RecentlyPlayed = (props) => {
return <div className="playlistExplorer_section">
<div className="playlistExplorer_section_header">
<h1>
<Icons.MdReplay />
<Translation>
{(t) => t("Recently Played")}
</Translation>
</h1>
</div>
<div>
<antd.Result
status="warning"
title="Failed to load"
subTitle="We are sorry, but we could not load your playlists. Please try again later."
/>
</div>
</div>
}
const MayLike = (props) => {
return <div className="playlistExplorer_section">
<div className="playlistExplorer_section_header">
<h1>
<Icons.MdRecommend />
<Translation>
{(t) => t("May you like")}
</Translation>
</h1>
</div>
<div>
<antd.Result
status="warning"
title="Failed to load"
subTitle="We are sorry, but we could not load your recomendations. Please try again later."
/>
</div>
</div>
}
const ResultGroupsDecorators = {
"playlists": {
icon: "MdPlaylistPlay",
label: "Playlists",
renderItem: (props) => {
return <PlaylistItem
key={props.key}
playlist={props.item}
/>
}
},
"tracks": {
icon: "MdMusicNote",
label: "Tracks",
renderItem: (props) => {
return <MusicTrack
key={props.key}
track={props.item}
onClick={() => app.cores.player.start(props.item)}
/>
}
}
}
const SearchResults = ({
data
}) => {
if (typeof data !== "object") {
return null
}
let groupsKeys = Object.keys(data)
// filter out empty groups
groupsKeys = groupsKeys.filter((key) => {
return data[key].length > 0
})
if (groupsKeys.length === 0) {
return <div className="music-explorer_search_results no_results">
<antd.Result
status="info"
title="No results"
subTitle="We are sorry, but we could not find any results for your search."
/>
</div>
}
return <div
className={classnames(
"music-explorer_search_results",
{
["one_column"]: groupsKeys.length === 1,
}
)}
>
<WithPlayerContext>
{
groupsKeys.map((key, index) => {
const decorator = ResultGroupsDecorators[key] ?? {
icon: null,
label: key,
renderItem: () => null
}
return <div className="music-explorer_search_results_group" key={index}>
<div className="music-explorer_search_results_group_header">
<h1>
{
createIconRender(decorator.icon)
}
<Translation>
{(t) => t(decorator.label)}
</Translation>
</h1>
</div>
<div className="music-explorer_search_results_group_list">
{
data[key].map((item, index) => {
return decorator.renderItem({
key: index,
item
})
})
}
</div>
</div>
})
}
</WithPlayerContext>
</div>
}
export default (props) => {
const [searchResults, setSearchResults] = React.useState(false)
return <div
className={classnames(
"musicExplorer",
{
//["search-focused"]: searchFocused,
}
)}
>
<Searcher
useUrlQuery
renderResults={false}
model={PlaylistModel.search}
onSearchResult={setSearchResults}
onEmpty={() => setSearchResults(false)}
/>
{
searchResults && <SearchResults
data={searchResults}
/>
}
{
!searchResults && <div className="feed_main">
<RecentlyPlayed />
<PlaylistsList
headerTitle="From your following artists"
headerIcon={<Icons.MdPerson />}
fetchMethod={FeedModel.getPlaylistsFeed}
/>
<PlaylistsList
headerTitle="Explore from global"
headerIcon={<Icons.MdExplore />}
fetchMethod={FeedModel.getGlobalMusicFeed}
/>
</div>
}
</div>
}