mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-09 18:44:16 +00:00
merge from local
This commit is contained in:
parent
75f1dddf48
commit
dfcafc6b18
@ -336,6 +336,10 @@ class ComtyApp extends React.Component {
|
|||||||
|
|
||||||
await this.flushState()
|
await this.flushState()
|
||||||
},
|
},
|
||||||
|
"auth:disabled_account": async () => {
|
||||||
|
await SessionModel.removeToken()
|
||||||
|
app.navigation.goAuth()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flushState = async () => {
|
flushState = async () => {
|
||||||
|
@ -240,8 +240,8 @@ const PlaylistView = (props) => {
|
|||||||
playlistType,
|
playlistType,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{
|
||||||
<div className="play_info_wrapper">
|
!props.noHeader && <div className="play_info_wrapper">
|
||||||
<div className="play_info">
|
<div className="play_info">
|
||||||
<div className="play_info_cover">
|
<div className="play_info_cover">
|
||||||
<ImageViewer src={playlist.cover ?? playlist?.thumbnail ?? "/assets/no_song.png"} />
|
<ImageViewer src={playlist.cover ?? playlist?.thumbnail ?? "/assets/no_song.png"} />
|
||||||
@ -298,12 +298,6 @@ const PlaylistView = (props) => {
|
|||||||
Play
|
Play
|
||||||
</antd.Button>
|
</antd.Button>
|
||||||
|
|
||||||
{
|
|
||||||
!props.favorite && <antd.Button
|
|
||||||
icon={<Icons.MdFavorite />}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
playlist.description && <antd.Button
|
playlist.description && <antd.Button
|
||||||
icon={<Icons.MdInfo />}
|
icon={<Icons.MdInfo />}
|
||||||
@ -331,6 +325,7 @@ const PlaylistView = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<div className="list">
|
<div className="list">
|
||||||
{
|
{
|
||||||
|
@ -159,7 +159,9 @@ export default React.memo((props) => {
|
|||||||
props.attachments?.length > 0 && <BearCarousel
|
props.attachments?.length > 0 && <BearCarousel
|
||||||
data={props.attachments.map((attachment, index) => {
|
data={props.attachments.map((attachment, index) => {
|
||||||
if (typeof attachment !== "object") {
|
if (typeof attachment !== "object") {
|
||||||
return null
|
attachment = {
|
||||||
|
url: attachment,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
45
packages/app/src/components/SponsorsList/index.jsx
Normal file
45
packages/app/src/components/SponsorsList/index.jsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import React from "react"
|
||||||
|
import { Skeleton } from "antd"
|
||||||
|
|
||||||
|
import "./index.less"
|
||||||
|
|
||||||
|
const SponsorsList = () => {
|
||||||
|
const fetchAPI = "https://raw.githubusercontent.com/ragestudio/comty/refs/heads/master/sponsors.json"
|
||||||
|
|
||||||
|
const [loading, setLoading] = React.useState(true)
|
||||||
|
const [sponsors, setSponsors] = React.useState(null)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
fetch(fetchAPI)
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
setLoading(false)
|
||||||
|
setSponsors(data)
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <Skeleton active />
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="sponsors_list">
|
||||||
|
{
|
||||||
|
sponsors && sponsors.map((sponsor, index) => {
|
||||||
|
return <a
|
||||||
|
key={index}
|
||||||
|
href={sponsor.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="sponsor"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={sponsor.promo_badge}
|
||||||
|
alt={sponsor.name}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SponsorsList
|
23
packages/app/src/components/SponsorsList/index.less
Normal file
23
packages/app/src/components/SponsorsList/index.less
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
.sponsors_list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
.sponsor {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
gap: 5px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 250px;
|
||||||
|
max-height: 100px;
|
||||||
|
|
||||||
|
max-width: 250px;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -72,6 +72,10 @@ export default class APICore extends Core {
|
|||||||
app.eventBus.emit("session.invalid", error)
|
app.eventBus.emit("session.invalid", error)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.client.eventBus.on("auth:disabled_account", () => {
|
||||||
|
app.eventBus.emit("auth:disabled_account")
|
||||||
|
})
|
||||||
|
|
||||||
// make a basic request to check if the API is available
|
// make a basic request to check if the API is available
|
||||||
await this.client.baseRequest({
|
await this.client.baseRequest({
|
||||||
method: "head",
|
method: "head",
|
||||||
|
4
packages/app/src/pages/about/index.jsx
Normal file
4
packages/app/src/pages/about/index.jsx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export default () => {
|
||||||
|
app.location.push("/settings?tab=about")
|
||||||
|
return null
|
||||||
|
}
|
108
packages/app/src/pages/disable-account/index.jsx
Normal file
108
packages/app/src/pages/disable-account/index.jsx
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import React from "react"
|
||||||
|
import { Alert, Divider, Checkbox, Button } from "antd"
|
||||||
|
|
||||||
|
import AuthModel from "@models/auth"
|
||||||
|
|
||||||
|
const DisableAccountPage = () => {
|
||||||
|
const [confirm, setConfirm] = React.useState(false)
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
if (!confirm) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthModel.disableAccount({ confirm })
|
||||||
|
.then(() => {
|
||||||
|
app.message.success("Your account has been disabled. More information will be sent to your email.")
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
app.message.error("Failed to disable your account.")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="flex-column gap-20 align-start w-100">
|
||||||
|
<div className="flex-column align-start">
|
||||||
|
<h1>Account Disablement</h1>
|
||||||
|
<p>You are about to disable your account.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-column gap-10 align-start">
|
||||||
|
<Divider
|
||||||
|
style={{
|
||||||
|
margin: "5px 0"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<p>
|
||||||
|
Due to our security policy, your data is retained when your account is disabled, this retention period is 2 months, but can be extended if appropriate.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Once the account hold expires, all of your data will be permanently deleted.
|
||||||
|
</p>
|
||||||
|
<Divider
|
||||||
|
style={{
|
||||||
|
margin: "5px 0"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<p>
|
||||||
|
This action cannot be stopped directly, you must contact support to stop this process.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>
|
||||||
|
In case an imminent deletion is necessary, you should contact support.
|
||||||
|
</strong>
|
||||||
|
</p>
|
||||||
|
<Divider
|
||||||
|
style={{
|
||||||
|
margin: "5px 0"
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-column gap-10 align-start w-100">
|
||||||
|
<p>
|
||||||
|
These are all the data that are deleted after the retention time:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>Your account data</li>
|
||||||
|
<li>Your profile</li>
|
||||||
|
<li>Sessions logs</li>
|
||||||
|
<li>All content you have created; tracks, videos, posts, images, products, events, etc...</li>
|
||||||
|
<li>All information related to your account in our Databases</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
While your account is on hold, all of your information will remain visible, but your account will not be usable by any services or log in.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Divider
|
||||||
|
style={{
|
||||||
|
margin: "10px 0"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Alert
|
||||||
|
type="warning"
|
||||||
|
message="Some features like API keys keeps working until your account is fully deleted."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex-column align-start justify-space-between gap-10 w-100">
|
||||||
|
<Checkbox
|
||||||
|
checked={confirm}
|
||||||
|
onChange={(e) => setConfirm(e.target.checked)}
|
||||||
|
>
|
||||||
|
Im aware of this action and I want to continue.
|
||||||
|
</Checkbox>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={submit}
|
||||||
|
disabled={!confirm}
|
||||||
|
>
|
||||||
|
Disable Account
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DisableAccountPage
|
@ -1,143 +0,0 @@
|
|||||||
import React from "react"
|
|
||||||
import * as antd from "antd"
|
|
||||||
|
|
||||||
import PlaylistView from "@components/Music/PlaylistView"
|
|
||||||
|
|
||||||
import MusicModel from "@models/music"
|
|
||||||
|
|
||||||
export default class FavoriteTracks extends React.Component {
|
|
||||||
state = {
|
|
||||||
error: null,
|
|
||||||
|
|
||||||
initialLoading: true,
|
|
||||||
loading: false,
|
|
||||||
|
|
||||||
list: [],
|
|
||||||
total_length: 0,
|
|
||||||
|
|
||||||
empty: false,
|
|
||||||
hasMore: true,
|
|
||||||
offset: 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
static loadLimit = 50
|
|
||||||
|
|
||||||
componentDidMount = async () => {
|
|
||||||
await this.loadItems()
|
|
||||||
}
|
|
||||||
|
|
||||||
onLoadMore = async () => {
|
|
||||||
console.log(`Loading more items...`, this.state.offset)
|
|
||||||
|
|
||||||
const newOffset = this.state.offset + FavoriteTracks.loadLimit
|
|
||||||
|
|
||||||
await this.setState({
|
|
||||||
offset: newOffset,
|
|
||||||
})
|
|
||||||
|
|
||||||
await this.loadItems({
|
|
||||||
offset: newOffset,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
loadItems = async ({
|
|
||||||
replace = false,
|
|
||||||
offset = 0,
|
|
||||||
limit = FavoriteTracks.loadLimit,
|
|
||||||
} = {}) => {
|
|
||||||
this.setState({
|
|
||||||
loading: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await MusicModel.getFavouriteFolder({
|
|
||||||
offset: offset,
|
|
||||||
limit: limit,
|
|
||||||
}).catch((error) => {
|
|
||||||
this.setState({
|
|
||||||
error: error.message,
|
|
||||||
})
|
|
||||||
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log("Loaded favorites => ", result)
|
|
||||||
|
|
||||||
if (result) {
|
|
||||||
const {
|
|
||||||
tracks,
|
|
||||||
releases,
|
|
||||||
playlists,
|
|
||||||
total_length,
|
|
||||||
} = result
|
|
||||||
|
|
||||||
const data = [
|
|
||||||
...tracks.list,
|
|
||||||
...releases.list,
|
|
||||||
...playlists.list,
|
|
||||||
]
|
|
||||||
|
|
||||||
if (total_length === 0) {
|
|
||||||
this.setState({
|
|
||||||
empty: true,
|
|
||||||
hasMore: false,
|
|
||||||
initialLoading: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.length === 0) {
|
|
||||||
return this.setState({
|
|
||||||
empty: false,
|
|
||||||
hasMore: false,
|
|
||||||
initialLoading: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (replace) {
|
|
||||||
this.setState({
|
|
||||||
list: data,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this.setState({
|
|
||||||
list: [...this.state.list, ...data],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
total_length
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
loading: false,
|
|
||||||
initialLoading: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.state.error) {
|
|
||||||
return <antd.Result
|
|
||||||
status="error"
|
|
||||||
title="Error"
|
|
||||||
subTitle={this.state.error}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.initialLoading) {
|
|
||||||
return <antd.Skeleton active />
|
|
||||||
}
|
|
||||||
|
|
||||||
return <PlaylistView
|
|
||||||
favorite
|
|
||||||
type="vertical"
|
|
||||||
playlist={{
|
|
||||||
title: "Your favorites",
|
|
||||||
cover: "https://storage.ragestudio.net/comty-static-assets/favorite_song.png",
|
|
||||||
list: this.state.list
|
|
||||||
}}
|
|
||||||
centered={app.isMobile}
|
|
||||||
onLoadMore={this.onLoadMore}
|
|
||||||
hasMore={this.state.hasMore}
|
|
||||||
length={this.state.total_length}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +1,4 @@
|
|||||||
import LibraryTab from "./library"
|
import LibraryTab from "./library"
|
||||||
import FavoritesTab from "./favorites"
|
|
||||||
import ExploreTab from "./explore"
|
import ExploreTab from "./explore"
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
@ -15,12 +14,6 @@ export default [
|
|||||||
icon: "MdLibraryMusic",
|
icon: "MdLibraryMusic",
|
||||||
component: LibraryTab,
|
component: LibraryTab,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: "favorites",
|
|
||||||
label: "Favorites",
|
|
||||||
icon: "MdFavoriteBorder",
|
|
||||||
component: FavoritesTab,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: "radio",
|
key: "radio",
|
||||||
label: "Radio",
|
label: "Radio",
|
||||||
|
@ -1,189 +1,40 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import * as antd from "antd"
|
import * as antd from "antd"
|
||||||
import classnames from "classnames"
|
|
||||||
|
|
||||||
import Image from "@components/Image"
|
|
||||||
import { Icons } from "@components/Icons"
|
import { Icons } from "@components/Icons"
|
||||||
import OpenPlaylistCreator from "@components/Music/PlaylistCreator"
|
|
||||||
|
|
||||||
import MusicModel from "@models/music"
|
import TracksLibraryView from "./views/tracks"
|
||||||
|
import PlaylistLibraryView from "./views/playlists"
|
||||||
|
|
||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
const ReleaseTypeDecorators = {
|
const TabToView = {
|
||||||
"user": () => <p >
|
tracks: TracksLibraryView,
|
||||||
<Icons.MdPlaylistAdd />
|
playlist: PlaylistLibraryView,
|
||||||
Playlist
|
releases: PlaylistLibraryView,
|
||||||
</p>,
|
|
||||||
"playlist": () => <p >
|
|
||||||
<Icons.MdPlaylistAdd />
|
|
||||||
Playlist
|
|
||||||
</p>,
|
|
||||||
"editorial": () => <p >
|
|
||||||
<Icons.MdPlaylistAdd />
|
|
||||||
Official Playlist
|
|
||||||
</p>,
|
|
||||||
"single": () => <p >
|
|
||||||
<Icons.MdMusicNote />
|
|
||||||
Single
|
|
||||||
</p>,
|
|
||||||
"album": () => <p >
|
|
||||||
<Icons.MdAlbum />
|
|
||||||
Album
|
|
||||||
</p>,
|
|
||||||
"ep": () => <p >
|
|
||||||
<Icons.MdAlbum />
|
|
||||||
EP
|
|
||||||
</p>,
|
|
||||||
"mix": () => <p >
|
|
||||||
<Icons.MdMusicNote />
|
|
||||||
Mix
|
|
||||||
</p>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isNotAPlaylist(type) {
|
const TabToHeader = {
|
||||||
return type === "album" || type === "ep" || type === "mix" || type === "single"
|
tracks: {
|
||||||
}
|
icon: <Icons.MdMusicNote />,
|
||||||
|
label: "Tracks",
|
||||||
const PlaylistItem = (props) => {
|
},
|
||||||
const data = props.data ?? {}
|
playlist: {
|
||||||
|
icon: <Icons.MdPlaylistPlay />,
|
||||||
const handleOnClick = () => {
|
label: "Playlists",
|
||||||
if (typeof props.onClick === "function") {
|
},
|
||||||
props.onClick(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.type !== "action") {
|
|
||||||
if (data.service) {
|
|
||||||
return app.navigation.goToPlaylist(`${data._id}?service=${data.service}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return app.navigation.goToPlaylist(data._id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div
|
|
||||||
className={classnames(
|
|
||||||
"playlist_item",
|
|
||||||
{
|
|
||||||
["action"]: props.type === "action",
|
|
||||||
["release"]: isNotAPlaylist(data.type),
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
onClick={handleOnClick}
|
|
||||||
>
|
|
||||||
<div className="playlist_item_icon">
|
|
||||||
{
|
|
||||||
React.isValidElement(data.icon)
|
|
||||||
? <div className="playlist_item_icon_svg">
|
|
||||||
{data.icon}
|
|
||||||
</div>
|
|
||||||
: <Image
|
|
||||||
src={data.icon}
|
|
||||||
alt="playlist icon"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="playlist_item_info">
|
|
||||||
<div className="playlist_item_info_title">
|
|
||||||
<h1>
|
|
||||||
{
|
|
||||||
data.service === "tidal" && <Icons.SiTidal />
|
|
||||||
}
|
|
||||||
{
|
|
||||||
data.title ?? "Unnamed playlist"
|
|
||||||
}
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
data.owner && <div className="playlist_item_info_owner">
|
|
||||||
<h4>
|
|
||||||
{
|
|
||||||
data.owner
|
|
||||||
}
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
data.description && <div className="playlist_item_info_description">
|
|
||||||
<p>
|
|
||||||
{
|
|
||||||
data.description
|
|
||||||
}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{
|
|
||||||
ReleaseTypeDecorators[String(data.type).toLowerCase()] && ReleaseTypeDecorators[String(data.type).toLowerCase()](props)
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
data.public
|
|
||||||
? <p>
|
|
||||||
<Icons.MdVisibility />
|
|
||||||
Public
|
|
||||||
</p>
|
|
||||||
|
|
||||||
: <p>
|
|
||||||
<Icons.MdVisibilityOff />
|
|
||||||
Private
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
const OwnPlaylists = (props) => {
|
|
||||||
const [L_Playlists, R_Playlists, E_Playlists, M_Playlists] = app.cores.api.useRequest(MusicModel.getFavoritePlaylists)
|
|
||||||
|
|
||||||
if (E_Playlists) {
|
|
||||||
console.error(E_Playlists)
|
|
||||||
|
|
||||||
return <antd.Result
|
|
||||||
status="warning"
|
|
||||||
title="Failed to load"
|
|
||||||
subTitle="We are sorry, but we could not load your playlists. Please try again later."
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
if (L_Playlists) {
|
|
||||||
return <antd.Skeleton />
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className="own_playlists">
|
|
||||||
<PlaylistItem
|
|
||||||
type="action"
|
|
||||||
data={{
|
|
||||||
icon: <Icons.MdPlaylistAdd />,
|
|
||||||
title: "Create new",
|
|
||||||
}}
|
|
||||||
onClick={OpenPlaylistCreator}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
R_Playlists.items.map((playlist) => {
|
|
||||||
playlist.icon = playlist.cover ?? playlist.thumbnail
|
|
||||||
playlist.description = `${playlist.numberOfTracks ?? playlist.list.length} tracks`
|
|
||||||
|
|
||||||
return <PlaylistItem
|
|
||||||
key={playlist.id}
|
|
||||||
data={playlist}
|
|
||||||
/>
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Library = (props) => {
|
const Library = (props) => {
|
||||||
|
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>
|
<h1>Library</h1>
|
||||||
|
|
||||||
<antd.Segmented
|
<antd.Segmented
|
||||||
|
value={selectedTab}
|
||||||
|
onChange={setSelectedTab}
|
||||||
options={[
|
options={[
|
||||||
{
|
{
|
||||||
value: "tracks",
|
value: "tracks",
|
||||||
@ -195,18 +46,18 @@ const Library = (props) => {
|
|||||||
label: "Playlists",
|
label: "Playlists",
|
||||||
icon: <Icons.MdPlaylistPlay />
|
icon: <Icons.MdPlaylistPlay />
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: "releases",
|
||||||
|
label: "Releases",
|
||||||
|
icon: <Icons.MdPlaylistPlay />
|
||||||
|
}
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<PlaylistItem
|
|
||||||
type="action"
|
{
|
||||||
data={{
|
selectedTab && TabToView[selectedTab] && React.createElement(TabToView[selectedTab])
|
||||||
icon: <Icons.MdPlaylistAdd />,
|
}
|
||||||
title: "Create new",
|
|
||||||
}}
|
|
||||||
onClick={OpenPlaylistCreator}
|
|
||||||
/>
|
|
||||||
<OwnPlaylists />
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,170 +1,3 @@
|
|||||||
@playlist_item_icon_size: 50px;
|
|
||||||
|
|
||||||
.playlist_item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
height: 70px;
|
|
||||||
|
|
||||||
gap: 10px;
|
|
||||||
|
|
||||||
padding: 10px;
|
|
||||||
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
|
|
||||||
border-radius: 12px;
|
|
||||||
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
&.release {
|
|
||||||
.playlist_item_icon {
|
|
||||||
img {
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.action {
|
|
||||||
.playlist_item_icon {
|
|
||||||
color: var(--colorPrimary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.playlist_item_icon {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
width: @playlist_item_icon_size;
|
|
||||||
height: @playlist_item_icon_size;
|
|
||||||
|
|
||||||
min-width: @playlist_item_icon_size;
|
|
||||||
min-height: @playlist_item_icon_size;
|
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
border-radius: 12px;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: @playlist_item_icon_size;
|
|
||||||
height: @playlist_item_icon_size;
|
|
||||||
|
|
||||||
min-width: @playlist_item_icon_size;
|
|
||||||
min-height: @playlist_item_icon_size;
|
|
||||||
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.playlist_item_icon_svg {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
background-color: var(--background-color-accent);
|
|
||||||
|
|
||||||
svg {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.playlist_item_info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
//align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
height: 100%;
|
|
||||||
width: 90%;
|
|
||||||
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
|
|
||||||
.playlist_item_info_title {
|
|
||||||
display: inline;
|
|
||||||
|
|
||||||
|
|
||||||
font-size: 0.8rem;
|
|
||||||
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-weight: 700;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.playlist_item_info_owner {
|
|
||||||
display: inline;
|
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
font-size: 0.7rem;
|
|
||||||
|
|
||||||
h4 {
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.playlist_item_info_description {
|
|
||||||
display: inline-flex;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
gap: 10px;
|
|
||||||
|
|
||||||
font-size: 0.7rem;
|
|
||||||
|
|
||||||
p {
|
|
||||||
font-weight: 500;
|
|
||||||
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
text-transform: uppercase;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
margin-right: 0.4rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
h4,
|
|
||||||
h5,
|
|
||||||
h6,
|
|
||||||
p,
|
|
||||||
span {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.own_playlists {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.music-library {
|
.music-library {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -184,4 +17,11 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.music-library-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
}
|
}
|
@ -0,0 +1,181 @@
|
|||||||
|
import React from "react"
|
||||||
|
import * as antd from "antd"
|
||||||
|
import classnames from "classnames"
|
||||||
|
|
||||||
|
import Image from "@components/Image"
|
||||||
|
import { Icons } from "@components/Icons"
|
||||||
|
import OpenPlaylistCreator from "@components/Music/PlaylistCreator"
|
||||||
|
|
||||||
|
import MusicModel from "@models/music"
|
||||||
|
|
||||||
|
import "./index.less"
|
||||||
|
|
||||||
|
const ReleaseTypeDecorators = {
|
||||||
|
"user": () => <p >
|
||||||
|
<Icons.MdPlaylistAdd />
|
||||||
|
Playlist
|
||||||
|
</p>,
|
||||||
|
"playlist": () => <p >
|
||||||
|
<Icons.MdPlaylistAdd />
|
||||||
|
Playlist
|
||||||
|
</p>,
|
||||||
|
"editorial": () => <p >
|
||||||
|
<Icons.MdPlaylistAdd />
|
||||||
|
Official Playlist
|
||||||
|
</p>,
|
||||||
|
"single": () => <p >
|
||||||
|
<Icons.MdMusicNote />
|
||||||
|
Single
|
||||||
|
</p>,
|
||||||
|
"album": () => <p >
|
||||||
|
<Icons.MdAlbum />
|
||||||
|
Album
|
||||||
|
</p>,
|
||||||
|
"ep": () => <p >
|
||||||
|
<Icons.MdAlbum />
|
||||||
|
EP
|
||||||
|
</p>,
|
||||||
|
"mix": () => <p >
|
||||||
|
<Icons.MdMusicNote />
|
||||||
|
Mix
|
||||||
|
</p>,
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNotAPlaylist(type) {
|
||||||
|
return type === "album" || type === "ep" || type === "mix" || type === "single"
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlaylistItem = (props) => {
|
||||||
|
const data = props.data ?? {}
|
||||||
|
|
||||||
|
const handleOnClick = () => {
|
||||||
|
if (typeof props.onClick === "function") {
|
||||||
|
props.onClick(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.type !== "action") {
|
||||||
|
if (data.service) {
|
||||||
|
return app.navigation.goToPlaylist(`${data._id}?service=${data.service}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return app.navigation.goToPlaylist(data._id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div
|
||||||
|
className={classnames(
|
||||||
|
"playlist_item",
|
||||||
|
{
|
||||||
|
["action"]: props.type === "action",
|
||||||
|
["release"]: isNotAPlaylist(data.type),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
onClick={handleOnClick}
|
||||||
|
>
|
||||||
|
<div className="playlist_item_icon">
|
||||||
|
{
|
||||||
|
React.isValidElement(data.icon)
|
||||||
|
? <div className="playlist_item_icon_svg">
|
||||||
|
{data.icon}
|
||||||
|
</div>
|
||||||
|
: <Image
|
||||||
|
src={data.icon}
|
||||||
|
alt="playlist icon"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="playlist_item_info">
|
||||||
|
<div className="playlist_item_info_title">
|
||||||
|
<h1>
|
||||||
|
{
|
||||||
|
data.service === "tidal" && <Icons.SiTidal />
|
||||||
|
}
|
||||||
|
{
|
||||||
|
data.title ?? "Unnamed playlist"
|
||||||
|
}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
data.owner && <div className="playlist_item_info_owner">
|
||||||
|
<h4>
|
||||||
|
{
|
||||||
|
data.owner
|
||||||
|
}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
data.description && <div className="playlist_item_info_description">
|
||||||
|
<p>
|
||||||
|
{
|
||||||
|
data.description
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{
|
||||||
|
ReleaseTypeDecorators[String(data.type).toLowerCase()] && ReleaseTypeDecorators[String(data.type).toLowerCase()](props)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
data.public
|
||||||
|
? <p>
|
||||||
|
<Icons.MdVisibility />
|
||||||
|
Public
|
||||||
|
</p>
|
||||||
|
|
||||||
|
: <p>
|
||||||
|
<Icons.MdVisibilityOff />
|
||||||
|
Private
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlaylistLibraryView = (props) => {
|
||||||
|
const [L_Playlists, R_Playlists, E_Playlists, M_Playlists] = app.cores.api.useRequest(MusicModel.getFavoritePlaylists)
|
||||||
|
|
||||||
|
if (E_Playlists) {
|
||||||
|
console.error(E_Playlists)
|
||||||
|
|
||||||
|
return <antd.Result
|
||||||
|
status="warning"
|
||||||
|
title="Failed to load"
|
||||||
|
subTitle="We are sorry, but we could not load your playlists. Please try again later."
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (L_Playlists) {
|
||||||
|
return <antd.Skeleton />
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="own_playlists">
|
||||||
|
<PlaylistItem
|
||||||
|
type="action"
|
||||||
|
data={{
|
||||||
|
icon: <Icons.MdPlaylistAdd />,
|
||||||
|
title: "Create new",
|
||||||
|
}}
|
||||||
|
onClick={OpenPlaylistCreator}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
R_Playlists.items.map((playlist) => {
|
||||||
|
playlist.icon = playlist.cover ?? playlist.thumbnail
|
||||||
|
playlist.description = `${playlist.numberOfTracks ?? playlist.list.length} tracks`
|
||||||
|
|
||||||
|
return <PlaylistItem
|
||||||
|
key={playlist.id}
|
||||||
|
data={playlist}
|
||||||
|
/>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PlaylistLibraryView
|
@ -0,0 +1,166 @@
|
|||||||
|
@playlist_item_icon_size: 50px;
|
||||||
|
|
||||||
|
.playlist_item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
height: 70px;
|
||||||
|
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&.release {
|
||||||
|
.playlist_item_icon {
|
||||||
|
img {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.action {
|
||||||
|
.playlist_item_icon {
|
||||||
|
color: var(--colorPrimary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist_item_icon {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
width: @playlist_item_icon_size;
|
||||||
|
height: @playlist_item_icon_size;
|
||||||
|
|
||||||
|
min-width: @playlist_item_icon_size;
|
||||||
|
min-height: @playlist_item_icon_size;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: @playlist_item_icon_size;
|
||||||
|
height: @playlist_item_icon_size;
|
||||||
|
|
||||||
|
min-width: @playlist_item_icon_size;
|
||||||
|
min-height: @playlist_item_icon_size;
|
||||||
|
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist_item_icon_svg {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
background-color: var(--background-color-accent);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist_item_info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
//align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
height: 100%;
|
||||||
|
width: 90%;
|
||||||
|
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
.playlist_item_info_title {
|
||||||
|
display: inline;
|
||||||
|
|
||||||
|
|
||||||
|
font-size: 0.8rem;
|
||||||
|
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-weight: 700;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist_item_info_owner {
|
||||||
|
display: inline;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
font-size: 0.7rem;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist_item_info_description {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
font-size: 0.7rem;
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
text-transform: uppercase;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
margin-right: 0.4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6,
|
||||||
|
p,
|
||||||
|
span {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.own_playlists {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
gap: 10px;
|
||||||
|
}
|
@ -0,0 +1,78 @@
|
|||||||
|
import React from "react"
|
||||||
|
import * as antd from "antd"
|
||||||
|
|
||||||
|
import PlaylistView from "@components/Music/PlaylistView"
|
||||||
|
|
||||||
|
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 [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
|
||||||
|
|
||||||
|
setOffset(newOffset)
|
||||||
|
|
||||||
|
M_Favourites({
|
||||||
|
offset: newOffset,
|
||||||
|
limit: loadLimit,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
]
|
||||||
|
|
||||||
|
return prev
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [R_Favourites])
|
||||||
|
|
||||||
|
if (E_Favourites) {
|
||||||
|
return <antd.Result
|
||||||
|
status="warning"
|
||||||
|
title="Failed to load"
|
||||||
|
subTitle={E_Favourites}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TracksLibraryView
|
@ -7,6 +7,12 @@ import MyReleasesList from "@components/MusicStudio/MyReleasesList"
|
|||||||
|
|
||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
|
const ReleasesAnalytics = () => {
|
||||||
|
return <div>
|
||||||
|
<h1>Analytics</h1>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
const MusicStudioPage = (props) => {
|
const MusicStudioPage = (props) => {
|
||||||
return <div
|
return <div
|
||||||
className="music-studio-page"
|
className="music-studio-page"
|
||||||
@ -25,6 +31,8 @@ const MusicStudioPage = (props) => {
|
|||||||
</antd.Button>
|
</antd.Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ReleasesAnalytics />
|
||||||
|
|
||||||
<MyReleasesList />
|
<MyReleasesList />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import * as antd from "antd"
|
|||||||
|
|
||||||
import { Icons } from "@components/Icons"
|
import { Icons } from "@components/Icons"
|
||||||
import LatencyIndicator from "@components/PerformanceIndicators/latency"
|
import LatencyIndicator from "@components/PerformanceIndicators/latency"
|
||||||
|
import SponsorsList from "@components/SponsorsList"
|
||||||
|
|
||||||
import config from "@config"
|
import config from "@config"
|
||||||
|
|
||||||
@ -157,6 +158,11 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="group">
|
||||||
|
<h3>Thanks to our sponsors</h3>
|
||||||
|
<SponsorsList />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="group">
|
<div className="group">
|
||||||
<div className="inline_field">
|
<div className="inline_field">
|
||||||
<div className="field_header">
|
<div className="field_header">
|
||||||
|
@ -32,6 +32,21 @@ export default {
|
|||||||
description: "Manage your active sessions",
|
description: "Manage your active sessions",
|
||||||
icon: "FiMonitor",
|
icon: "FiMonitor",
|
||||||
component: loadable(() => import("../components/sessions")),
|
component: loadable(() => import("../components/sessions")),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "disable-account",
|
||||||
|
group: "security.account",
|
||||||
|
title: "Disable Account",
|
||||||
|
description: "Disable your account",
|
||||||
|
icon: "FiUserX",
|
||||||
|
component: "Button",
|
||||||
|
props: {
|
||||||
|
danger: true,
|
||||||
|
children: "Disable",
|
||||||
|
onClick: () => {
|
||||||
|
app.location.push("/disable-account")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
@ -1,3 +1,15 @@
|
|||||||
|
// DIMENSIONS
|
||||||
|
|
||||||
|
// WIDTH
|
||||||
|
.w-100 {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// HEIGHT
|
||||||
|
.h-100 {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
// FLEX
|
// FLEX
|
||||||
.flex-row {
|
.flex-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -50,6 +62,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GAPS
|
// GAPS
|
||||||
|
.gap-20,
|
||||||
|
.gap20 {
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.gap-10,
|
.gap-10,
|
||||||
.gap10 {
|
.gap10 {
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
@ -68,6 +68,10 @@ export default {
|
|||||||
activated: {
|
activated: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
}
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -389,8 +389,8 @@ export default class Gateway {
|
|||||||
async initialize() {
|
async initialize() {
|
||||||
onExit(this.onGatewayExit)
|
onExit(this.onGatewayExit)
|
||||||
|
|
||||||
process.stdout.setMaxListeners(50)
|
process.stdout.setMaxListeners(150)
|
||||||
process.stderr.setMaxListeners(50)
|
process.stderr.setMaxListeners(150)
|
||||||
|
|
||||||
this.services = await scanServices()
|
this.services = await scanServices()
|
||||||
this.proxy = new Proxy()
|
this.proxy = new Proxy()
|
||||||
|
@ -8,4 +8,5 @@ export default class Account {
|
|||||||
static deleteSession = require("./methods/deleteSession").default
|
static deleteSession = require("./methods/deleteSession").default
|
||||||
static sendActivationCode = require("./methods/sendActivationCode").default
|
static sendActivationCode = require("./methods/sendActivationCode").default
|
||||||
static activateAccount = require("./methods/activateAccount").default
|
static activateAccount = require("./methods/activateAccount").default
|
||||||
|
static disableAccount = require("./methods/disableAccount").default
|
||||||
}
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
import { User } from "@db_models"
|
||||||
|
|
||||||
|
export default async (payload) => {
|
||||||
|
const { user_id } = payload
|
||||||
|
|
||||||
|
if (!user_id) {
|
||||||
|
throw new OperationError(400, "Missing user_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = await User.findOne({
|
||||||
|
_id: user_id,
|
||||||
|
}).select("+email")
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new OperationError(404, "User not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
user = await User.findOneAndUpdate(
|
||||||
|
{
|
||||||
|
_id: user._id.toString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
disabled: true
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return user.toObject()
|
||||||
|
}
|
@ -14,6 +14,10 @@ export default async ({ username, password, hash }, user) => {
|
|||||||
throw new OperationError(401, "User not found")
|
throw new OperationError(401, "User not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (user.disabled == true) {
|
||||||
|
throw new OperationError(401, "User is disabled")
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof hash !== "undefined") {
|
if (typeof hash !== "undefined") {
|
||||||
if (user.password !== hash) {
|
if (user.password !== hash) {
|
||||||
throw new OperationError(401, "Invalid credentials")
|
throw new OperationError(401, "Invalid credentials")
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
import AccountClass from "@classes/account"
|
||||||
|
import { OperationLog } from "@db_models"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
middlewares: ["withAuthentication"],
|
||||||
|
fn: async (req) => {
|
||||||
|
const user_id = req.auth.session.user_id
|
||||||
|
|
||||||
|
await OperationLog.create({
|
||||||
|
user_id: user_id,
|
||||||
|
type: "disable_account",
|
||||||
|
date: Date.now()
|
||||||
|
})
|
||||||
|
|
||||||
|
return await AccountClass.disableAccount({ user_id })
|
||||||
|
}
|
||||||
|
}
|
@ -31,6 +31,7 @@ export default async (req, res) => {
|
|||||||
|
|
||||||
if (user.activated === false) {
|
if (user.activated === false) {
|
||||||
return res.status(401).json({
|
return res.status(401).json({
|
||||||
|
code: user.email,
|
||||||
user_id: user._id.toString(),
|
user_id: user._id.toString(),
|
||||||
activation_required: true,
|
activation_required: true,
|
||||||
})
|
})
|
||||||
|
@ -38,7 +38,7 @@ export default {
|
|||||||
parentDir: req.auth.session.user_id,
|
parentDir: req.auth.session.user_id,
|
||||||
source: localFilepath,
|
source: localFilepath,
|
||||||
service: providerType,
|
service: providerType,
|
||||||
useCompression: req.headers["use-compression"] ?? true,
|
useCompression: ToBoolean(req.headers["use-compression"]) ?? true,
|
||||||
})
|
})
|
||||||
|
|
||||||
fs.promises.rm(tmpPath, { recursive: true, force: true })
|
fs.promises.rm(tmpPath, { recursive: true, force: true })
|
||||||
|
@ -47,6 +47,7 @@ export default (input, params = defaultParams) => {
|
|||||||
const commands = {
|
const commands = {
|
||||||
input: input,
|
input: input,
|
||||||
...params,
|
...params,
|
||||||
|
preset: "ultrafast",
|
||||||
output: outputFilepath,
|
output: outputFilepath,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,12 +10,23 @@ export default async (payload = {}) => {
|
|||||||
let { user_id, message, attachments, timestamp, reply_to, poll_options } = payload
|
let { user_id, message, attachments, timestamp, reply_to, poll_options } = payload
|
||||||
|
|
||||||
// check if is a Array and have at least one element
|
// check if is a Array and have at least one element
|
||||||
const isAttachmentsValid = Array.isArray(attachments) && attachments.length > 0
|
const isAttachmentArray = Array.isArray(attachments) && attachments.length > 0
|
||||||
|
|
||||||
if (!isAttachmentsValid && !message) {
|
if (!isAttachmentArray && !message) {
|
||||||
throw new OperationError(400, "Cannot create a post without message or attachments")
|
throw new OperationError(400, "Cannot create a post without message or attachments")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fix attachments with url strings
|
||||||
|
attachments = attachments.map((attachment) => {
|
||||||
|
if (typeof attachment === "string") {
|
||||||
|
attachment = {
|
||||||
|
url: attachment,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return attachment
|
||||||
|
})
|
||||||
|
|
||||||
if (!timestamp) {
|
if (!timestamp) {
|
||||||
timestamp = DateTime.local().toISO()
|
timestamp = DateTime.local().toISO()
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user