improve explore & library

This commit is contained in:
SrGooglo 2025-02-05 02:45:40 +00:00
parent 39b427dea7
commit e54c3a1abe
10 changed files with 629 additions and 464 deletions

View File

@ -1,18 +1,22 @@
import React from "react" import React from "react"
import Searcher from "@components/Searcher" import Searcher from "@components/Searcher"
import MusicModel from "@models/music" import SearchModel from "@models/search"
const MusicNavbar = (props) => { const MusicNavbar = (props) => {
return <div className="music_navbar"> return (
<Searcher <div className="music_navbar">
useUrlQuery <Searcher
renderResults={false} useUrlQuery
model={MusicModel.search} renderResults={false}
onSearchResult={props.setSearchResults} model={async (keywords, params) =>
onEmpty={() => props.setSearchResults(false)} SearchModel.search(keywords, params, ["tracks"])
/> }
</div> onSearchResult={props.setSearchResults}
onEmpty={() => props.setSearchResults(false)}
/>
</div>
)
} }
export default MusicNavbar export default MusicNavbar

View File

@ -1,12 +1,66 @@
import React from "react" import React from "react"
import * as antd from "antd"
import Image from "@components/Image"
import { Icons } from "@components/Icons" import { Icons } from "@components/Icons"
import MusicModel from "@models/music"
import "./index.less" import "./index.less"
const RecentlyPlayedItem = (props) => {
const { track } = props
return <div
className="recently_played-item"
onClick={() => app.cores.player.start(track._id)}
>
<div className="recently_played-item-icon">
<Icons.FiPlay />
</div>
<div className="recently_played-item-cover">
<Image
src={track.cover}
/>
</div>
<div className="recently_played-item-content">
<h3>{track.title}</h3>
</div>
</div>
}
const RecentlyPlayedList = (props) => { const RecentlyPlayedList = (props) => {
const [L_Tracks, R_Tracks, E_Tracks, M_Tracks] = app.cores.api.useRequest(MusicModel.getRecentyPlayed, {
limit: 7
})
if (E_Tracks) {
return null
}
return <div className="recently_played"> return <div className="recently_played">
<div className="recently_played-header"> <div className="recently_played-header">
<h1>Recently played</h1> <h1><Icons.MdHistory /> Recently played</h1>
</div>
<div className="recently_played-content">
{
L_Tracks && <antd.Skeleton active />
}
{
!L_Tracks && <div className="recently_played-content-items">
{
R_Tracks.map((track, index) => {
return <RecentlyPlayedItem
key={index}
track={track}
/>
})
}
</div>
}
</div> </div>
</div> </div>
} }

View File

@ -14,4 +14,95 @@
font-size: 1rem; font-size: 1rem;
} }
.recently_played-content {
display: flex;
flex-direction: column;
overflow: hidden;
overflow-x: scroll;
padding: 0 20px;
.recently_played-content-items {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: auto;
grid-column-gap: 20px;
grid-row-gap: 20px;
}
}
}
.recently_played-item {
position: relative;
z-index: 50;
display: flex;
flex-direction: row;
width: 100%;
height: 100px;
overflow: hidden;
background-color: var(--background-color-accent);
border-radius: 12px;
cursor: pointer;
&:hover {
.recently_played-item-icon {
opacity: 1;
}
.recently_played-item-cover {
opacity: 0.1;
}
}
.recently_played-item-icon {
z-index: 55;
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
opacity: 0;
font-size: 3rem;
transition: all 150ms ease-in-out;
}
.recently_played-item-cover {
position: absolute;
z-index: 50;
width: 100%;
height: 100%;
opacity: 0.5;
transition: all 150ms ease-in-out;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.recently_played-item-content {
z-index: 55;
padding: 10px;
}
} }

View File

