diff --git a/packages/app/src/pages/music/[type].jsx b/packages/app/src/pages/music/[type].jsx index 4fe4eb11..74feb53b 100755 --- a/packages/app/src/pages/music/[type].jsx +++ b/packages/app/src/pages/music/[type].jsx @@ -15,10 +15,6 @@ export default class MusicDashboard extends React.Component { primaryPanelRef = React.createRef() - componentDidMount() { - app.eventBus.emit("style.compactMode", false) - } - renderActiveTab() { const tab = Tabs[this.state.activeTab] @@ -68,18 +64,17 @@ export default class MusicDashboard extends React.Component { selectedKeys={[this.state.activeTab]} activeKey={this.state.activeTab} onClick={({ key }) => this.handleTabChange(key)} - > - {Object.keys(Tabs).map((key) => { + items={Object.keys(Tabs).map((key) => { const tab = Tabs[key] - return - {tab.title} - + return { + key, + icon: createIconRender(tab.icon), + label: tab.label, + disabled: tab.disabled + } })} - + /> diff --git a/packages/app/src/pages/music/components/dashboard/index.jsx b/packages/app/src/pages/music/components/dashboard/index.jsx new file mode 100644 index 00000000..196eba5c --- /dev/null +++ b/packages/app/src/pages/music/components/dashboard/index.jsx @@ -0,0 +1,130 @@ +import React from "react" +import { Icons } from "components/Icons" +import { ImageViewer } from "components" + +import * as antd from "antd" +import PlaylistsModel from "models/playlists" + +import "./index.less" + +const getReleases = async () => { + const response = await PlaylistsModel.getMyReleases().catch((err) => { + console.error(err) + app.message.error("Failed to load releases") + return null + }) + + return response +} + +const ReleaseItem = (props) => { + const { key, release } = props + + console.log(props) + + return
+
+
+ +
+
+

+ {release.title} +

+ +

+ {release.description} +

+
+
+ +
+ } + > + Modify + +
+
+} + +export default (props) => { + const [releases, setReleases] = React.useState([]) + const [loading, setLoading] = React.useState(false) + + const onClickEditTrack = (track_id) => { + console.log("Edit track", track_id) + + app.setLocation(`/music/creator?playlist_id=${track_id}`) + } + + const loadData = async () => { + setLoading(true) + + const releases = await getReleases() + + setLoading(false) + + console.log(releases) + + if (releases) { + setReleases(releases) + } + } + + React.useEffect(() => { + loadData() + }, []) + + if (loading) { + return + } + + return
+
+

+ + Your releases +

+ +
+ app.setLocation("/music/creator")} + icon={} + type="primary" + > + New release + +
+
+ +
+ { + releases.map((release) => { + return onClickEditTrack(release._id)} + /> + }) + } +
+
+} \ No newline at end of file diff --git a/packages/app/src/pages/music/components/dashboard/index.less b/packages/app/src/pages/music/components/dashboard/index.less new file mode 100644 index 00000000..9379dddd --- /dev/null +++ b/packages/app/src/pages/music/components/dashboard/index.less @@ -0,0 +1,109 @@ +.music_panel_creator { + display: flex; + flex-direction: column; + + width: 100%; + + + .music_panel_releases_header { + display: flex; + flex-direction: row; + + align-items: center; + justify-content: space-between; + + width: 100%; + + .music_panel_releases_header_actions { + display: flex; + flex-direction: row; + + align-items: center; + justify-content: flex-end; + + .ant-btn { + margin-left: 10px; + + &:first-child { + margin-left: 0; + } + } + } + } + + .music_panel_releases_list { + display: flex; + flex-direction: column; + + width: 100%; + + .music_panel_releases_item { + display: flex; + flex-direction: row; + + align-items: center; + justify-content: space-between; + + background-color: var(--background-color-accent); + + border-radius: 8px; + + padding: 10px; + + margin-bottom: 10px; + + &:last-child { + margin-bottom: 0; + } + + .music_panel_releases_info { + display: flex; + flex-direction: row; + + .music_panel_releases_info_cover { + display: flex; + flex-direction: column; + + align-items: center; + justify-content: center; + + margin-right: 10px; + + border-radius: 8px; + + img { + width: 100px; + height: 100px; + + border-radius: 8px; + } + } + + .music_panel_releases_info_title { + display: flex; + flex-direction: column; + + h1 { + margin: 0 !important; + } + } + + } + + .music_panel_releases_actions { + display: flex; + flex-direction: column; + + margin-left: 10px; + + .ant-btn { + margin-bottom: 10px; + + &:last-child { + margin-bottom: 0; + } + } + } + } + } +} \ No newline at end of file diff --git a/packages/app/src/pages/music/components/feed/index.jsx b/packages/app/src/pages/music/components/feed/index.jsx index 23c28085..1caf04b4 100755 --- a/packages/app/src/pages/music/components/feed/index.jsx +++ b/packages/app/src/pages/music/components/feed/index.jsx @@ -2,6 +2,7 @@ import React from "react" import * as antd from "antd" import { ImageViewer } from "components" import { Icons } from "components/Icons" +import { Translation } from "react-i18next" import FeedModel from "models/feed" @@ -18,6 +19,14 @@ const PlaylistItem = (props) => { return app.setLocation(`/play/${playlist._id}`) } + const onClickPlay = (e) => { + e.stopPropagation() + + console.log(playlist.list) + + app.cores.player.startPlaylist(playlist.list) + } + return
{ onClick={onClick} >
- +
@@ -35,6 +44,14 @@ const PlaylistItem = (props) => { {playlist.user.username}
+
+ } + type="primary" + shape="circle" + onClick={onClickPlay} + /> +
} @@ -74,23 +91,12 @@ export default () => { return
-

Releases from your artist

-
-
- { - list.map((playlist, index) => { - return - }) - } -
-
- -
-
-

Discover new trends

+

+ + + {(t) => t("Releases from your artists")} + +

{ diff --git a/packages/app/src/pages/music/components/feed/index.less b/packages/app/src/pages/music/components/feed/index.less index abe710da..215b032d 100755 --- a/packages/app/src/pages/music/components/feed/index.less +++ b/packages/app/src/pages/music/components/feed/index.less @@ -22,18 +22,11 @@ .playlistExplorer_section_list { display: flex; - flex-direction: row; - flex-wrap: nowrap; + //flex-direction: row; + flex-wrap: wrap; + justify-content: center; align-items: center; - - overflow-x: scroll; - - padding: 10px 0; - - >.playlistItem { - margin-right: 20px; - } } } } @@ -41,36 +34,58 @@ .playlistItem { position: relative; display: flex; - flex-direction: column; + flex-direction: row; cursor: pointer; - width: 200px; - min-width: 200px; + width: 45%; + height: 10vh; border-radius: 12px; transition: all 0.2s ease-in-out; - - &:hover { - transform: translate(0, -5px); - outline: 2px solid var(--background-color-accent); - } + margin-right: 20px; background-color: var(--background-color-accent); border-radius: 8px; - .playlistItem_cover { - width: 100%; - height: 100%; + margin-bottom: 20px; - transition: all 0.2s ease-in-out; + &:hover { + .playlistItem_cover { + transform: scale(1.1); + } - filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.25)); + .playlistItem_info { + transform: translateX(10px); + } + } + + .image-wrapper { + width: 10vh; + height: 10vh; + + border-radius: 8px; + + overflow: hidden; img { width: 100%; height: 100%; + + object-fit: cover; + } + } + + .playlistItem_cover { + height: 10vh; + + transition: all 0.2s ease-in-out; + + img { + width: 100%; + height: 100%; + object-fit: cover; border-radius: 8px; } @@ -104,4 +119,21 @@ white-space: nowrap; } } + + .playlistItem_actions { + display: flex; + flex-direction: column; + + justify-content: center; + align-items: center; + + height: 100%; + padding: 10px; + + .ant-btn { + svg { + margin: 0 !important; + } + } + } } \ No newline at end of file diff --git a/packages/app/src/pages/music/creator/index.jsx b/packages/app/src/pages/music/creator/index.jsx new file mode 100644 index 00000000..1e493379 --- /dev/null +++ b/packages/app/src/pages/music/creator/index.jsx @@ -0,0 +1,699 @@ +import React from "react" +import * as antd from "antd" +import classnames from "classnames" +import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd" + +import UploadButton from "components/UploadButton" +import { Icons } from "components/Icons" + +import PlaylistModel from "models/playlists" +import UploadModel from "models/upload" + +import "./index.less" + +const UploadHint = (props) => { + return
+ +

Upload your tracks

+

Drag and drop your tracks here or click this box to start uploading files.

+
+} + +const FileListItem = React.memo((props) => { + console.log(props) + + const isUploading = props.track.status === "uploading" + + return + {(provided, snapshot) => { + return
+ + { + isUploading && + + } + +
+ Track cover { + if (typeof props.onClickChangeCover === "function") { + if (!isUploading) { + props.onClickChangeCover(props.track.uid) + } + } + }} + /> +
+ +
+
+
+ +

Track name

+
+ + +
+ +
+ + } + danger + /> + +
+
+ +
+ +
+
+ } + } +
+}) + +// TODO: Make cover preview style more beautiful (E.G. Use the entire div as background) +// TODO: Make files list item can be dragged to change their order + +export default class PlaylistCreator extends React.Component { + state = { + playlistName: null, + playlistDescription: null, + playlistThumbnail: null, + + fileList: [], + trackList: [], + + pendingUploads: [], + loading: false, + } + + onDragEnd = (result) => { + if (!result.destination) { + return + } + + const trackList = this.state.trackList + + const [removed] = trackList.splice(result.source.index, 1); + trackList.splice(result.destination.index, 0, removed); + + this.setState({ + trackList, + }) + } + + handleTitleOnChange = (event) => { + this.setState({ + playlistName: event.target.value + }) + } + + handleDescriptionOnChange = (event) => { + this.setState({ + playlistDescription: event.target.value + }) + } + + handleTrackTitleOnChange = (event, uid) => { + // find the file in the trackinfo + const file = this.state.trackList.find((file) => file.uid === uid) + + if (file) { + file.title = event.target.value + } + + this.setState({ + trackList: this.state.trackList + }) + } + + handleTrackRemove = (uid) => { + this.setState({ + fileList: this.state.fileList.filter((file) => file.uid !== uid), + trackList: this.state.trackList.filter((file) => file.uid !== uid) + }) + } + + handleTrackCoverChange = (uid) => { + // open a file dialog + const fileInput = document.createElement("input") + + fileInput.type = "file" + fileInput.accept = "image/*" + + fileInput.onchange = (event) => { + const file = event.target.files[0] + + if (file) { + // upload the file + UploadModel.uploadFile(file).then((response) => { + // update the file url in the track info + const file = this.state.trackList.find((file) => file.uid === uid) + + if (file) { + file.thumbnail = response.url + } + + this.setState({ + trackList: this.state.trackList + }) + }) + } + } + + fileInput.click() + } + + removeTrack = (uid) => { + this.setState({ + fileList: this.state.fileList.filter((file) => file.uid !== uid), + trackList: this.state.trackList.filter((file) => file.uid !== uid), + pendingUploads: this.state.pendingUploads.filter((uid) => uid !== uid) + }) + } + + handleUploaderOnChange = (change) => { + console.log(change) + + switch (change.file.status) { + case "uploading": { + const { pendingUploads } = this.state + + if (!pendingUploads.includes(change.file.uid)) { + pendingUploads.push(change.file.uid) + } + + this.setState({ + pendingUploads: pendingUploads, + fileList: [...this.state.fileList, change.file], + trackList: [...this.state.trackList, { + uid: change.file.uid, + title: change.file.name, + source: null, + status: "uploading", + thumbnail: "https://storage.ragestudio.net/comty-static-assets/default_song.png" + }] + }) + + break + } + case "done": { + // remove pending file + this.setState({ + pendingUploads: this.state.pendingUploads.filter(uid => uid !== change.file.uid) + }) + + // update file url in the track info + const track = this.state.trackList.find((file) => file.uid === change.file.uid) + + if (track) { + track.source = change.file.response.url + track.status = "done" + } + + this.setState({ + trackList: this.state.trackList + }) + + break + } + case "error": { + // remove pending file + this.setState({ + pendingUploads: this.state.pendingUploads.filter(uid => uid !== change.file.uid) + }) + + // open a dialog to show the error and ask user to retry + antd.Modal.error({ + title: "Upload failed", + content: "An error occurred while uploading the file. You want to retry?", + cancelText: "No", + okText: "Retry", + onOk: () => { + this.handleUpload(change) + }, + onCancel: () => { + this.removeTrack(change.file.uid) + } + }) + } + case "removed": { + // remove from file list and if it's pending, remove from pending list + this.removeTrack(change.file.uid) + } + + default: { + break + } + } + } + + handleUpload = async (req) => { + const response = await UploadModel.uploadFile(req.file).catch((error) => { + console.error(error) + antd.message.error(error) + + req.onError(error) + + return false + }) + + if (response) { + req.onSuccess(response) + } + } + + checkCanSubmit = () => { + const { playlistName, fileList, pendingUploads, trackList } = this.state + + const nameValid = playlistName !== null && playlistName.length !== 0 + const filesListValid = fileList.length !== 0 + const isPending = pendingUploads.length !== 0 + const isTrackListValid = trackList.every((track) => { + return track.title !== null && track.title?.length !== 0 && track.source !== null && track.source?.length !== 0 + }) + + return nameValid && filesListValid && !isPending && isTrackListValid + } + + handleSubmit = async () => { + this.setState({ + loading: true + }) + + const { playlistName, playlistDescription, playlistThumbnail, trackList } = this.state + + let tracksIds = [] + + // first, publish the tracks + for await (const track of trackList) { + console.log(track) + + let trackPublishResponse = null + + if (typeof track._id === "undefined") { + console.log(`Track ${track.uid} is not published yet. Publishing it...`) + + trackPublishResponse = await PlaylistModel.publishTrack({ + title: track.title, + thumbnail: track.thumbnail, + source: track.source + }).catch((error) => { + console.error(error) + app.message.error(error.response.data.message) + + return false + }) + } else { + console.log(`Track ${track.uid} is already published. Updating...`) + + trackPublishResponse = await PlaylistModel.updateTrack({ + _id: track._id, + title: track.title, + thumbnail: track.thumbnail, + source: track.source + }).catch((error) => { + console.error(error) + app.message.error(error.response.data.message) + + return false + }) + } + + if (trackPublishResponse) { + tracksIds.push(trackPublishResponse._id) + + // update the track id in the track list + const trackList = this.state.trackList + + const trackIndex = trackList.findIndex((track) => track.uid === track.uid) + + if (trackIndex !== -1) { + trackList[trackIndex]._id = trackPublishResponse._id.toString() + } + + this.setState({ + trackList: trackList + }) + } + } + + if (tracksIds.length === 0) { + antd.message.error("Failed to publish tracks") + + this.setState({ + loading: false + }) + + return + } + + let playlistPublishResponse = null + + if (this.props.query.playlist_id) { + console.log(`Playlist ${this.props.query.playlist_id} is already published. Updating...`) + + // update the playlist + playlistPublishResponse = await PlaylistModel.updatePlaylist({ + _id: this.props.query.playlist_id, + title: playlistName, + description: playlistDescription, + thumbnail: playlistThumbnail, + list: tracksIds + }).catch((error) => { + console.error(error) + app.message.error(error.response.data.message) + + return false + }) + } else { + console.log(`Playlist is not published yet. Publishing it...`) + + playlistPublishResponse = await PlaylistModel.publish({ + title: playlistName, + description: playlistDescription, + thumbnail: playlistThumbnail, + list: tracksIds + }).catch((error) => { + console.error(error) + app.message.error(error.response.data.message) + + return false + }) + } + + this.setState({ + loading: false + }) + + if (playlistPublishResponse) { + app.message.success("Playlist published") + + if (typeof this.props.close === "function") { + this.props.close() + } + } + } + + handleDeletePlaylist = async () => { + const action = async () => { + this.setState({ + loading: true + }) + + const deleteResponse = await PlaylistModel.deletePlaylist(this.props.query.playlist_id).catch((error) => { + console.error(error) + antd.message.error(error) + + return false + }) + + this.setState({ + loading: false + }) + + if (deleteResponse) { + app.message.success("Playlist deleted") + + if (typeof this.props.close === "function") { + this.props.close() + } + } + } + + antd.Modal.confirm({ + title: "Delete playlist", + content: "Are you sure you want to delete this playlist?", + onOk: action + }) + } + + __removeExtensionsFromNames = () => { + this.setState({ + trackList: this.state.trackList.map((track) => { + track.title = track.title.replace(/\.[^/.]+$/, "") + + return track + }) + }) + } + + __removeNumbersFromNames = () => { + // remove the order number from the track name ( 01 - trackname.ext => trackname.ext ) + this.setState({ + trackList: this.state.trackList.map((track) => { + track.title = track.title.replace(/^[0-9]{2} - /, "") + + return track + }) + }) + } + + loadData = async (playlist_id) => { + const playlist = await PlaylistModel.getPlaylist(playlist_id).catch((error) => { + console.error(error) + antd.message.error(error) + + return false + }) + + if (playlist) { + const trackList = playlist.list.map((track) => { + return { + _id: track._id, + uid: track._id, + title: track.title, + source: track.source, + status: "done", + thumbnail: track.thumbnail + } + }) + + this.setState({ + playlistName: playlist.title, + playlistDescription: playlist.description, + trackList: trackList, + fileList: trackList.map((track) => { + return { + uid: track.uid, + name: track.title, + status: "done", + url: track.source + } + }) + }) + } + } + + componentDidMount() { + console.log(this.props.query.playlist_id) + + if (this.props.query.playlist_id) { + this.loadData(this.props.query.playlist_id) + } + + window._hacks = { + removeExtensionsFromNames: this.__removeExtensionsFromNames, + removeNumbersFromNames: this.__removeNumbersFromNames + } + } + + componentWillUnmount() { + window._hacks = null + } + + render() { + return
+
+

+ + Creator +

+ +
+ { + this.props.query.playlist_id && + Delete Playlist + + } +
+
+ +
+ + +
+
+ + +
+
+ + { + this.state.playlistThumbnail &&
+ cover + + { + this.setState({ + playlistThumbnail: null + }) + }} + icon={} + shape="round" + > + Remove Cover + +
+ } + { + !this.state.playlistThumbnail && { + this.setState({ + playlistThumbnail: file.url + }) + }} + multiple={false} + accept="image/*" + icon={} + > + Upload cover + + } +
+ +
+ + { + this.state.fileList.length === 0 ? + : }> + Upload files + + } + + + + + {(provided, snapshot) => ( +
+ { + this.state.trackList.map((track, index) => { + return { + return this.handleTrackCoverChange(track.uid) + }} + onTitleChange={(event) => { + return this.handleTrackTitleOnChange(event, track.uid) + }} + onClickRemove={() => { + return this.handleTrackRemove(track.uid) + }} + /> + }) + } + {provided.placeholder} +
+ )} +
+
+
+ +
+ { + this.state.pendingUploads.length !== 0 &&
+

+ + + {this.state.pendingUploads.length} file(s) are being uploaded + +

+
+ } +
+ +
+ } + loading={this.state.loading} + onClick={this.handleSubmit} + > + Publish + +
+ +
+

