304 lines
8.4 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 { Icons, createIconRender } from "components/Icons"
import { WithPlayerContext } from "contexts/WithPlayerContext"
import FeedModel from "models/feed"
import MusicModel from "models/music"
import MusicTrack from "components/Music/Track"
import PlaylistItem from "components/Music/PlaylistItem"
import "./index.less"
const MusicNavbar = (props) => {
return <div className="music_navbar">
<Searcher
useUrlQuery
renderResults={false}
model={MusicModel.search}
modelParams={{
useTidal: app.cores.sync.getActiveLinkedServices().tidal,
}}
onSearchResult={props.setSearchResults}
onEmpty={() => props.setSearchResults(false)}
/>
</div>
}
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 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}
onClickPlayBtn={() => app.cores.player.start(props.item)}
onClick={() => app.location.push(`/play/${props.item._id}`)}
/>
}
}
}
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,
}
)}
>
{
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>
})
}
</div>
}
export default (props) => {
const [searchResults, setSearchResults] = React.useState(false)
React.useEffect(() => {
app.layout.toggleCenteredContent(true)
app.layout.page_panels.attachComponent("music_navbar", MusicNavbar, {
props: {
setSearchResults: setSearchResults,
}
})
return () => {
if (app.layout.page_panels) {
app.layout.page_panels.detachComponent("music_navbar")
}
}
}, [])
return <div
className={classnames(
"musicExplorer",
)}
>
{
app.isMobile && <Searcher
useUrlQuery
renderResults={false}
model={MusicModel.search}
modelParams={{
useTidal: app.cores.sync.getActiveLinkedServices().tidal,
}}
onSearchResult={setSearchResults}
onEmpty={() => setSearchResults(false)}
/>
}
{
searchResults && <SearchResults
data={searchResults}
/>
}
{
!searchResults && <div className="feed_main">
<PlaylistsList
headerTitle="From your following artists"
headerIcon={<Icons.MdPerson />}
fetchMethod={FeedModel.getMusicFeed}
/>
<PlaylistsList
headerTitle="Explore from global"
headerIcon={<Icons.MdExplore />}
fetchMethod={FeedModel.getGlobalMusicFeed}
/>
</div>
}
</div>
}