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 Searcher from "@components/Searcher"
import MusicModel from "@models/music"
import SearchModel from "@models/search"
const MusicNavbar = (props) => {
return <div className="music_navbar">
<Searcher
useUrlQuery
renderResults={false}
model={MusicModel.search}
onSearchResult={props.setSearchResults}
onEmpty={() => props.setSearchResults(false)}
/>
</div>
return (
<div className="music_navbar">
<Searcher
useUrlQuery
renderResults={false}
model={async (keywords, params) =>
SearchModel.search(keywords, params, ["tracks"])
}
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 * as antd from "antd"
import Image from "@components/Image"
import { Icons } from "@components/Icons"
import MusicModel from "@models/music"
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 [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">
<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>
}

View File

@ -14,4 +14,95 @@
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 { Icons } from "@components/Icons"
import PlaylistItem from "@components/Music/PlaylistItem"
import Playlist from "@components/Music/Playlist"
import "./index.less"
const ReleasesList = (props) => {
const hopNumber = props.hopsPerPage ?? 6
const hopNumber = props.hopsPerPage ?? 9
const [offset, setOffset] = React.useState(0)
const [ended, setEnded] = React.useState(false)
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 [loading, result, error, makeRequest] = app.cores.api.useRequest(
props.fetchMethod,
{
limit: hopNumber,
trim: offset,
},
)
const onClickPrev = () => {
if (offset === 0) {
return
}
const onClickPrev = () => {
if (offset === 0) {
return
}
setOffset((value) => {
const newOffset = value - hopNumber
setOffset((value) => {
const newOffset = value - hopNumber
// check if newOffset is NaN
if (newOffset !== newOffset) {
return false
}
// check if newOffset is NaN
if (newOffset !== newOffset) {
return false
}
if (typeof makeRequest === "function") {
makeRequest({
trim: newOffset,
limit: hopNumber,
})
}
if (typeof makeRequest === "function") {
makeRequest({
trim: newOffset,
limit: hopNumber,
})
}
return newOffset
})
}
return newOffset
})
}
const onClickNext = () => {
if (ended) {
return
}
const onClickNext = () => {
if (ended) {
return
}
setOffset((value) => {
const newOffset = value + hopNumber
setOffset((value) => {
const newOffset = value + hopNumber
// check if newOffset is NaN
if (newOffset !== newOffset) {
return false
}
// check if newOffset is NaN
if (newOffset !== newOffset) {
return false
}
if (typeof makeRequest === "function") {
makeRequest({
trim: newOffset,
limit: hopNumber,
})
}
if (typeof makeRequest === "function") {
makeRequest({
trim: newOffset,
limit: hopNumber,
})
}
return newOffset
})
}
return newOffset
})
}
React.useEffect(() => {
if (result) {
if (typeof result.has_more !== "undefined") {
setEnded(!result.has_more)
} else {
setEnded(result.items.length < hopNumber)
}
}
}, [result])
React.useEffect(() => {
if (result) {
if (typeof result.has_more !== "undefined") {
setEnded(!result.has_more)
} else {
setEnded(result.items.length < hopNumber)
}
}
}, [result])
if (error) {
console.error(error)
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">
<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="music-releases-list">
<div className="music-releases-list-header">
<h1>
{
props.headerIcon
}
<Translation>
{(t) => t(props.headerTitle)}
</Translation>
</h1>
return (
<div className="music-releases-list">
<div className="music-releases-list-header">
<h1>
{props.headerIcon}
<Translation>{(t) => t(props.headerTitle)}</Translation>
</h1>
<div className="music-releases-list-actions">
<antd.Button
icon={<Icons.MdChevronLeft />}
onClick={onClickPrev}
disabled={offset === 0 || loading}
/>
<div className="music-releases-list-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="music-releases-list-items">
{
loading && <antd.Skeleton active />
}
{
!loading && result.items.map((playlist, index) => {
return <PlaylistItem
key={index}
playlist={playlist}
/>
})
}
</div>
</div>
<antd.Button
icon={<Icons.MdChevronRight />}
onClick={onClickNext}
disabled={ended || loading}
/>
</div>
</div>
<div className="music-releases-list-items">
{loading && <antd.Skeleton active />}
{!loading &&
result.items.map((playlist, index) => {
return <Playlist key={index} playlist={playlist} />
})}
</div>
</div>
)
}
export default ReleasesList
export default ReleasesList

View File

@ -1,52 +1,63 @@
.music-releases-list {
display: flex;
flex-direction: column;
display: flex;
flex-direction: column;
overflow-x: visible;
overflow-x: visible;
.music-releases-list-header {
display: flex;
flex-direction: row;
.music-releases-list-header {
display: flex;
flex-direction: row;
align-items: center;
align-items: center;
margin-bottom: 20px;
margin-bottom: 20px;
h1 {
font-size: 1.5rem;
margin: 0;
}
h1 {
font-size: 1.5rem;
margin: 0;
}
.music-releases-list-actions {
display: flex;
flex-direction: row;
.music-releases-list-actions {
display: flex;
flex-direction: row;
gap: 10px;
gap: 10px;
align-self: center;
align-self: center;
margin-left: auto;
}
}
margin-left: auto;
}
}
.music-releases-list-items {
display: grid;
.music-releases-list-items {
display: grid;
grid-gap: 20px;
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-gap: 20px;
grid-template-columns: repeat(2, 1fr);
min-width: 372px !important;
@media (min-width: 768px) {
grid-template-columns: repeat(3, 1fr);
}
@media (min-width: 2000px) {
grid-template-columns: repeat(4, 1fr);
}
@media (min-width: 1000px) {
grid-template-columns: repeat(4, 1fr);
}
@media (min-width: 2300px) {
grid-template-columns: repeat(5, 1fr);
}
@media (min-width: 1500px) {
grid-template-columns: repeat(7, 1fr);
}
.playlistItem {
justify-self: center;
}
}
}
@media (min-width: 1600px) {
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 MusicTrack from "@components/Music/Track"
import PlaylistItem from "@components/Music/PlaylistItem"
import Playlist from "@components/Music/Playlist"
const ResultGroupsDecorators = {
"playlists": {
icon: "MdPlaylistPlay",
label: "Playlists",
renderItem: (props) => {
return <PlaylistItem
return <Playlist
key={props.key}
playlist={props.item}
/>
@ -41,9 +41,18 @@ const SearchResults = ({
let groupsKeys = Object.keys(data)
// filter out empty groups
// filter out groups with no items array property
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) {
@ -86,7 +95,7 @@ const SearchResults = ({
<div className="music-explorer_search_results_group_list">
{
data[key].map((item, index) => {
data[key].items.map((item, index) => {
return decorator.renderItem({
key: index,
item

View File

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

View File

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

View File

@ -9,56 +9,56 @@ import PlaylistLibraryView from "./views/playlists"
import "./index.less"
const TabToView = {
tracks: TracksLibraryView,
playlist: PlaylistLibraryView,
releases: PlaylistLibraryView,
tracks: TracksLibraryView,
playlist: PlaylistLibraryView,
releases: PlaylistLibraryView,
}
const TabToHeader = {
tracks: {
icon: <Icons.MdMusicNote />,
label: "Tracks",
},
playlist: {
icon: <Icons.MdPlaylistPlay />,
label: "Playlists",
},
tracks: {
icon: <Icons.MdMusicNote />,
label: "Tracks",
},
playlist: {
icon: <Icons.MdPlaylistPlay />,
label: "Playlists",
},
}
const Library = (props) => {
const [selectedTab, setSelectedTab] = React.useState("tracks")
const [selectedTab, setSelectedTab] = React.useState("tracks")
return <div className="music-library">
<div className="music-library_header">
<h1>Library</h1>
return (
<div className="music-library">
<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
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>
{
selectedTab && TabToView[selectedTab] && React.createElement(TabToView[selectedTab])
}
</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 TracksLibraryView = () => {
const [offset, setOffset] = React.useState(0)
const [list, setList] = React.useState([])
const [hasMore, setHasMore] = React.useState(true)
const [initialLoading, setInitialLoading] = React.useState(true)
const [offset, setOffset] = React.useState(0)
const [items, setItems] = React.useState([])
const [hasMore, setHasMore] = React.useState(true)
const [initialLoading, setInitialLoading] = React.useState(true)
const [L_Favourites, R_Favourites, E_Favourites, M_Favourites] = app.cores.api.useRequest(MusicModel.getFavouriteFolder, {
offset: offset,
limit: loadLimit,
})
const [L_Favourites, R_Favourites, E_Favourites, M_Favourites] =
app.cores.api.useRequest(MusicModel.getFavouriteFolder, {
offset: offset,
limit: loadLimit,
})
async function onLoadMore() {
const newOffset = offset + loadLimit
async function onLoadMore() {
const newOffset = offset + loadLimit
setOffset(newOffset)
setOffset(newOffset)
M_Favourites({
offset: newOffset,
limit: loadLimit,
})
}
M_Favourites({
offset: newOffset,
limit: loadLimit,
})
}
React.useEffect(() => {
if (R_Favourites && R_Favourites.tracks) {
if (initialLoading === true) {
setInitialLoading(false)
}
React.useEffect(() => {
if (R_Favourites && R_Favourites.tracks) {
if (initialLoading === true) {
setInitialLoading(false)
}
if (R_Favourites.tracks.list.length === 0) {
setHasMore(false)
} else {
setList((prev) => {
prev = [
...prev,
...R_Favourites.tracks.list,
]
if (R_Favourites.tracks.items.length === 0) {
setHasMore(false)
} else {
setItems((prev) => {
prev = [...prev, ...R_Favourites.tracks.items]
return prev
})
}
}
}, [R_Favourites])
return prev
})
}
}
}, [R_Favourites])
if (E_Favourites) {
return <antd.Result
status="warning"
title="Failed to load"
subTitle={E_Favourites}
/>
}
if (E_Favourites) {
return (
<antd.Result
status="warning"
title="Failed to load"
subTitle={E_Favourites}
/>
)
}
if (initialLoading) {
return <antd.Skeleton active />
}
if (initialLoading) {
return <antd.Skeleton active />
}
return <PlaylistView
noHeader
loading={L_Favourites}
type="vertical"
playlist={{
list: list
}}
onLoadMore={onLoadMore}
hasMore={hasMore}
length={R_Favourites.tracks.total_length}
/>
return (
<PlaylistView
noHeader
noSearch
loading={L_Favourites}
type="vertical"
playlist={{
items: items,
total_length: R_Favourites.tracks.total_items,
}}
onLoadMore={onLoadMore}
hasMore={hasMore}
length={R_Favourites.tracks.total_length}
/>
)
}
export default TracksLibraryView
export default TracksLibraryView