@ -3,130 +3,127 @@ import * as antd from "antd"
import { Translation } from "react-i18next" import { Translation } from "react-i18next"
import { Icons } from "@components/Icons" import { Icons } from "@components/Icons"
import PlaylistItem from "@components/Music/PlaylistItem" import Playlist from "@components/Music/Playlist"
import "./index.less" import "./index.less"
const ReleasesList = (props) => { const ReleasesList = (props) => {
const hopNumber = props.hopsPerPage ?? 6 const hopNumber = props.hopsPerPage ?? 9
const [offset, setOffset] = React.useState(0) const [offset, setOffset] = React.useState(0)
const [ended, setEnded] = React.useState(false) const [ended, setEnded] = React.useState(false)
const [loading, result, error, makeRequest] = app.cores.api.useRequest(props.fetchMethod, { const [loading, result, error, makeRequest] = app.cores.api.useRequest(
limit: hopNumber, props.fetchMethod,
trim: offset {
}) limit: hopNumber,
trim: offset,
},
)
const onClickPrev = () => { const onClickPrev = () => {
if (offset === 0) { if (offset === 0) {
return return
} }
setOffset((value) => { setOffset((value) => {
const newOffset = value - hopNumber const newOffset = value - hopNumber
// check if newOffset is NaN // check if newOffset is NaN
if (newOffset !== newOffset) { if (newOffset !== newOffset) {
return false return false
} }
if (typeof makeRequest === "function") { if (typeof makeRequest === "function") {
makeRequest({ makeRequest({
trim: newOffset, trim: newOffset,
limit: hopNumber, limit: hopNumber,
}) })
} }
return newOffset return newOffset
}) })
} }
const onClickNext = () => { const onClickNext = () => {
if (ended) { if (ended) {
return return
} }
setOffset((value) => { setOffset((value) => {
const newOffset = value + hopNumber const newOffset = value + hopNumber
// check if newOffset is NaN // check if newOffset is NaN
if (newOffset !== newOffset) { if (newOffset !== newOffset) {
return false return false
} }
if (typeof makeRequest === "function") { if (typeof makeRequest === "function") {
makeRequest({ makeRequest({
trim: newOffset, trim: newOffset,
limit: hopNumber, limit: hopNumber,
}) })
} }
return newOffset return newOffset
}) })
} }
React.useEffect(() => { React.useEffect(() => {
if (result) { if (result) {
if (typeof result.has_more !== "undefined") { if (typeof result.has_more !== "undefined") {
setEnded(!result.has_more) setEnded(!result.has_more)
} else { } else {
setEnded(result.items.length < hopNumber) setEnded(result.items.length < hopNumber)
} }
} }
}, [result]) }, [result])
if (error) { if (error) {
console.error(error) console.error(error)
return <div className="playlistExplorer_section"> return (
<antd.Result <div className="playlistExplorer_section">
status="warning" <antd.Result
title="Failed to load" status="warning"
subTitle="We are sorry, but we could not load this requests. Please try again later." title="Failed to load"
/> subTitle="We are sorry, but we could not load this requests. Please try again later."
</div> />
} </div>
)
}
return <div className="music-releases-list"> return (
<div className="music-releases-list-header"> <div className="music-releases-list">
<h1> <div className="music-releases-list-header">
{ <h1>
props.headerIcon {props.headerIcon}
} <Translation>{(t) => t(props.headerTitle)}</Translation>
<Translation> </h1>
{(t) => t(props.headerTitle)}
</Translation>
</h1>
<div className="music-releases-list-actions"> <div className="music-releases-list-actions">
<antd.Button <antd.Button
icon={<Icons.MdChevronLeft />} icon={<Icons.MdChevronLeft />}
onClick={onClickPrev} onClick={onClickPrev}
disabled={offset === 0 || loading} disabled={offset === 0 || loading}
/> />
<antd.Button <antd.Button
icon={<Icons.MdChevronRight />} icon={<Icons.MdChevronRight />}
onClick={onClickNext} onClick={onClickNext}
disabled={ended || loading} disabled={ended || loading}
/> />
</div> </div>
</div> </div>
<div className="music-releases-list-items"> <div className="music-releases-list-items">
{ {loading && <antd.Skeleton active />}
loading && <antd.Skeleton active /> {!loading &&
} result.items.map((playlist, index) => {
{ return <Playlist key={index} playlist={playlist} />
!loading && result.items.map((playlist, index) => { })}
return <PlaylistItem </div>
key={index} </div>
playlist={playlist} )
/>
})
}
</div>
</div>
} }
export default ReleasesList export default ReleasesList

View File

