mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-09 10:34:17 +00:00
improve explore & library
This commit is contained in:
parent
39b427dea7
commit
e54c3a1abe
@ -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
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user