+ Uploading files that are not permitted by our app.setLocation("/terms")}>Terms of Service may result in your account being suspended. +

+
+
+ } +} \ No newline at end of file diff --git a/packages/app/src/pages/music/creator/index.less b/packages/app/src/pages/music/creator/index.less new file mode 100755 index 00000000..22699646 --- /dev/null +++ b/packages/app/src/pages/music/creator/index.less @@ -0,0 +1,281 @@ +.playlistCreator { + display: flex; + flex-direction: column; + + align-items: center; + justify-content: center; + + width: 100%; + + transition: all 0.3s ease-in-out; + + .header { + display: flex; + flex-direction: row; + + align-items: center; + justify-content: space-between; + + font-size: 1rem; + + width: 100%; + + margin-bottom: 10px; + + .actions { + display: flex; + flex-direction: row; + + align-items: center; + justify-content: flex-end; + + .ant-btn { + margin-left: 10px; + + &:first-child { + margin-left: 0; + } + } + } + } + + .inputField { + display: inline-flex; + flex-direction: row; + + align-self: start; + align-items: center; + justify-content: flex-start; + + width: 100%; + + margin-bottom: 20px; + + font-size: 2rem; + + color: var(--text-color); + + h1, + h2, + h3, + h4, + h5, + h6, + p, + span { + color: var(--text-color); + } + + .inputText { + width: 100%; + color: var(--text-color); + } + + .coverUploader { + width: 100px; + } + + .coverPreview { + height: 5vh; + + img { + height: 100%; + border-radius: 10px; + margin-right: 10px; + } + + svg { + margin: 0 !important; + } + } + } + + .files { + display: flex; + flex-direction: column; + + align-items: center; + justify-content: center; + + border: 1px solid var(--border-color); + border-radius: 8px; + + width: 100%; + height: 100%; + + padding: 10px; + margin-bottom: 20px; + + .ant-upload { + display: flex; + flex-direction: column; + + align-items: center; + justify-content: center; + + width: 100%; + } + + .ant-upload-wrapper { + margin-bottom: 10px; + } + + .fileList { + position: relative; + + display: flex; + flex-direction: column; + width: 100%; + + .fileListItem { + position: relative; + + display: inline-flex; + flex-direction: row; + + align-items: center; + + padding: 10px; + + border-radius: 8px; + + background-color: var(--background-color-accent); + + border: 1px solid var(--border-color); + + margin-bottom: 10px; + + &:last-child { + margin-bottom: 0; + } + + &.uploading { + .fileListItem_cover { + img { + filter: blur(3px); + } + } + } + + .fileListItem_loadingIcon { + position: absolute; + top: 0; + right: 0; + + padding: 10px; + + svg { + margin: 0 !important; + } + } + + .ant-btn { + svg { + margin: 0 !important; + } + } + + .fileListItem_title_label { + display: inline-flex; + flex-direction: row; + + align-items: center; + } + + .fileListItem_cover { + display: flex; + flex-direction: column; + + align-items: center; + justify-content: center; + + cursor: pointer; + + img { + width: 100px; + height: 100px; + object-fit: cover; + border-radius: 12px; + } + } + + .fileListItem_details { + display: flex; + flex-direction: column; + + align-items: flex-start; + justify-content: center; + + margin-left: 10px; + + width: 100%; + + .fileListItem_title { + width: 100%; + + .ant-input { + width: 100%; + } + } + } + + .fileListItem_actions { + display: flex; + flex-direction: row; + + align-items: center; + justify-content: center; + + margin-top: 20px; + + margin-left: 10px; + + .ant-btn { + svg { + margin: 0 !important; + } + + margin-right: 10px; + + &:last-child { + margin-right: 0; + } + } + } + } + + .fileListItem_dragHandle { + svg { + font-size: 1.5rem; + margin: 0 !important; + } + } + } + + } + + .uploadHint { + display: flex; + flex-direction: column; + + align-self: center; + align-items: center; + justify-content: center; + + width: 100%; + height: 100%; + + svg { + font-size: 3rem; + } + + h3 { + font-size: 1.5rem; + } + } + + .footer { + position: relative; + padding: 10px; + } +} \ No newline at end of file diff --git a/packages/app/src/pages/music/tabs.jsx b/packages/app/src/pages/music/tabs.jsx index e21c8246..804f75b6 100755 --- a/packages/app/src/pages/music/tabs.jsx +++ b/packages/app/src/pages/music/tabs.jsx @@ -1,25 +1,28 @@ import FeedTab from "./components/feed" -import SpacesTabs from "./components/spaces" +import SpacesTab from "./components/spaces" +import DashboardTab from "./components/dashboard" export default { + "dashboard": { + label: "Dashboard", + icon: "MdOutlineDashboard", + component: DashboardTab, + }, "feed": { - title: "Feed", + label: "Feed", icon: "Compass", component: FeedTab }, "library": { - title: "Library", + label: "Library", icon: "MdLibraryMusic", - component: FeedTab - }, - "dashboard": { - title: "Dashboard", - icon: "MdOutlineDashboard", - component: FeedTab + component: FeedTab, + disabled: true }, "spaces": { - title: "Spaces", + label: "Spaces", icon: "MdDeck", - component: SpacesTabs + component: SpacesTab, + disabled: true }, } \ No newline at end of file