@ -1,52 +1,63 @@
.music-releases-list { .music-releases-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow-x: visible; overflow-x: visible;
.music-releases-list-header { .music-releases-list-header {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
margin-bottom: 20px; margin-bottom: 20px;
h1 { h1 {
font-size: 1.5rem; font-size: 1.5rem;
margin: 0; margin: 0;
} }
.music-releases-list-actions { .music-releases-list-actions {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 10px; gap: 10px;
align-self: center; align-self: center;
margin-left: auto; margin-left: auto;
} }
} }
.music-releases-list-items { .music-releases-list-items {
display: grid; display: grid;
grid-gap: 20px; grid-gap: 20px;
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(2, 1fr);
min-width: 372px !important; @media (min-width: 768px) {
grid-template-columns: repeat(3, 1fr);
}
@media (min-width: 2000px) { @media (min-width: 1000px) {
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, 1fr);
} }
@media (min-width: 2300px) { @media (min-width: 1500px) {
grid-template-columns: repeat(5, 1fr); grid-template-columns: repeat(7, 1fr);
} }
.playlistItem { @media (min-width: 1600px) {
justify-self: center; grid-template-columns: repeat(7, 1fr);
} }
}
@media (min-width: 1920px) {
grid-template-columns: repeat(9, 1fr);
}
.playlist {
justify-self: center;
//min-width: 372px !important;
}
}
} }

View File

