diff --git a/packages/app/src/pages/music/creator/components/BasicInformation/index.jsx b/packages/app/src/pages/music/creator/components/BasicInformation/index.jsx new file mode 100644 index 00000000..0b945f64 --- /dev/null +++ b/packages/app/src/pages/music/creator/components/BasicInformation/index.jsx @@ -0,0 +1,151 @@ +import React from "react" +import * as antd from "antd" +import { Icons } from "components/Icons" +import UploadButton from "components/UploadButton" + +export default (props) => { + const [playlistName, setPlaylistName] = React.useState(props.playlist.title) + const [playlistDescription, setPlaylistDescription] = React.useState(props.playlist.description) + const [playlistThumbnail, setPlaylistThumbnail] = React.useState(props.playlist.thumbnail) + const [playlistVisibility, setPlaylistVisibility] = React.useState(props.playlist.visibility) + + const handleTitleOnChange = (event) => { + setPlaylistName(event.target.value) + + props.onTitleChange(event.target.value) + } + + const handleDescriptionOnChange = (event) => { + setPlaylistDescription(event.target.value) + + props.onDescriptionChange(event.target.value) + } + + const handleCoverChange = (file) => { + setPlaylistThumbnail(file.url) + + props.onPlaylistCoverChange(file.url) + } + + const handleRemoveCover = () => { + setPlaylistThumbnail(null) + + props.onPlaylistCoverChange(null) + } + + const handleVisibilityChange = (value) => { + setPlaylistVisibility(value) + + props.onVisibilityChange(value) + } + + return
+
+
+
+ + Title +
+ + +
+ +
+
+ + Description +
+ + +
+ + + +
+
+ + Visibility +
+ + + Public + Private + +
+
+ +
+
+
+ + Cover +
+ +
+
+ Thumbnail +
+ +
+ + Upload cover + + + } + type="text" + > + Remove Cover + +
+
+
+ + + +
+ } + danger + > + Delete Playlist + +
+
+
+} diff --git a/packages/app/src/pages/music/creator/components/TracksUploads/index.jsx b/packages/app/src/pages/music/creator/components/TracksUploads/index.jsx new file mode 100644 index 00000000..910f0a78 --- /dev/null +++ b/packages/app/src/pages/music/creator/components/TracksUploads/index.jsx @@ -0,0 +1,323 @@ +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 "./index.less" + +const UploadHint = (props) => { + return
+ +

Upload your tracks

+

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

+
+} + +const FileItemEditor = (props) => { + const [track, setTrack] = React.useState(props.track ?? {}) + + const handleChange = (key, value) => { + setTrack((oldData) => { + return { + ...oldData, + [key]: value + } + }) + } + + const onClose = () => { + if (typeof props.close === "function") { + props.close() + } + } + + const onSave = async () => { + await props.onSave(track) + + if (typeof props.close === "function") { + props.close() + } + } + + return
+
+
+ + Thumbnail +
+ +
+ +
+ +
+ handleChange("thumbnail", file.url)} + /> + { + track.thumbnail && } + type="text" + onClick={() => handleChange("thumbnail", null)} + > + Remove + + } +
+
+ +
+
+ + Title +
+ + handleChange("title", e.target.value)} + /> +
+ +
+
+ + Artist +
+ + handleChange("artist", e.target.value)} + /> +
+ +
+
+ + Album +
+ + handleChange("album", e.target.value)} + /> +
+ +
+
+ + Explicit +
+ + handleChange("explicit", value)} + /> +
+ +
+ } + onClick={onClose} + > + Cancel + + + } + onClick={onSave} + > + Save + +
+
+} + +const FileListItem = (props) => { + const isUploading = props.track.status === "uploading" + + return + {(provided, snapshot) => { + return
+ + { + isUploading && + + } + +
+ Track cover +
+ +
+
+

+ + {props.index + 1} - + + + { + props.track?.title ?? "Unknown title" + } +

+ +

+ + { + props.track?.artist ?? "Unknown artist" + } + + + - + + + { + props.track?.album ?? "Unknown album" + } + +

+
+ +
+ } + onClick={props.onClickEdit} + disabled={isUploading} + /> + + + } + /> + +
+
+ +
+ +
+
+ }} +
+} + +export default (props) => { + const onClickEditTrack = (track) => { + console.log("Editing track", track) + + app.DrawerController.open("track_editor", FileItemEditor, { + type: "drawer", + componentProps: { + track, + onSave: (newTrackData) => { + props.handleTrackInfoChange(newTrackData.uid, newTrackData) + } + }, + }) + } + + return
+

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

+ +
+ + { + props.fileList.length === 0 ? + : } + /> + } + + +
+ + + {(provided, snapshot) => ( +
+ { + props.trackList.map((track, index) => { + return { + return props.handleTrackCoverChange(track.uid) + }} + onTitleChange={(event) => { + return props.handleTrackInfoChange(track.uid, "title", event.target.value) + }} + onClickRemove={() => { + return props.handleTrackRemove(track.uid) + }} + onClickEdit={() => { + return onClickEditTrack(track) + }} + /> + }) + } + {provided.placeholder} +
+ )} +
+
+
+
+
+} \ No newline at end of file diff --git a/packages/app/src/pages/music/creator/components/TracksUploads/index.less b/packages/app/src/pages/music/creator/components/TracksUploads/index.less new file mode 100644 index 00000000..f8d0a6c4 --- /dev/null +++ b/packages/app/src/pages/music/creator/components/TracksUploads/index.less @@ -0,0 +1,344 @@ +.tracksUploads { + position: relative; + + display: flex; + flex-direction: column; + + align-items: center; + + width: 100%; + height: 100%; + + .uploadBox { + position: relative; + + width: 100%; + //height: 100%; + + padding: 10px; + + overflow-y: hidden; + + border: 1px solid var(--border-color); + + border-radius: 8px; + + .ant-upload { + display: flex; + flex-direction: column; + + align-items: center; + justify-content: center; + + width: 100%; + + .uploadMoreButton { + width: 100%; + height: 30px; + + margin-bottom: 10px; + + background-color: transparent; + + outline: 1px solid var(--border-color); + + box-shadow: none; + + &:hover { + height: 70px; + background-color: var(--background-color-accent); + } + } + } + + .ant-upload-wrapper { + margin-bottom: 10px; + } + + .fileList_wrapper { + position: relative; + + border-radius: 8px; + + overflow: hidden; + + // add a blur effect on top and bottom to decorate overflown content + &::before, + &::after { + z-index: 50; + content: ""; + + position: absolute; + + width: 100%; + height: 10px; + } + + &::before { + top: 0; + left: 0; + + background: linear-gradient(0deg, + rgba(255, 255, 255, 0) 0%, + var(--background-color-primary) 40%); + } + + &::after { + bottom: 0; + left: 0; + + background: linear-gradient(180deg, + rgba(255, 255, 255, 0) 0%, + var(--background-color-primary) 40%); + } + } + + .fileList { + display: flex; + flex-direction: column; + + max-height: 60vh; + + overflow-y: scroll; + + padding: 15px 0; + + gap: 10px; + + .fileListItem { + position: relative; + + z-index: 49; + + display: inline-flex; + flex-direction: row; + + align-items: center; + + padding: 10px; + + border-radius: 8px; + + background-color: var(--background-color-accent); + color: var(--text-color); + + border: 1px solid var(--border-color); + + gap: 10px; + + &.uploading { + .fileListItem_cover { + img { + filter: blur(3px); + } + + pointer-events: none; + } + } + + .fileListItem_loadingIcon { + position: absolute; + top: 0; + right: 0; + + padding: 10px; + + svg { + margin: 0 !important; + } + } + + .ant-btn { + svg { + margin: 0 !important; + } + } + + .fileListItem_cover { + display: flex; + flex-direction: column; + + align-items: center; + justify-content: center; + + transition: all 0.2s ease-in-out; + + 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: 20px; + + width: 100%; + + .fileListItem_namings { + width: 100%; + + p { + font-size: 0.8rem; + } + } + + .fileListItem_artist { + width: 100%; + + span { + font-size: 0.8rem; + + margin-right: 10px; + } + } + } + + .fileListItem_actions { + display: flex; + flex-direction: row; + + align-items: center; + justify-content: center; + + .ant-btn { + svg { + margin: 0 !important; + } + + margin-right: 10px; + + &:last-child { + margin-right: 0; + } + } + } + } + + .fileListItem_dragHandle { + color: var(--text-color); + + 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; + } + } +} + +.fileItemEditor { + display: flex; + + flex-direction: column; + + align-items: center; + + gap: 20px; + + .fileItemEditor_actions { + display: flex; + flex-direction: row; + + align-items: center; + justify-content: flex-end; + + align-self: center; + + gap: 10px; + } + + .fileItemEditor_field { + display: flex; + flex-direction: column; + + align-items: flex-start; + + gap: 10px; + + width: 100%; + + .fileItemEditor_field_header { + display: inline-flex; + flex-direction: row; + + justify-content: flex-start; + + align-items: center; + + width: 100%; + + //margin-bottom: 10px; + + h3 { + font-size: 1.2rem; + } + } + + .fileItemEditor_field_actions { + display: flex; + flex-direction: row; + + align-items: center; + justify-content: center; + + gap: 10px; + + width: 100%; + } + + .fileItemEditor_field_thumnail { + display: flex; + flex-direction: column; + + align-items: center; + justify-content: center; + + width: 100%; + + img { + width: 15vw; + height: 15vw; + + max-width: 330px; + max-height: 330px; + + object-fit: cover; + + border-radius: 12px; + + background-color: black; + + } + } + } +} \ 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 index 5fcf4481..23b09169 100755 --- a/packages/app/src/pages/music/creator/index.jsx +++ b/packages/app/src/pages/music/creator/index.jsx @@ -1,222 +1,285 @@ 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 jsmediatags from "jsmediatags/dist/jsmediatags.min.js" import PlaylistModel from "models/playlists" import FilesModel from "models/files" +import BasicInformation from "./components/BasicInformation" +import TracksUploads from "./components/TracksUploads" + 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 allowedTrackFieldChanges = [ + "title", + "artist", + "thumbnail", + "album", + "year", + "genre", + "comment", + "explicit", +] -const FileListItem = React.memo((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 { +export default class PlaylistCreatorSteps extends React.Component { state = { - playlistName: null, - playlistDescription: null, - playlistThumbnail: null, + playlistData: {}, fileList: [], trackList: [], + pendingTracksUpload: [], - pendingUploads: [], - loading: false, + loading: true, + submitting: false, + + currentStep: 0, } - onDragEnd = (result) => { + updatePlaylistData = (key, data) => { + this.setState({ + playlistData: { + ...this.state.playlistData, + [key]: data + } + }) + } + + updateTrackList = (trackList) => { + this.setState({ + trackList + }) + } + + canSubmit = () => { + const { playlistData, trackList, pendingTracksUpload } = this.state + + const hasValidTitle = playlistData.title && playlistData.title.length > 0 + const hasTracks = trackList.length > 0 + const hasPendingUploads = pendingTracksUpload.length > 0 + const tracksHasValidData = trackList.every((track) => { + return track.title !== null && track.title?.length !== 0 && track.source !== null && track.source?.length !== 0 + }) + + return hasValidTitle && hasTracks && !hasPendingUploads && tracksHasValidData + } + + submit = async () => { + this.setState({ + submitting: true + }) + + const { playlistData, trackList } = this.state + + console.log(`Submitting playlist ${playlistData.title} with ${trackList.length} tracks`, playlistData, trackList) + + const result = await PlaylistModel.putPlaylist({ + ...playlistData, + list: trackList, + }) + + this.setState({ + submitting: false + }) + + if (result) { + app.message.success("Playlist published") + + if (typeof this.props.close === "function") { + this.props.close() + } + } + } + + // TRACK UPLOADS METHODS + analyzeTrackMetadata = async (file) => { + return new Promise((resolve, reject) => { + jsmediatags.read(file, { + onSuccess: (data) => { + return resolve(data) + }, + onError: (error) => { + return reject(error) + } + }) + }) + } + + handleUploadTrack = async (req) => { + const response = await FilesModel.uploadFile(req.file, { + timeout: 2000 + }).catch((error) => { + console.error(error) + antd.message.error(error) + + req.onError(error) + + return false + }) + + if (response) { + req.onSuccess(response) + } + } + + handleTrackDragEnd = (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); + 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 - }) - } - - 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 - FilesModel.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) => { + handleTrackRemove = (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((file_uid) => file_uid !== uid) + pendingTracksUpload: this.state.pendingTracksUpload.filter((file_uid) => file_uid !== uid) }) } - handleUploaderOnChange = (change) => { - console.log(change) + handleTrackInfoChange = (uid, key, value) => { + if (!uid) { + console.error(`Cannot update track withouth uid`) + return + } + let trackList = this.state.trackList + + const track = trackList.find((track) => track.uid === uid) + + if (typeof key === "object") { + allowedTrackFieldChanges.forEach((_key) => { + if (key[_key]) { + track[_key] = key[_key] + } + }) + } else { + if (!allowedTrackFieldChanges.includes(key)) { + console.error(`Cannot update track with key ${key}`) + return + } + + track[key] = value + } + + this.setState({ + trackList: trackList + }) + + console.log(`New data for track ${uid}: `, this.state.trackList.find((track) => track.uid === uid)) + } + + handleTrackCoverChange = async (uid, file) => { + if (!uid) { + console.error(`Cannot update track withouth uid`) + return + } + + // upload cover file + const result = await FilesModel.uploadFile(file, { + timeout: 2000 + }) + + if (result) { + this.handleTrackInfoChange(uid, "thumbnail", result.url) + } + } + + handleDeletePlaylist = async () => { + if (!this.props.playlist_id) { + console.error(`Cannot delete playlist without id`) + return + } + + antd.Modal.confirm({ + title: "Are you sure you want to delete this playlist?", + content: "This action cannot be undone", + okText: "Delete", + okType: "danger", + cancelText: "Cancel", + onOk: async () => { + const result = await PlaylistModel.deletePlaylist(this.props.playlist_id) + + if (result) { + app.message.success("Playlist deleted") + + if (typeof this.props.close === "function") { + this.props.close() + } + } + } + }) + } + + onTrackUploaderChange = async (change) => { switch (change.file.status) { case "uploading": { - const { pendingUploads } = this.state + const { pendingTracksUpload } = this.state - if (!pendingUploads.includes(change.file.uid)) { - pendingUploads.push(change.file.uid) + if (!pendingTracksUpload.includes(change.file.uid)) { + pendingTracksUpload.push(change.file.uid) + } + + const trackMetadata = await this.analyzeTrackMetadata(change.file.originFileObj) + .catch((error) => { + console.error(`Failed to analyze track metadata: `, error) + + // return empty metadata + return { + tags: {} + } + }) + + console.log(trackMetadata) + + if (trackMetadata.tags.picture) { + const data = trackMetadata.tags.picture.data + const format = trackMetadata.tags.picture.format + + if (data && format) { + console.log(data, format) + + const filenameExt = format.split("/")[1] + const filename = `cover.${filenameExt}` + + const byteArray = new Uint8Array(data) + const blob = new Blob([byteArray], { type: data.type }) + + // create a file object + const file = new File([blob], filename, { + type: format, + }) + + console.log(file) + + this.handleTrackCoverChange(change.file.uid, file) + } } this.setState({ - pendingUploads: pendingUploads, + pendingTracksUpload: pendingTracksUpload, fileList: [...this.state.fileList, change.file], trackList: [...this.state.trackList, { uid: change.file.uid, - title: change.file.name, + title: trackMetadata.tags.title ?? change.file.name, + artist: trackMetadata.tags.artist ?? null, + album: trackMetadata.tags.album ?? null, + metadata: { + duration: undefined, + bitrate: undefined, + sampleRate: undefined, + channels: undefined, + codec: undefined, + format: undefined, + }, source: null, status: "uploading", thumbnail: "https://storage.ragestudio.net/comty-static-assets/default_song.png" @@ -228,7 +291,7 @@ export default class PlaylistCreator extends React.Component { case "done": { // remove pending file this.setState({ - pendingUploads: this.state.pendingUploads.filter(uid => uid !== change.file.uid) + pendingTracksUpload: this.state.pendingTracksUpload.filter((uid) => uid !== change.file.uid) }) // update file url in the track info @@ -247,7 +310,7 @@ export default class PlaylistCreator extends React.Component { } case "error": { // remove pending file - this.removeTrack(change.file.uid) + this.handleTrackRemove(change.file.uid) // open a dialog to show the error and ask user to retry antd.Modal.error({ @@ -259,12 +322,12 @@ export default class PlaylistCreator extends React.Component { this.handleUpload(change) }, onCancel: () => { - this.removeTrack(change.file.uid) + this.handleTrackRemove(change.file.uid) } }) } case "removed": { - this.removeTrack(change.file.uid) + this.handleTrackRemove(change.file.uid) } default: { @@ -273,232 +336,102 @@ export default class PlaylistCreator extends React.Component { } } - handleUpload = async (req) => { - const response = await FilesModel.uploadFile(req.file, { - timeout: 2000 - }).catch((error) => { - console.error(error) - antd.message.error(error) - - req.onError(error) - - return false - }) - - if (response) { - req.onSuccess(response) + steps = [ + { + title: "Information", + crender: BasicInformation, + }, + { + title: "Tracks", + crender: TracksUploads, } - } + ] - checkCanSubmit = () => { - const { playlistName, fileList, pendingUploads, trackList } = this.state + onChangeStep = (toStep) => { + // check if can change step + if (toStep > this.state.currentStep) { + if (!this.canNextStep()) { + return + } + } - 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 + currentStep: toStep }) + } - 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 - }) - + nextStep = () => { + if (!this.canNextStep()) { return } - let playlistPublishResponse = null + const nextStep = this.state.currentStep + 1 - if (this.props.playlist_id) { - console.log(`Playlist ${this.props.playlist_id} is already published. Updating...`) - - // update the playlist - playlistPublishResponse = await PlaylistModel.updatePlaylist({ - _id: this.props.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 - }) + if (nextStep >= this.steps.length) { + return this.submit() } this.setState({ - loading: false + currentStep: nextStep }) + } - if (playlistPublishResponse) { - app.message.success("Playlist published") + previousStep = () => { + const previusStep = this.state.currentStep - 1 - if (typeof this.props.close === "function") { - this.props.close() - } + if (previusStep < 0) { + return + } + + this.setState({ + currentStep: previusStep + }) + } + + canNextStep = () => { + // check current step + switch (this.state.currentStep) { + case 0: + return typeof this.state.playlistData.title === "string" && this.state.playlistData.title.length > 0 + case 1: + return this.canSubmit() + default: + return true } } - handleDeletePlaylist = async () => { - const action = async () => { - this.setState({ - loading: true - }) - - const deleteResponse = await PlaylistModel.deletePlaylist(this.props.playlist_id).catch((error) => { - console.error(error) - antd.message.error(error) - - return false - }) - + componentDidMount() { + if (this.props.playlist_id) { + this.loadPlaylistData(this.props.playlist_id) + } else { 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(/\.[^/.]+$/, "") + loadPlaylistData = async (playlist_id) => { + console.log(`Loading playlist data for playlist ${playlist_id}...`) - 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) => { + const playlistData = await PlaylistModel.getPlaylist(playlist_id).catch((error) => { console.error(error) antd.message.error(error) return false }) - if (playlist) { - const trackList = playlist.list.map((track) => { + if (playlistData) { + const trackList = playlistData.list.map((track) => { return { + ...track, _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, + playlistData: playlistData, trackList: trackList, fileList: trackList.map((track) => { return { @@ -510,177 +443,73 @@ export default class PlaylistCreator extends React.Component { }) }) } - } - componentDidMount() { - console.log(this.props.playlist_id) - - if (this.props.playlist_id) { - this.loadData(this.props.playlist_id) - } - - window._hacks = { - removeExtensionsFromNames: this.__removeExtensionsFromNames, - removeNumbersFromNames: this.__removeNumbersFromNames - } - } - - componentWillUnmount() { - window._hacks = null + this.setState({ + loading: false + }) } render() { + if (this.state.loading) { + return + } + return
-
- - -
-
- - - -
-
- + +
{ - this.state.playlistThumbnail &&
- cover + React.createElement(this.steps[this.state.currentStep].crender, { + playlist: this.state.playlistData, - { - this.setState({ - playlistThumbnail: null - }) - }} - icon={} - shape="round" - > - Remove Cover - -
- } + trackList: this.state.trackList, + fileList: this.state.fileList, - { - !this.state.playlistThumbnail && { - this.setState({ - playlistThumbnail: file.url - }) - }} - multiple={false} - accept="image/*" - > - Upload cover - + onTitleChange: (title) => { + this.updatePlaylistData("title", title) + }, + onDescriptionChange: (description) => { + this.updatePlaylistData("description", description) + }, + onPlaylistCoverChange: (url) => { + this.updatePlaylistData("thumbnail", url) + }, + onVisibilityChange: (visibility) => { + this.updatePlaylistData("visibility", visibility) + }, + onDeletePlaylist: this.handleDeletePlaylist, + + handleUploadTrack: this.handleUploadTrack, + handleTrackDragEnd: this.handleTrackDragEnd, + handleTrackRemove: this.handleTrackRemove, + handleTrackInfoChange: this.handleTrackInfoChange, + onTrackUploaderChange: this.onTrackUploaderChange, + }) }
-
- - { - 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.removeTrack(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} + onClick={this.previousStep} + disabled={this.state.currentStep === 0} > - Publish + Previous - { - this.props.playlist_id && - Delete Playlist - - } -
- -
-

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

+ + { + this.state.currentStep === this.steps.length - 1 ? "Finish" : "Next" + } +
} diff --git a/packages/app/src/pages/music/creator/index.less b/packages/app/src/pages/music/creator/index.less index 6d40cdbd..e707d596 100755 --- a/packages/app/src/pages/music/creator/index.less +++ b/packages/app/src/pages/music/creator/index.less @@ -1,14 +1,68 @@ .playlistCreator { + position: relative; + display: flex; flex-direction: column; align-items: center; - justify-content: center; + //justify-content: center; - width: 100%; + width: 50vw; + height: 100%; + + min-width: 800px; transition: all 0.3s ease-in-out; - + + .stepContent { + display: flex; + flex-direction: column; + + width: 100%; + height: 100%; + + padding: 20px 0; + } + + .stepActions { + position: sticky; + + bottom: 0; + right: 0; + + display: flex; + flex-direction: row; + + align-items: center; + //justify-content: flex-end; + + width: 100%; + padding: 20px; + + gap: 10px; + + background-color: var(--background-color-accent); + border-radius: 12px; + } + + .playlistCreator_layout_row { + display: flex; + flex-direction: row; + + width: 100%; + + gap: 20px; + + .playlistCreator_layout_column { + display: flex; + flex-direction: column; + + width: 100%; + + gap: 20px; + } + } + .actions { display: flex; flex-direction: row; @@ -19,256 +73,80 @@ gap: 20px; } - .inputField { + .field { display: inline-flex; - flex-direction: row; + flex-direction: column; align-self: start; - align-items: center; - justify-content: flex-start; width: 100%; margin-bottom: 20px; - font-size: 2rem; + gap: 10px; color: var(--text-color); - h1, - h2, - h3, - h4, - h5, - h6, - p, - span { - color: var(--text-color); + .field_header { + font-size: 1rem; } .inputText { width: 100%; - color: var(--text-color); - } - .coverUploader { - width: 100px; - } + background-color: transparent; - .coverPreview { - height: 5vh; + outline: 1px solid var(--border-color); + } + } + + .coverPreview { + display: flex; + flex-direction: column; + + gap: 10px; + + .coverPreview_preview { + align-self: center; + + width: 15vw; + height: 15vw; + + max-width: 300px; + max-height: 300px; img { - height: 100%; - border-radius: 10px; - margin-right: 10px; - } + width: 15vw; + height: 15vw; - svg { - margin: 0 !important; + max-width: 300px; + max-height: 300px; + + border-radius: 12px; + + object-fit: cover; } } - } - .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 { + .coverPreview_actions { display: flex; - flex-direction: column; + flex-direction: row; align-items: center; - justify-content: center; + + gap: 10px; 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); - } - - pointer-events: none; - } - } - - .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_cover_mask { - opacity: 0; - backdrop-filter: blur(3px); - } - - &:hover { - .fileListItem_cover_mask { - opacity: 1; - } - } - } - - .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; + margin: 0 !important; } } - .footer { - position: relative; - padding: 10px; + .ant-steps-icon { + svg { + margin: 0 !important; + } } } \ No newline at end of file