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 (
<div className="music_navbar">
<Searcher <Searcher
useUrlQuery useUrlQuery
renderResults={false} renderResults={false}
model={MusicModel.search} model={async (keywords, params) =>
SearchModel.search(keywords, params, ["tracks"])
}
onSearchResult={props.setSearchResults} onSearchResult={props.setSearchResults}
onEmpty={() => props.setSearchResults(false)} onEmpty={() => props.setSearchResults(false)}
/> />
</div> </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,20 +3,23 @@ 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(
props.fetchMethod,
{
limit: hopNumber, limit: hopNumber,
trim: offset trim: offset,
}) },
)
const onClickPrev = () => { const onClickPrev = () => {
if (offset === 0) { if (offset === 0) {
@ -79,24 +82,23 @@ const ReleasesList = (props) => {
if (error) { if (error) {
console.error(error) console.error(error)
return <div className="playlistExplorer_section"> return (
<div className="playlistExplorer_section">
<antd.Result <antd.Result
status="warning" status="warning"
title="Failed to load" title="Failed to load"
subTitle="We are sorry, but we could not load this requests. Please try again later." 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">
<div className="music-releases-list-header"> <div className="music-releases-list-header">
<h1> <h1>
{ {props.headerIcon}
props.headerIcon <Translation>{(t) => t(props.headerTitle)}</Translation>
}
<Translation>
{(t) => t(props.headerTitle)}
</Translation>
</h1> </h1>
<div className="music-releases-list-actions"> <div className="music-releases-list-actions">
@ -114,19 +116,14 @@ const ReleasesList = (props) => {
</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
key={index}
playlist={playlist}
/>
})
}
</div> </div>
</div> </div>
)
} }
export default ReleasesList export default ReleasesList

View File

@ -33,20 +33,31 @@
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) {
grid-template-columns: repeat(7, 1fr);
}
@media (min-width: 1920px) {
grid-template-columns: repeat(9, 1fr);
}
.playlist {
justify-self: center; 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"
@ -18,9 +20,9 @@ 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

@ -35,7 +35,6 @@ html {
&:hover { &:hover {
.featured_playlist_content { .featured_playlist_content {
h1, h1,
p { p {
-webkit-text-stroke-width: 1.6px; -webkit-text-stroke-width: 1.6px;
@ -106,8 +105,6 @@ html {
padding: 10px 20px; padding: 10px 20px;
} }
} }
} }
.music_navbar { .music_navbar {
@ -248,7 +245,9 @@ html {
@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;

View File

@ -28,10 +28,9 @@ const TabToHeader = {
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">
<div className="music-library_header"> <div className="music-library_header">
<h1>Library</h1>
<antd.Segmented <antd.Segmented
value={selectedTab} value={selectedTab}
onChange={setSelectedTab} onChange={setSelectedTab}
@ -39,26 +38,27 @@ const Library = (props) => {
{ {
value: "tracks", value: "tracks",
label: "Tracks", label: "Tracks",
icon: <Icons.MdMusicNote /> icon: <Icons.MdMusicNote />,
}, },
{ {
value: "playlist", value: "playlist",
label: "Playlists", label: "Playlists",
icon: <Icons.MdPlaylistPlay /> icon: <Icons.MdPlaylistPlay />,
}, },
{ {
value: "releases", value: "releases",
label: "Releases", label: "Releases",
icon: <Icons.MdPlaylistPlay /> icon: <Icons.MdPlaylistPlay />,
} },
]} ]}
/> />
</div> </div>
{ {selectedTab &&
selectedTab && TabToView[selectedTab] && React.createElement(TabToView[selectedTab]) TabToView[selectedTab] &&
} React.createElement(TabToView[selectedTab])}
</div> </div>
)
} }
export default Library export default Library

View File

@ -9,11 +9,12 @@ 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] =
app.cores.api.useRequest(MusicModel.getFavouriteFolder, {
offset: offset, offset: offset,
limit: loadLimit, limit: loadLimit,
}) })
@ -35,14 +36,11 @@ const TracksLibraryView = () => {
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
}) })
@ -51,28 +49,34 @@ const TracksLibraryView = () => {
}, [R_Favourites]) }, [R_Favourites])
if (E_Favourites) { if (E_Favourites) {
return <antd.Result return (
<antd.Result
status="warning" status="warning"
title="Failed to load" title="Failed to load"
subTitle={E_Favourites} subTitle={E_Favourites}
/> />
)
} }
if (initialLoading) { if (initialLoading) {
return <antd.Skeleton active /> return <antd.Skeleton active />
} }
return <PlaylistView return (
<PlaylistView
noHeader noHeader
noSearch
loading={L_Favourites} loading={L_Favourites}
type="vertical" type="vertical"
playlist={{ playlist={{
list: list items: items,
total_length: R_Favourites.tracks.total_items,
}} }}
onLoadMore={onLoadMore} onLoadMore={onLoadMore}
hasMore={hasMore} hasMore={hasMore}
length={R_Favourites.tracks.total_length} length={R_Favourites.tracks.total_length}
/> />
)
} }
export default TracksLibraryView export default TracksLibraryView