@ -5,14 +5,14 @@ import { Translation } from "react-i18next"
import { createIconRender } from "@components/Icons" import { createIconRender } from "@components/Icons"
import MusicTrack from "@components/Music/Track" import MusicTrack from "@components/Music/Track"
import PlaylistItem from "@components/Music/PlaylistItem" import Playlist from "@components/Music/Playlist"
const ResultGroupsDecorators = { const ResultGroupsDecorators = {
"playlists": { "playlists": {
icon: "MdPlaylistPlay", icon: "MdPlaylistPlay",
label: "Playlists", label: "Playlists",
renderItem: (props) => { renderItem: (props) => {
return <PlaylistItem return <Playlist
key={props.key} key={props.key}
playlist={props.item} playlist={props.item}
/> />
@ -41,9 +41,18 @@ const SearchResults = ({
let groupsKeys = Object.keys(data) let groupsKeys = Object.keys(data)
// filter out empty groups // filter out groups with no items array property
groupsKeys = groupsKeys.filter((key) => { groupsKeys = groupsKeys.filter((key) => {
return data[key].length > 0 if (!Array.isArray(data[key].items)) {
return false
}
return true
})
// filter out groups with empty items array
groupsKeys = groupsKeys.filter((key) => {
return data[key].items.length > 0
}) })
if (groupsKeys.length === 0) { if (groupsKeys.length === 0) {
@ -86,7 +95,7 @@ const SearchResults = ({
<div className="music-explorer_search_results_group_list"> <div className="music-explorer_search_results_group_list">
{ {
data[key].map((item, index) => { data[key].items.map((item, index) => {
return decorator.renderItem({ return decorator.renderItem({
key: index, key: index,
item item

View File

@ -1,11 +1,13 @@
import React from "react" import React from "react"
import classnames from "classnames" import classnames from "classnames"
import useCenteredContainer from "@hooks/useCenteredContainer"
import Searcher from "@components/Searcher" import Searcher from "@components/Searcher"
import { Icons } from "@components/Icons" import { Icons } from "@components/Icons"
import FeedModel from "@models/feed" import FeedModel from "@models/feed"
import MusicModel from "@models/music" import SearchModel from "@models/search"
import Navbar from "./components/Navbar" import Navbar from "./components/Navbar"
import RecentlyPlayedList from "./components/RecentlyPlayedList" import RecentlyPlayedList from "./components/RecentlyPlayedList"
@ -15,12 +17,12 @@ import FeaturedPlaylist from "./components/FeaturedPlaylist"
import "./index.less" import "./index.less"
const MusicExploreTab = (props) => { const MusicExploreTab = (props) => {
const [searchResults, setSearchResults] = React.useState(false) const [searchResults, setSearchResults] = React.useState(false)
React.useEffect(() => { useCenteredContainer(false)
app.layout.toggleCenteredContent(true)
React.useEffect(() => {
app.layout.page_panels.attachComponent("music_navbar", Navbar, { app.layout.page_panels.attachComponent("music_navbar", Navbar, {
props: { props: {
setSearchResults: setSearchResults, setSearchResults: setSearchResults,
@ -43,7 +45,7 @@ import "./index.less"
app.isMobile && <Searcher app.isMobile && <Searcher
useUrlQuery useUrlQuery
renderResults={false} renderResults={false}
model={MusicModel.search} model={(keywords, params) => SearchModel.search("music", keywords, params)}
onSearchResult={setSearchResults} onSearchResult={setSearchResults}
onEmpty={() => setSearchResults(false)} onEmpty={() => setSearchResults(false)}
/> />
@ -62,13 +64,7 @@ import "./index.less"
<RecentlyPlayedList /> <RecentlyPlayedList />
<ReleasesList <ReleasesList
headerTitle="From your following artists" headerTitle="Explore"
headerIcon={<Icons.MdPerson />}
fetchMethod={FeedModel.getMusicFeed}
/>
<ReleasesList
headerTitle="Explore from global"
headerIcon={<Icons.MdExplore />} headerIcon={<Icons.MdExplore />}
fetchMethod={FeedModel.getGlobalMusicFeed} fetchMethod={FeedModel.getGlobalMusicFeed}
/> />

View File

@ -1,288 +1,287 @@
html { html {
&.mobile { &.mobile {
.musicExplorer { .musicExplorer {
.playlistExplorer_section_list { .playlistExplorer_section_list {
overflow: visible; overflow: visible;
overflow-x: scroll; overflow-x: scroll;
width: unset; width: unset;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
grid-gap: 10px; grid-gap: 10px;
} }
} }
} }
} }
.featured_playlist { .featured_playlist {
position: relative; position: relative;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
overflow: hidden; overflow: hidden;
background-color: var(--background-color-accent); background-color: var(--background-color-accent);
width: 100%; width: 100%;
min-height: 200px; min-height: 200px;
height: fit-content; height: fit-content;
border-radius: 12px; border-radius: 12px;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
.featured_playlist_content { .featured_playlist_content {
h1,
p {
-webkit-text-stroke-width: 1.6px;
-webkit-text-stroke-color: var(--border-color);
h1, color: var(--background-color-contrast);
p { }
-webkit-text-stroke-width: 1.6px; }
-webkit-text-stroke-color: var(--border-color);
color: var(--background-color-contrast); .lazy-load-image-background {
} opacity: 1;
} }
}
.lazy-load-image-background { .lazy-load-image-background {
opacity: 1; z-index: 50;
}
}
.lazy-load-image-background { position: absolute;
z-index: 50;
position: absolute; opacity: 0.3;
opacity: 0.3; transition: all 300ms ease-in-out !important;
transition: all 300ms ease-in-out !important; img {
width: 100%;
height: 100%;
}
}
img { .featured_playlist_content {
width: 100%; z-index: 55;
height: 100%;
}
}
.featured_playlist_content { padding: 20px;
z-index: 55;
padding: 20px; display: flex;
flex-direction: column;
display: flex; h1 {
flex-direction: column; font-size: 2.5rem;
font-family: "Space Grotesk", sans-serif;
font-weight: 900;
h1 { transition: all 300ms ease-in-out !important;
font-size: 2.5rem; }
font-family: "Space Grotesk", sans-serif;
font-weight: 900;
transition: all 300ms ease-in-out !important; p {
} font-size: 1rem;
font-family: "Space Grotesk", sans-serif;
p { transition: all 300ms ease-in-out !important;
font-size: 1rem; }
font-family: "Space Grotesk", sans-serif;
transition: all 300ms ease-in-out !important; .featured_playlist_genre {
} z-index: 55;
.featured_playlist_genre { position: absolute;
z-index: 55;
position: absolute; left: 0;
bottom: 0;
left: 0; margin: 10px;
bottom: 0;
margin: 10px; background-color: var(--background-color-accent);
border: 1px solid var(--border-color);
background-color: var(--background-color-accent);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 10px 20px;
}
}
border-radius: 12px;
padding: 10px 20px;
}
}
} }
.music_navbar { .music_navbar {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
background-color: var(--background-color-accent); background-color: var(--background-color-accent);
border-radius: 12px; border-radius: 12px;
} }
.musicExplorer { .musicExplorer {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
height: 100%; height: 100%;
gap: 20px; gap: 20px;
&.search-focused { &.search-focused {
.feed_main { .feed_main {
height: 0px; height: 0px;
opacity: 0; opacity: 0;
} }
} }
.feed_main { .feed_main {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
height: 100%; height: 100%;
gap: 50px; gap: 50px;
transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out;
overflow-x: visible; overflow-x: visible;
} }
} }
.music-explorer_search_results { .music-explorer_search_results {
display: grid; display: grid;
width: 100%; width: 100%;
// if only need 1 column, it will be 1fr // if only need 1 column, it will be 1fr
// if need 2 colums, first column will be 1fr, and the second one will be 2fr // if need 2 colums, first column will be 1fr, and the second one will be 2fr
grid-template-columns: 1fr 2fr; grid-template-columns: 1fr 2fr;
// auto generate rows // auto generate rows
grid-template-rows: auto; grid-template-rows: auto;
grid-column-gap: 20px; grid-column-gap: 20px;
grid-row-gap: 20px; grid-row-gap: 20px;
@media screen and (max-width: 1750px) { @media screen and (max-width: 1750px) {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
.music-explorer_search_results_group_list { .music-explorer_search_results_group_list {
.playlistItem { .playlistItem {
width: 100% !important; width: 100% !important;
max-width: 100% !important; max-width: 100% !important;
.playlistItem_info_subtitle { .playlistItem_info_subtitle {
max-width: 300px; max-width: 300px;
} }
.playlistItem_bottom { .playlistItem_bottom {
display: flex !important; display: flex !important;
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
padding: 10px; padding: 10px;
//-webkit-backdrop-filter: blur(5px); //-webkit-backdrop-filter: blur(5px);
//backdrop-filter: blur(5px); //backdrop-filter: blur(5px);
background-color: var(--background-color-primary); background-color: var(--background-color-primary);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
p { p {
display: flex; display: flex;
flex-direction: row-reverse; flex-direction: row-reverse;
gap: 7px; gap: 7px;
} }
} }
} }
} }
} }
&.one_column { &.one_column {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
&.no_results { &.no_results {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.music-explorer_search_results_group { .music-explorer_search_results_group {
background-color: var(--background-color-accent); background-color: var(--background-color-accent);
padding: 20px; padding: 20px;
border-radius: 8px; border-radius: 8px;
height: fit-content; height: fit-content;
width: 100%; width: 100%;
gap: 20px; gap: 20px;
.explorer_search_results_group_header { .explorer_search_results_group_header {
h1 { h1 {
margin: 0; margin: 0;
} }
} }
.music-explorer_search_results_group_list { .music-explorer_search_results_group_list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
@playlistItem_height: 80px; @playlistItem_height: 80px;
@playlistItem_padding: 10px; @playlistItem_padding: 10px;
@playlistItem_cover_size: calc(@playlistItem_height - @playlistItem_padding * 2); @playlistItem_cover_size: calc(
@playlistItem_height - @playlistItem_padding * 2
);
.playlistItem { .playlistItem {
flex-direction: row; flex-direction: row;
background-color: var(--background-color-primary); background-color: var(--background-color-primary);
max-width: 300px; max-width: 300px;
width: 100%; width: 100%;
height: @playlistItem_height; height: @playlistItem_height;
padding: @playlistItem_padding; padding: @playlistItem_padding;
.playlistItem_cover { .playlistItem_cover {
width: @playlistItem_cover_size; width: @playlistItem_cover_size;
height: @playlistItem_cover_size; height: @playlistItem_cover_size;
min-height: @playlistItem_cover_size; min-height: @playlistItem_cover_size;
min-width: @playlistItem_cover_size; min-width: @playlistItem_cover_size;
} }
.playlistItem_bottom { .playlistItem_bottom {
display: none; display: none;
} }
&:hover { &:hover {
.playlistItem_info { .playlistItem_info {
transform: none; transform: none;
} }
} }
} }
.music-track { .music-track {
background-color: var(--background-color-primary); background-color: var(--background-color-primary);
} }
} }
} }
} }

View File

@ -9,56 +9,56 @@ import PlaylistLibraryView from "./views/playlists"
import "./index.less" import "./index.less"
const TabToView = { const TabToView = {
tracks: TracksLibraryView, tracks: TracksLibraryView,
playlist: PlaylistLibraryView, playlist: PlaylistLibraryView,
releases: PlaylistLibraryView, releases: PlaylistLibraryView,
} }
const TabToHeader = { const TabToHeader = {
tracks: { tracks: {
icon: <Icons.MdMusicNote />, icon: <Icons.MdMusicNote />,
label: "Tracks", label: "Tracks",
}, },
playlist: { playlist: {
icon: <Icons.MdPlaylistPlay />, icon: <Icons.MdPlaylistPlay />,
label: "Playlists", label: "Playlists",
}, },
} }
const Library = (props) => { const Library = (props) => {
const [selectedTab, setSelectedTab] = React.useState("tracks") const [selectedTab, setSelectedTab] = React.useState("tracks")
return <div className="music-library"> return (
<div className="music-library_header"> <div className="music-library">
<h1>Library</h1> <div className="music-library_header">
<antd.Segmented
value={selectedTab}
onChange={setSelectedTab}
options={[
{
value: "tracks",
label: "Tracks",
icon: <Icons.MdMusicNote />,
},
{
value: "playlist",
label: "Playlists",
icon: <Icons.MdPlaylistPlay />,
},
{
value: "releases",
label: "Releases",
icon: <Icons.MdPlaylistPlay />,
},
]}
/>
</div>
<antd.Segmented {selectedTab &&
value={selectedTab} TabToView[selectedTab] &&
onChange={setSelectedTab} React.createElement(TabToView[selectedTab])}
options={[ </div>
{ )
value: "tracks",
label: "Tracks",
icon: <Icons.MdMusicNote />
},
{
value: "playlist",
label: "Playlists",
icon: <Icons.MdPlaylistPlay />
},
{
value: "releases",
label: "Releases",
icon: <Icons.MdPlaylistPlay />
}
]}
/>
</div>
{
selectedTab && TabToView[selectedTab] && React.createElement(TabToView[selectedTab])
}
</div>
} }
export default Library export default Library

View File

@ -8,71 +8,75 @@ import MusicModel from "@models/music"
const loadLimit = 50 const loadLimit = 50
const TracksLibraryView = () => { const TracksLibraryView = () => {
const [offset, setOffset] = React.useState(0) const [offset, setOffset] = React.useState(0)
const [list, setList] = React.useState([]) const [items, setItems] = React.useState([])
const [hasMore, setHasMore] = React.useState(true) const [hasMore, setHasMore] = React.useState(true)
const [initialLoading, setInitialLoading] = React.useState(true) const [initialLoading, setInitialLoading] = React.useState(true)
const [L_Favourites, R_Favourites, E_Favourites, M_Favourites] = app.cores.api.useRequest(MusicModel.getFavouriteFolder, { const [L_Favourites, R_Favourites, E_Favourites, M_Favourites] =
offset: offset, app.cores.api.useRequest(MusicModel.getFavouriteFolder, {
limit: loadLimit, offset: offset,
}) limit: loadLimit,
})
async function onLoadMore() { async function onLoadMore() {
const newOffset = offset + loadLimit const newOffset = offset + loadLimit
setOffset(newOffset) setOffset(newOffset)
M_Favourites({ M_Favourites({
offset: newOffset, offset: newOffset,
limit: loadLimit, limit: loadLimit,
}) })
} }
React.useEffect(() => { React.useEffect(() => {
if (R_Favourites && R_Favourites.tracks) { if (R_Favourites && R_Favourites.tracks) {
if (initialLoading === true) { if (initialLoading === true) {
setInitialLoading(false) setInitialLoading(false)
} }
if (R_Favourites.tracks.list.length === 0) { if (R_Favourites.tracks.items.length === 0) {
setHasMore(false) setHasMore(false)
} else { } else {
setList((prev) => { setItems((prev) => {
prev = [ prev = [...prev, ...R_Favourites.tracks.items]
...prev,
...R_Favourites.tracks.list,
]
return prev return prev
}) })
} }
} }
}, [R_Favourites]) }, [R_Favourites])
if (E_Favourites) { if (E_Favourites) {
return <antd.Result return (
status="warning" <antd.Result
title="Failed to load" status="warning"
subTitle={E_Favourites} title="Failed to load"
/> subTitle={E_Favourites}
} />
)
}
if (initialLoading) { if (initialLoading) {
return <antd.Skeleton active /> return <antd.Skeleton active />
} }
return <PlaylistView return (
noHeader <PlaylistView
loading={L_Favourites} noHeader
type="vertical" noSearch
playlist={{ loading={L_Favourites}
list: list type="vertical"
}} playlist={{
onLoadMore={onLoadMore} items: items,
hasMore={hasMore} total_length: R_Favourites.tracks.total_items,
length={R_Favourites.tracks.total_length} }}
/> onLoadMore={onLoadMore}
hasMore={hasMore}
length={R_Favourites.tracks.total_length}
/>
)
} }
export default TracksLibraryView export default TracksLibraryView